Compare commits

...
54 Commits
Author SHA1 Message Date
Joshua KingandCursor aed97280b8 Add Right Porch Outlet 2 to porch decoration automations.
Turn the outlet on with the sunset schedule and off at bedtime, 9 AM, and the midnight decoration sweep.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-02 12:56:15 -05:00
root 5f0080e11c updates 2026-11-02 08:44:07 -05:00
Joshua KingandCursor af7f268888 Derive Centauri progress from layers or print time when percent stays at zero.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 14:02:50 -04:00
Joshua KingandCursor 994cfb6b60 Fill overview gauges to each printer's actual progress.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-20 21:00:03 -04:00
Joshua KingandCursor 12f8ba2def Show a grey ring behind black filament gauges so they stay visible.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-20 20:46:41 -04:00
Joshua KingandCursor 5b8a3f24c2 Format the overview clock through a mutable ESPTime so ESPHome 2026.9.1 compiles.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-20 19:34:52 -04:00
Joshua KingandCursor c5b7408b18 Color overview gauges by active filament and show finish times.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-20 19:22:04 -04:00
Joshua KingandCursor 360e86e46a Show each printer on the fleet page as a status gauge.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-20 15:13:24 -04:00
root 4626019a3e automation updates 2026-10-20 14:30:20 -04:00
Joshua KingandCursor 43f416ab69 Add a fleet overview page that shows every printer's status at once.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-20 13:05:32 -04:00
Joshua KingandCursor 0ec94a909e Add a second ESPHome fleet display that shares the existing printer pages.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-20 11:24:47 -04:00
Joshua King 5e50c9a296 Refactor automation configurations by removing obsolete NestQuest access controls and updating automation inclusion in configuration.yaml 2026-10-19 21:08:59 -04:00
root 4104e995c0 Merge branch 'main' of https://code.cubecraftlabs.com/overseer/Home-Assistant 2026-10-19 20:10:14 -04:00
root d7f0584e17 updates 2026-10-19 20:03:46 -04:00
Joshua King 472b66cf0e updated blueprints 2026-10-19 16:18:52 -04:00
root 17633d5909 Merge branch 'main' of https://code.cubecraftlabs.com/overseer/Home-Assistant 2026-10-19 15:04:00 -04:00
Joshua King c469aa4aae Add Elegoo printers to ESPHome fleet display 2026-10-19 11:24:02 -04:00
ESPHome Device Builder 0bd960dbf7 Delete led-controller-polly-a1mini.yaml 2026-10-18 09:32:25 -04:00
ESPHome Device Builder 5a0ccf6b36 Delete esp32-h2c-camera.yaml 2026-10-18 09:32:05 -04:00
ESPHome Device Builder bdd3d4b0ae Delete family-room-remote.yaml 2026-10-18 09:31:59 -04:00
ESPHome Device Builder 91c3996f93 Delete living-room-remote.yaml 2026-10-18 09:31:47 -04:00
Joshua King 3c940f8027 Fix Orion AMS mapping and show printer health details 2026-10-18 09:29:07 -04:00
Joshua King f672de484e updates 2026-10-17 17:26:25 -04:00
root 19adc71fef Merge branch 'main' of https://code.cubecraftlabs.com/overseer/Home-Assistant 2026-10-17 17:10:03 -04:00
Joshua King 495efeb672 updates 2026-10-17 17:09:47 -04:00
ESPHome Device Builder 14c50015a6 Edit living-room.yaml 2026-10-17 16:32:29 -04:00
Joshua King 5e51a1f9aa Add Home Assistant controls for resin curing timer 2026-10-17 15:33:27 -04:00
Joshua King 34c4e54c13 updates 2026-10-17 15:19:53 -04:00
root f6f922f364 updates 2026-10-17 15:13:22 -04:00
ESPHome Device Builder 9d14e8a513 Edit resin-curing-timer.yaml 2026-10-17 15:12:20 -04:00
Joshua King 5bddada3b8 adding in resin timer 2026-10-17 15:04:18 -04:00
root 7b9d81b21e Merge branch 'main' of https://code.cubecraftlabs.com/overseer/Home-Assistant 2026-10-14 08:07:53 -04:00
Joshua King e7e22b48fb fix(automations): remove dining room media player from announcement targets
Removed media_player.dinning_room from the target lists across multiple
announcement automations, including Declan and Jordyn's family
announcements, bedtime routines, and the school announcement volume
automation. This speaker no longer participates in household
announcements.
2026-10-14 08:07:35 -04:00
ESPHome Device Builder 8c48e04ad9 Delete esp32-saturn4-cam.yaml 2026-10-12 19:30:10 -04:00
Joshua King d28e388eda feat(automations): update descriptions and add Outside Lamppost to lighting schedules 2026-10-10 14:16:42 -04:00
Joshua King 35d815de79 feat: add litter box alert and kiosk dashboard automations
Add new automation files to the Home Assistant configuration:

- 02a_litter_box_alerts.yaml: Notifications for multiple litter boxes
  (Poop Box, Triple Threat, Cat Crapper 3000, The Deuce, and Luma
  Smart) covering waste bin full, low litter level, door error, and
  deodorant empty alerts, leveraging shared notification blueprints
- 06a_kiosk_dashboards.yaml: Automations for managing kiosk dashboards

These automations centralize pet-care and maintenance notifications
so alerts are triggered consistently across devices.
2026-10-08 14:47:08 -04:00
Joshua King 2f4108eab5 feat(automations): expand 9AM shutoff and disable fan rule
Add switch.turn_off actions to the first floor 9AM automation so the
washing machine socket, family room lamp, studio plug, and porch
decorations outlets are also powered off alongside the lights, with
continue_on_error to ensure remaining actions still run. Comment out
the parents ceiling fan cold-weather automation to temporarily disable
its temperature-based turn-off behavior.
2026-10-08 14:05:45 -04:00
Joshua KingandClaude Opus 5.5 72d92e46f6 Restore 05:30 fireplace mantle outlet morning schedule
The morning time trigger was replaced by the motion trigger in f076657,
so the outlet stopped turning on at 05:30. Re-add it as a separate
automation alongside the motion-based one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 07:07:00 -04:00
root fc8d869f69 updates 2026-10-05 16:11:37 -04:00
Joshua King f076657834 Add wax warmer automation for fireplace with motion detection and occupancy timeout 2026-10-02 13:11:51 -04:00
Joshua King f2514f8b22 Add holiday and kids' automation scripts, including notifications and schedules
- Introduced holiday automations for Christmas and Halloween, including turning decorations on/off and playing music.
- Added day camp announcements and reminders for kids, including preparation and departure notifications.
- Created specific automation scripts for Declan and Jordyn, focusing on medication reminders and school preparations.
- Implemented daily schedules for kids during summer, with structured activities and announcements.
- Added blueprints for litter notifications and tank moisture interlocks to enhance home automation.
- Established a family schedule package to manage school dates and events effectively.
2026-10-02 08:57:53 -04:00
Joshua King 057744abce Add lampstand control to Living Room remote with toggle functionality 2026-09-29 21:09:30 -04:00
Joshua King 8ebac52160 Refactor binary sensor initial state handling and update OTA configuration for remote panels 2026-09-29 21:01:14 -04:00
Joshua King 0be12509d1 Add Family Room and Living Room remote configurations with touch functionality 2026-09-29 20:57:26 -04:00
overseer 64249539d3 updates 2026-09-29 20:54:55 -04:00
root c69a867a2e Because.
Merge branch 'main' of https://code.cubecraftlabs.com/overseer/Home-Assistant
2026-09-29 20:52:55 -04:00
root 9cabeaa06b Merge branch 'main' of https://code.cubecraftlabs.com/overseer/Home-Assistant 2026-09-29 20:50:55 -04:00
ESPHome Device Builder 352f59f4cf Delete family-room-remote.yaml 2026-09-29 20:39:54 -04:00
ESPHome Device Builder f46a8c7f58 Delete living-room-remote.yaml 2026-09-29 20:39:49 -04:00
root 25d4744f45 Merge branch 'main' of https://code.cubecraftlabs.com/overseer/Home-Assistant 2026-09-29 20:37:01 -04:00
root 71f8b14408 Merge branch 'main' of https://code.cubecraftlabs.com/overseer/Home-Assistant 2026-09-29 07:53:03 -04:00
ESPHome Device Builder 8ad42f1d7f Edit airqualitysensor-3.yaml 2026-09-20 11:44:38 -04:00
ESPHome Device Builder d7da8a7805 Edit airqualitysensor-2.yaml 2026-09-20 11:44:38 -04:00
ESPHome Device Builder ac3d126bc1 Edit airqualitysensor-1.yaml 2026-09-20 11:44:38 -04:00
83 changed files with 14730 additions and 4038 deletions
+4
View File
@@ -61,6 +61,9 @@
### ESPHome Folders ###
!esphome/
!esphome/fonts/**
!esphome/bambu-display/
!esphome/bambu-display/**
esphome/bambu-display/**/__pycache__/
!esphome/ha-remote/
!esphome/ha-remote/components/
!esphome/ha-remote/components/**
@@ -101,6 +104,7 @@ ip_bans.yaml
# =============================================
# Generated files and folders - always ignore
# =============================================
esphome/.device-builder.json
.storage
.cloud
.google.token
+34
View File
@@ -0,0 +1,34 @@
- id: '1792458749185'
alias: Declan - NestQuest Access Block
description: Blocks network access for Declan's devices once the remaining daily
quest sensor reaches zero.
use_blueprint:
path: Maniacs-Mansion/unifi_quest_access.yaml
input:
remaining_sensor: sensor.nestquest_declan_quests_remaining_today
client_switches:
- switch.declan_iphone_blocked
- switch.default_blocked_2
- switch.iphone_blocked_2
- switch.iphone_blocked_3
- id: '1792458829477'
alias: Jordyn - NestQuest Access Block
description: Automatically blocks network access for Jordyn's devices once the remaining
daily quests sensor reaches zero.
use_blueprint:
path: nestquest/unifi_quest_access.yaml
input:
remaining_sensor: sensor.nestquest_jordyn_quests_remaining_today
client_switches:
- switch.jordyn_iphone_blocked
- switch.default_blocked_3
- id: '1792458886560'
alias: Chloe - NestQuest Access Block
description: Automatically blocks network access for Chloe's device once the daily
NestQuest allowance has been exhausted.
use_blueprint:
path: nestquest/unifi_quest_access.yaml
input:
remaining_sensor: sensor.nestquest_chloe_quests_remaining_today
client_switches:
- switch.switch_v1_blocked
-987
View File
@@ -1,987 +0,0 @@
# Home and lighting automations.
- id: '1760000000001'
alias: Fireplace mantle outlet morning schedule
description: Turn the fireplace mantle outlet on each morning.
triggers:
- trigger: time
at: '05:30:00'
conditions: []
actions:
- action: switch.turn_on
target:
entity_id: switch.fireplace
mode: single
# Evening living-room motion automations disabled.
#
# - id: '1738951431044'
# alias: Turn off living room lights
# description: ''
# triggers:
# - entity_id:
# - binary_sensor.hsjq_motion
# to: 'off'
# for:
# hours: 0
# minutes: 15
# seconds: 0
# trigger: state
# conditions:
# - condition: time
# after: 09:00:00
# before: 06:00:00
# weekday:
# - sun
# - mon
# - tue
# - wed
# - thu
# - fri
# - sat
# actions:
# - target:
# entity_id: media_player.hisense_g204x_cd41
# action: media_player.turn_off
# data: {}
# enabled: false
# - action: light.turn_off
# data: {}
# target:
# entity_id:
# - light.living_room_light_2
# mode: single
#
# - id: '1739826648241'
# alias: Turn on Living room lights motion sense
# description: ''
# triggers:
# - trigger: state
# entity_id:
# - binary_sensor.hsjq_motion
# to: 'on'
# conditions:
# - condition: and
# conditions:
# - condition: sun
# after: sunset
# - condition: time
# before: '22:30:00'
# actions:
# - action: light.turn_on
# metadata: {}
# data:
# color_temp_kelvin: 3701
# brightness_pct: 78
# target:
# device_id:
# - d76728b35b6f17a89be03ef00645812e
# entity_id: light.living_room_light_2
# mode: single
- id: '1788748800000'
alias: Living room nightly shutoff
description: Turn off all living-room lights and the fireplace at bedtime.
triggers:
- trigger: time
at: '22:00:00'
id: weeknight
- trigger: time
at: '00:00:00'
id: weekend_night
conditions:
- condition: or
conditions:
- condition: and
conditions:
- condition: trigger
id: weeknight
- condition: time
weekday:
- sun
- mon
- tue
- wed
- thu
- condition: and
conditions:
- condition: trigger
id: weekend_night
- condition: time
weekday:
- sat
- sun
actions:
- action: light.turn_off
target:
entity_id:
- light.living_room_light_2
- light.living_room_lamp_1
- light.small_living_room_lamp
- action: switch.turn_off
target:
entity_id: switch.fireplace
mode: single
- id: '1739835025769'
alias: Turn on bedroom lights with motion
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.bedroom_motion
to: 'on'
conditions:
- condition: time
after: '16:00:00'
before: '20:00:00'
weekday:
- sun
- mon
- tue
- wed
- thu
- fri
- sat
- condition: sun
after: sunset
actions:
- type: turn_on
device_id: a8b108e7836eeb9c66cd0169afc4a486
entity_id: d48390b30da2add2f866a8cec5dc2726
domain: switch
- type: turn_on
device_id: d371342d65df3ddbd28fb841b6ce0a13
entity_id: 7df615008eff2c537a1d6ac4be76def4
domain: switch
- delay: 00:10:00
mode: single
- id: '1739840523909'
alias: Turn off bedside lights in the evening
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.bedroom_occupancy
to: 'off'
for:
hours: 0
minutes: 1
seconds: 0
conditions:
- condition: time
after: '16:00:00'
before: '20:00:00'
weekday:
- sun
- mon
- tue
- wed
- thu
- fri
- sat
actions:
- type: turn_off
device_id: a8b108e7836eeb9c66cd0169afc4a486
entity_id: d48390b30da2add2f866a8cec5dc2726
domain: switch
- type: turn_off
device_id: d371342d65df3ddbd28fb841b6ce0a13
entity_id: 7df615008eff2c537a1d6ac4be76def4
domain: switch
mode: single
- id: '1739841110645'
alias: Turn on family room in the morning
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.meat_heater_motion
to: 'on'
conditions:
- condition: time
after: 05:00:00
before: 07:45:00
weekday:
- sun
- mon
- tue
- wed
- thu
- fri
- sat
actions:
- type: turn_on
device_id: 389d36aa5a0ecfd3eb2e1ec8795464d0
entity_id: 0ae75b98556682467fedf0ff441346ef
domain: light
brightness_pct: 20
enabled: true
- type: turn_on
device_id: 6f341a97517eecd7e4befd4633bb0c3a
entity_id: 4b21efa79430d63e813137d08a8df60e
domain: light
- type: turn_on
device_id: 77092d966014d6eb6222928cce911caf
entity_id: af55f6e7e0ffa642407923fc6e8cdaf3
domain: light
enabled: false
- type: turn_on
device_id: 3096deb74c1dddf893e78fcdfa5b23aa
entity_id: aa15e9f5b3c00639b64501acd18f2081
domain: switch
- type: turn_on
device_id: 58dfbd5f60265682b98691217b9bd038
entity_id: abf15d05be0dadf1bba145f0835d3b3a
domain: switch
- type: turn_on
device_id: 2fde35e8a4dfe850d53d35a650d05b6e
entity_id: c4ec82fc55ef125a4b39f842e06b7261
domain: switch
- type: turn_on
device_id: 4f7810cf96260d17363b63542447ffe7
entity_id: 70347118804cc5d1b5770362ab369251
domain: switch
- type: turn_on
device_id: 84566a79d41ebedd4512d2418195a898
entity_id: fb7b78a1f6d3146585e2f93be33bf4c1
domain: switch
- type: turn_on
device_id: 88c1bfde7eba191a46899d4c59754512
entity_id: 594a0d24aa2fd2dc2156d9918a2204bf
domain: switch
- type: turn_on
device_id: 00ce230936f351558f177d5ed229759b
entity_id: 9ef15fb77ca7e81bcb6c7551329f50aa
domain: switch
mode: single
- id: '1739970708082'
alias: Lights On Before Sunrise
description: ''
triggers:
- trigger: time
at: 05:45:00
conditions: []
actions:
- type: turn_on
device_id: c58fc0103836dffe365aeb33056cee71
entity_id: 37852e5f667fe36826f2ac1ae9f2376a
domain: switch
- type: turn_on
device_id: 389d36aa5a0ecfd3eb2e1ec8795464d0
entity_id: 0ae75b98556682467fedf0ff441346ef
domain: light
- type: turn_on
device_id: 31d67bb445f5c8920a1dc076d0d71e76
entity_id: 6c1fd91e0ff15419f7bdb6b9e233cffc
domain: switch
- type: turn_on
device_id: 77092d966014d6eb6222928cce911caf
entity_id: af55f6e7e0ffa642407923fc6e8cdaf3
domain: light
enabled: false
- type: turn_on
device_id: 00ce230936f351558f177d5ed229759b
entity_id: 9ef15fb77ca7e81bcb6c7551329f50aa
domain: switch
enabled: false
- type: turn_on
device_id: 6f341a97517eecd7e4befd4633bb0c3a
entity_id: 4b21efa79430d63e813137d08a8df60e
domain: light
enabled: false
mode: single
- id: '1739970910638'
alias: 10PM Turn Jordyn's Lamp Off
description: ''
triggers:
- trigger: time
at: '22:00:00'
conditions: []
actions:
- type: turn_off
device_id: ef731a5e6e666bf4fc20cc321f596029
entity_id: 9629035816fddf7de8926e4e4c29aa34
domain: switch
mode: single
- id: '1739971192633'
alias: Turn On Lights in Evening
description: ''
triggers:
- trigger: sun
event: sunset
offset: -00:30:00
conditions: []
actions:
- type: turn_on
device_id: e04306aa947ba9901108c363d8109ca0
entity_id: 12662d2294935b2a21500e3f6d69ba15
domain: switch
enabled: true
- type: turn_on
device_id: e2343a74462cc1434c4acfa2e32d1364
entity_id: ea84e174f0fa3316b9923c4254454a37
domain: switch
enabled: true
- type: turn_on
device_id: c0de273e40c6f4f1cafe4db27e5a6782
entity_id: e6f4af2d9bc05634f994af34bad9a477
domain: switch
enabled: true
- type: turn_on
device_id: 389d36aa5a0ecfd3eb2e1ec8795464d0
entity_id: 0ae75b98556682467fedf0ff441346ef
domain: light
brightness_pct: 50
enabled: true
- type: turn_on
device_id: a1fdc7c838bdded4d8e90bb7941476a3
entity_id: 84d24335edc946153ed4f81ac7906f3c
domain: switch
enabled: true
- type: turn_on
device_id: a8b108e7836eeb9c66cd0169afc4a486
entity_id: d48390b30da2add2f866a8cec5dc2726
domain: switch
- type: turn_on
device_id: d371342d65df3ddbd28fb841b6ce0a13
entity_id: 7df615008eff2c537a1d6ac4be76def4
domain: switch
- type: turn_on
device_id: 872cdc7e609c8df8544349fceae0828e
entity_id: df6f91a7fc4619ff7d664e12518fc9c2
domain: switch
enabled: true
- type: turn_on
device_id: 6f341a97517eecd7e4befd4633bb0c3a
entity_id: 4b21efa79430d63e813137d08a8df60e
domain: light
brightness_pct: 15
- type: turn_on
device_id: 40d38ce4c97de6008962e2b3d84896c9
entity_id: d794e094113a14e6de0db3e5d3f65e88
domain: light
brightness_pct: 50
- type: turn_on
device_id: 84566a79d41ebedd4512d2418195a898
entity_id: fb7b78a1f6d3146585e2f93be33bf4c1
domain: switch
- type: turn_on
device_id: 31622aa8682d29be0d631680776131c3
entity_id: dbc4e84732121a8cc63eb1b6e95fd78b
domain: switch
enabled: false
- type: turn_on
device_id: 3096deb74c1dddf893e78fcdfa5b23aa
entity_id: aa15e9f5b3c00639b64501acd18f2081
domain: switch
enabled: false
- type: turn_on
device_id: eae417dc05ec7c9f1da72f76f32bff18
entity_id: 8ebd9a84e00e9a8a4c05860cb01767ef
domain: switch
enabled: false
- type: turn_on
device_id: 2fde35e8a4dfe850d53d35a650d05b6e
entity_id: c4ec82fc55ef125a4b39f842e06b7261
domain: switch
enabled: false
- type: turn_on
device_id: 58dfbd5f60265682b98691217b9bd038
entity_id: abf15d05be0dadf1bba145f0835d3b3a
domain: switch
enabled: false
- type: turn_on
device_id: 4f7810cf96260d17363b63542447ffe7
entity_id: 70347118804cc5d1b5770362ab369251
domain: switch
enabled: false
- type: turn_on
device_id: 88c1bfde7eba191a46899d4c59754512
entity_id: 594a0d24aa2fd2dc2156d9918a2204bf
domain: switch
enabled: false
- type: turn_on
device_id: 77092d966014d6eb6222928cce911caf
entity_id: af55f6e7e0ffa642407923fc6e8cdaf3
domain: light
enabled: false
- type: turn_on
device_id: e117bada67c4b115d445163c93d2f78c
entity_id: ee44672a0acd4c35658f69b1ec71bdf7
domain: light
enabled: false
mode: single
- id: '1739971331649'
alias: Master Bedroom On At 8PM
description: ''
triggers:
- trigger: time
at: '20:00:00'
conditions: []
actions:
- type: turn_on
device_id: a8b108e7836eeb9c66cd0169afc4a486
entity_id: d48390b30da2add2f866a8cec5dc2726
domain: switch
- type: turn_on
device_id: d371342d65df3ddbd28fb841b6ce0a13
entity_id: 7df615008eff2c537a1d6ac4be76def4
domain: switch
- type: turn_on
device_id: 8b5cc6f6d2e727522ad3069656e2020f
entity_id: ff7e86ebc87458f60b69d1599ff06d13
domain: fan
mode: single
- id: '1739980877023'
alias: Downstairs Lights off at 9AM
description: ''
triggers:
- trigger: time
at: 09:00:00
conditions: []
actions:
- type: turn_off
device_id: 8b5cc6f6d2e727522ad3069656e2020f
entity_id: ff7e86ebc87458f60b69d1599ff06d13
domain: fan
- action: light.turn_off
metadata: {}
data: {}
target:
device_id:
- 389d36aa5a0ecfd3eb2e1ec8795464d0
- type: turn_off
device_id: 6f341a97517eecd7e4befd4633bb0c3a
entity_id: 4b21efa79430d63e813137d08a8df60e
domain: light
- type: turn_off
device_id: 77092d966014d6eb6222928cce911caf
entity_id: af55f6e7e0ffa642407923fc6e8cdaf3
domain: light
- type: turn_off
device_id: 3096deb74c1dddf893e78fcdfa5b23aa
entity_id: aa15e9f5b3c00639b64501acd18f2081
domain: switch
enabled: false
- type: turn_off
device_id: 58dfbd5f60265682b98691217b9bd038
entity_id: abf15d05be0dadf1bba145f0835d3b3a
domain: switch
enabled: false
- type: turn_off
device_id: 2fde35e8a4dfe850d53d35a650d05b6e
entity_id: c4ec82fc55ef125a4b39f842e06b7261
domain: switch
enabled: false
- type: turn_off
device_id: 4f7810cf96260d17363b63542447ffe7
entity_id: 70347118804cc5d1b5770362ab369251
domain: switch
enabled: false
- type: turn_off
device_id: d76728b35b6f17a89be03ef00645812e
entity_id: 7c25efba75f15af0be530ff9c9b9c729
domain: light
enabled: false
- type: turn_off
device_id: 84566a79d41ebedd4512d2418195a898
entity_id: fb7b78a1f6d3146585e2f93be33bf4c1
domain: switch
enabled: false
- type: turn_off
device_id: d76728b35b6f17a89be03ef00645812e
entity_id: 7c25efba75f15af0be530ff9c9b9c729
domain: light
- type: turn_off
device_id: 9d37094a9a9d22b1fb8bf640ca072083
entity_id: f0a27ed262997bfc54caf8a75ac15619
domain: light
- type: turn_off
device_id: 88c1bfde7eba191a46899d4c59754512
entity_id: 594a0d24aa2fd2dc2156d9918a2204bf
domain: switch
enabled: false
- type: turn_off
device_id: e117bada67c4b115d445163c93d2f78c
entity_id: ee44672a0acd4c35658f69b1ec71bdf7
domain: light
enabled: false
mode: single
- id: '1739997032502'
alias: Echo Plug Power Toggle Automation
description: ''
triggers:
- trigger: time
at: '17:00:00'
conditions: []
actions:
- type: turn_off
device_id: 31d67bb445f5c8920a1dc076d0d71e76
entity_id: 6c1fd91e0ff15419f7bdb6b9e233cffc
domain: switch
mode: single
- id: '1740099961116'
alias: Office Lights on with Motion
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.office_motion
to: 'on'
conditions: []
actions:
- action: light.turn_on
metadata: {}
data:
brightness_pct: 100
color_temp_kelvin: 6500
target:
device_id:
- 42b9a4c3e4edfd663f2b8a0a95c3997c
- 889b4b9e38814db0249b94d2dea4ae43
- type: turn_on
device_id: 397d7f06bfbbeaf3014d989a1e32ce07
entity_id: 92505d68c2d00ef59e8ba61c3e3db32b
domain: switch
mode: single
- id: '1740100159283'
alias: Turn off lights when no one is in office
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.office_occupancy
to: 'off'
for:
hours: 1
minutes: 0
seconds: 0
conditions: []
actions:
- action: light.turn_off
metadata: {}
data: {}
target:
device_id:
- 42b9a4c3e4edfd663f2b8a0a95c3997c
- 889b4b9e38814db0249b94d2dea4ae43
- type: turn_off
device_id: 397d7f06bfbbeaf3014d989a1e32ce07
entity_id: 92505d68c2d00ef59e8ba61c3e3db32b
domain: switch
mode: single
- id: '1740929956187'
alias: Turn off Family room when no one is in it.
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.meat_heater_motion
to: 'off'
for:
hours: 0
minutes: 30
seconds: 0
conditions:
- condition: or
conditions:
- condition: time
after: '22:00:00'
before: 02:00:00
weekday:
- sat
- fri
- thu
- wed
- tue
- mon
- sun
- condition: time
after: 09:00:00
before: '16:00:00'
weekday:
- sat
- fri
- thu
- wed
- tue
- mon
- sun
actions:
- type: turn_off
device_id: 389d36aa5a0ecfd3eb2e1ec8795464d0
entity_id: 0ae75b98556682467fedf0ff441346ef
domain: light
- type: turn_off
device_id: 389d36aa5a0ecfd3eb2e1ec8795464d0
entity_id: 0ae75b98556682467fedf0ff441346ef
domain: light
mode: single
- id: '1742691391008'
alias: Turn on office echo
description: ''
triggers:
- trigger: time
at: '18:00:00'
conditions: []
actions:
- type: turn_on
device_id: 31d67bb445f5c8920a1dc076d0d71e76
entity_id: 6c1fd91e0ff15419f7bdb6b9e233cffc
domain: switch
mode: single
- id: '1742691696852'
alias: Family Room Off at Midnight
description: ''
triggers:
- trigger: time
at: 00:00:00
conditions: []
actions:
- type: turn_off
device_id: 389d36aa5a0ecfd3eb2e1ec8795464d0
entity_id: 0ae75b98556682467fedf0ff441346ef
domain: light
- type: turn_off
device_id: 77092d966014d6eb6222928cce911caf
entity_id: af55f6e7e0ffa642407923fc6e8cdaf3
domain: light
- type: turn_off
device_id: 6f341a97517eecd7e4befd4633bb0c3a
entity_id: 4b21efa79430d63e813137d08a8df60e
domain: light
mode: single
- id: '1751505592389'
alias: Turn off bug zapper at 1030PM
description: ''
triggers:
- trigger: time
at: '22:30:00'
conditions: []
actions:
- type: turn_off
device_id: 872cdc7e609c8df8544349fceae0828e
entity_id: df6f91a7fc4619ff7d664e12518fc9c2
domain: switch
mode: single
- id: '1756172550854'
alias: Turn off Fan in AM
description: ''
triggers:
- trigger: time
at: 08:00:00
conditions: []
actions:
- type: turn_off
device_id: 31622aa8682d29be0d631680776131c3
entity_id: dbc4e84732121a8cc63eb1b6e95fd78b
domain: switch
mode: single
- id: '1757038271421'
alias: Turn off ceiling fan when it’s below 55
description: ''
triggers:
- trigger: numeric_state
entity_id:
- sensor.openweathermap_temperature
below: 55
conditions:
- condition: time
after: '23:00:00'
before: 06:00:00
actions:
- type: turn_off
device_id: 31622aa8682d29be0d631680776131c3
entity_id: dbc4e84732121a8cc63eb1b6e95fd78b
domain: switch
mode: single
- id: '1757038394971'
alias: Ceiling fan on above 71
description: ''
triggers:
- trigger: numeric_state
entity_id:
- sensor.bedroom_temperature
above: 70
conditions: []
actions:
- type: turn_on
device_id: 31622aa8682d29be0d631680776131c3
entity_id: dbc4e84732121a8cc63eb1b6e95fd78b
domain: switch
mode: single
- id: '1759631437832'
alias: 'Turn off decorations '
description: ''
triggers:
- trigger: time
at: 00:00:00
conditions: []
actions:
- type: turn_off
device_id: a1fdc7c838bdded4d8e90bb7941476a3
entity_id: 84d24335edc946153ed4f81ac7906f3c
domain: switch
- type: turn_off
device_id: 872cdc7e609c8df8544349fceae0828e
entity_id: df6f91a7fc4619ff7d664e12518fc9c2
domain: switch
- type: turn_off
device_id: eae417dc05ec7c9f1da72f76f32bff18
entity_id: 8ebd9a84e00e9a8a4c05860cb01767ef
domain: switch
enabled: true
- type: turn_off
device_id: 3096deb74c1dddf893e78fcdfa5b23aa
entity_id: aa15e9f5b3c00639b64501acd18f2081
domain: switch
- type: turn_off
device_id: 58dfbd5f60265682b98691217b9bd038
entity_id: abf15d05be0dadf1bba145f0835d3b3a
domain: switch
- type: turn_off
device_id: 4f7810cf96260d17363b63542447ffe7
entity_id: 70347118804cc5d1b5770362ab369251
domain: switch
- type: turn_off
device_id: 2fde35e8a4dfe850d53d35a650d05b6e
entity_id: c4ec82fc55ef125a4b39f842e06b7261
domain: switch
- type: turn_off
device_id: e117bada67c4b115d445163c93d2f78c
entity_id: ee44672a0acd4c35658f69b1ec71bdf7
domain: light
- type: turn_off
device_id: 88c1bfde7eba191a46899d4c59754512
entity_id: 594a0d24aa2fd2dc2156d9918a2204bf
domain: switch
- type: turn_off
device_id: 84566a79d41ebedd4512d2418195a898
entity_id: fb7b78a1f6d3146585e2f93be33bf4c1
domain: switch
- type: turn_off
device_id: c0de273e40c6f4f1cafe4db27e5a6782
entity_id: e6f4af2d9bc05634f994af34bad9a477
domain: switch
mode: single
- id: '1762279134131'
alias: Turn on decorations in the AM
description: ''
triggers:
- trigger: time
at: 06:00:00
conditions: []
actions:
- type: turn_on
device_id: eae417dc05ec7c9f1da72f76f32bff18
entity_id: 8ebd9a84e00e9a8a4c05860cb01767ef
domain: switch
enabled: false
- type: turn_on
device_id: a1fdc7c838bdded4d8e90bb7941476a3
entity_id: 84d24335edc946153ed4f81ac7906f3c
domain: switch
enabled: false
- type: turn_on
device_id: 3096deb74c1dddf893e78fcdfa5b23aa
entity_id: aa15e9f5b3c00639b64501acd18f2081
domain: switch
- type: turn_on
device_id: 58dfbd5f60265682b98691217b9bd038
entity_id: abf15d05be0dadf1bba145f0835d3b3a
domain: switch
- type: turn_on
device_id: 2fde35e8a4dfe850d53d35a650d05b6e
entity_id: c4ec82fc55ef125a4b39f842e06b7261
domain: switch
- type: turn_on
device_id: 4f7810cf96260d17363b63542447ffe7
entity_id: 70347118804cc5d1b5770362ab369251
domain: switch
- type: turn_on
device_id: 84566a79d41ebedd4512d2418195a898
entity_id: fb7b78a1f6d3146585e2f93be33bf4c1
domain: switch
- type: turn_on
device_id: e117bada67c4b115d445163c93d2f78c
entity_id: ee44672a0acd4c35658f69b1ec71bdf7
domain: light
- type: turn_on
device_id: 00ce230936f351558f177d5ed229759b
entity_id: 9ef15fb77ca7e81bcb6c7551329f50aa
domain: switch
- type: turn_on
device_id: 88c1bfde7eba191a46899d4c59754512
entity_id: 594a0d24aa2fd2dc2156d9918a2204bf
domain: switch
mode: single
- id: '1763411863796'
alias: Holiday Christmas Pandora at 5PM
description: Trigger Alexa routine to play Christmas Hits Holiday Playlist on Pandora
triggers:
- at: '17:00:00'
trigger: time
conditions:
- condition: template
value_template: '{{ now().month == 11 or now().month == 12 }}
'
actions:
- target:
entity_id: media_player.family_room_show
data:
media:
media_content_id: Play Christmas Music
media_content_type: routine
metadata: {}
action: media_player.play_media
mode: single
- id: '1765116337200'
alias: Turn off living room tv when empty
description: ''
triggers:
- entity_id:
- binary_sensor.hsjq_motion
to:
- 'off'
for:
hours: 0
minutes: 30
seconds: 0
trigger: state
conditions:
- condition: time
after: '18:00:00'
before: 06:00:00
weekday:
- sun
- mon
- tue
- wed
- thu
- fri
- sat
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.turn_off
data: {}
enabled: true
mode: single
- id: '1769311377293'
alias: Front Porch Off at 10P
description: ''
triggers:
- trigger: time
at: '22:00:00'
conditions: []
actions:
- type: turn_off
device_id: e04306aa947ba9901108c363d8109ca0
entity_id: 12662d2294935b2a21500e3f6d69ba15
domain: switch
- type: turn_off
device_id: c0de273e40c6f4f1cafe4db27e5a6782
entity_id: e6f4af2d9bc05634f994af34bad9a477
domain: switch
- type: turn_off
device_id: e2343a74462cc1434c4acfa2e32d1364
entity_id: ea84e174f0fa3316b9923c4254454a37
domain: switch
- type: turn_off
device_id: 872cdc7e609c8df8544349fceae0828e
entity_id: df6f91a7fc4619ff7d664e12518fc9c2
domain: switch
- type: turn_off
device_id: a1fdc7c838bdded4d8e90bb7941476a3
entity_id: 84d24335edc946153ed4f81ac7906f3c
domain: switch
mode: single
- id: '1770433866161'
alias: Turn off Kitchen Sink at Sunrise
description: ''
triggers:
- trigger: sun
event: sunrise
offset: 01:00:00
conditions: []
actions:
- type: turn_off
device_id: 40d38ce4c97de6008962e2b3d84896c9
entity_id: d794e094113a14e6de0db3e5d3f65e88
domain: light
mode: single
- id: freddy_fazbear_mask_schedule
alias: Freddy Fazbear Mask - Sunrise on, bedtime off
description: Turn the Freddy Fazbear Mask on at sunrise; off at 10:00 PM Sun-Thu and 11:59 PM Fri-Sat.
mode: single
triggers:
- trigger: sun
event: sunrise
id: mask_on
- trigger: time
at: '22:00:00'
id: mask_off_school_night
- trigger: time
at: '23:59:00'
id: mask_off_weekend
conditions: []
actions:
- choose:
- conditions:
- condition: trigger
id: mask_on
sequence:
- action: homeassistant.turn_on
target:
entity_id: switch.freddy_fazbear_mask
- conditions:
- condition: trigger
id: mask_off_school_night
- condition: time
weekday:
- sun
- mon
- tue
- wed
- thu
sequence:
- action: homeassistant.turn_off
target:
entity_id: switch.freddy_fazbear_mask
- conditions:
- condition: trigger
id: mask_off_weekend
- condition: time
weekday:
- fri
- sat
sequence:
- action: homeassistant.turn_off
target:
entity_id: switch.freddy_fazbear_mask
@@ -0,0 +1,87 @@
##############################################
############ OFFICE AUTOMATIONS ##############
##############################################
- id: '1740099961116'
alias: Office Lights turn on with motion
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.office_motion
to: 'on'
conditions: []
actions:
- action: light.turn_on
metadata: {}
data:
brightness_pct: 100
color_temp_kelvin: 6500
target:
device_id:
- 42b9a4c3e4edfd663f2b8a0a95c3997c
- action: switch.turn_on
target:
entity_id: switch.office_wax_warmer_socket_1
- type: turn_on
device_id: 397d7f06bfbbeaf3014d989a1e32ce07
entity_id: 92505d68c2d00ef59e8ba61c3e3db32b
domain: switch
mode: single
- id: '1742691391008'
alias: Turn on office echo
description: ''
triggers:
- trigger: time
at: '18:00:00'
conditions: []
actions:
- type: turn_on
device_id: 31d67bb445f5c8920a1dc076d0d71e76
entity_id: 6c1fd91e0ff15419f7bdb6b9e233cffc
domain: switch
mode: single
- id: '1740100159283'
alias: Turn off lights when no one is in office
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.office_occupancy
to: 'off'
for:
hours: 1
minutes: 0
seconds: 0
conditions: []
actions:
- action: light.turn_off
metadata: {}
data: {}
target:
device_id:
- 42b9a4c3e4edfd663f2b8a0a95c3997c
- action: switch.turn_off
target:
entity_id: switch.office_wax_warmer_socket_1
- type: turn_off
device_id: 397d7f06bfbbeaf3014d989a1e32ce07
entity_id: 92505d68c2d00ef59e8ba61c3e3db32b
domain: switch
mode: single
- id: '1739997032502'
alias: Office echo Plug Power Toggle Automation
description: ''
triggers:
- trigger: time
at: '20:00:00'
conditions: []
actions:
- type: turn_off
device_id: 31d67bb445f5c8920a1dc076d0d71e76
entity_id: 6c1fd91e0ff15419f7bdb6b9e233cffc
domain: switch
mode: single
@@ -0,0 +1,516 @@
###############################################
########### FIRST FLOOR AUTOMATIONS ###########
###############################################
###################
#### ALL AREAS ####
###################
- id: '1739970708082'
alias: Lights On Before Sunrise
description: ''
triggers:
- trigger: time
at: 05:45:00
conditions: []
actions:
- type: turn_on
device_id: c58fc0103836dffe365aeb33056cee71
entity_id: 37852e5f667fe36826f2ac1ae9f2376a
domain: switch
- type: turn_on
device_id: 389d36aa5a0ecfd3eb2e1ec8795464d0
entity_id: 0ae75b98556682467fedf0ff441346ef
domain: light
- type: turn_on
device_id: 31d67bb445f5c8920a1dc076d0d71e76
entity_id: 6c1fd91e0ff15419f7bdb6b9e233cffc
domain: switch
- type: turn_on
device_id: 77092d966014d6eb6222928cce911caf
entity_id: af55f6e7e0ffa642407923fc6e8cdaf3
domain: light
enabled: false
- type: turn_on
device_id: 00ce230936f351558f177d5ed229759b
entity_id: 9ef15fb77ca7e81bcb6c7551329f50aa
domain: switch
enabled: false
- type: turn_on
device_id: 6f341a97517eecd7e4befd4633bb0c3a
entity_id: 4b21efa79430d63e813137d08a8df60e
domain: light
enabled: false
mode: single
- id: '1739980877023'
alias: First floor lights off at 9AM
description: Turn off first floor lights, outdoor porch decoration outlets, and the Outside Lamppost at 9AM.
triggers:
- trigger: time
at: 09:00:00
conditions: []
actions:
- action: switch.turn_off
continue_on_error: true
target:
entity_id:
- switch.washing_machine_socket_1
- switch.porch_fan_socket_1
- switch.big_family_room_lamp
- switch.studio_plug_1_2
- switch.porch_decorations_3
- switch.outside_right_porch_outlet_2
- switch.outside_lamppost
- type: turn_off
device_id: 8b5cc6f6d2e727522ad3069656e2020f
entity_id: ff7e86ebc87458f60b69d1599ff06d13
domain: fan
- action: light.turn_off
metadata: {}
data: {}
target:
device_id:
- 389d36aa5a0ecfd3eb2e1ec8795464d0
- type: turn_off
device_id: 6f341a97517eecd7e4befd4633bb0c3a
entity_id: 4b21efa79430d63e813137d08a8df60e
domain: light
- type: turn_off
device_id: 77092d966014d6eb6222928cce911caf
entity_id: af55f6e7e0ffa642407923fc6e8cdaf3
domain: light
- type: turn_off
device_id: 3096deb74c1dddf893e78fcdfa5b23aa
entity_id: aa15e9f5b3c00639b64501acd18f2081
domain: switch
enabled: false
- type: turn_off
device_id: 58dfbd5f60265682b98691217b9bd038
entity_id: abf15d05be0dadf1bba145f0835d3b3a
domain: switch
enabled: false
- type: turn_off
device_id: 2fde35e8a4dfe850d53d35a650d05b6e
entity_id: c4ec82fc55ef125a4b39f842e06b7261
domain: switch
enabled: false
- type: turn_off
device_id: 4f7810cf96260d17363b63542447ffe7
entity_id: 70347118804cc5d1b5770362ab369251
domain: switch
enabled: false
- type: turn_off
device_id: d76728b35b6f17a89be03ef00645812e
entity_id: 7c25efba75f15af0be530ff9c9b9c729
domain: light
enabled: false
- type: turn_off
device_id: 84566a79d41ebedd4512d2418195a898
entity_id: fb7b78a1f6d3146585e2f93be33bf4c1
domain: switch
enabled: false
- type: turn_off
device_id: d76728b35b6f17a89be03ef00645812e
entity_id: 7c25efba75f15af0be530ff9c9b9c729
domain: light
- type: turn_off
device_id: 9d37094a9a9d22b1fb8bf640ca072083
entity_id: f0a27ed262997bfc54caf8a75ac15619
domain: light
- type: turn_off
device_id: 88c1bfde7eba191a46899d4c59754512
entity_id: 594a0d24aa2fd2dc2156d9918a2204bf
domain: switch
enabled: false
- type: turn_off
device_id: e117bada67c4b115d445163c93d2f78c
entity_id: ee44672a0acd4c35658f69b1ec71bdf7
domain: light
enabled: false
mode: single
- id: '1739971192633'
alias: Turn On Lights in Evening
description: ''
triggers:
- trigger: sun
event: sunset
offset: -00:30:00
conditions: []
actions:
- type: turn_on
device_id: e04306aa947ba9901108c363d8109ca0
entity_id: 12662d2294935b2a21500e3f6d69ba15
domain: switch
enabled: true
- type: turn_on
device_id: e2343a74462cc1434c4acfa2e32d1364
entity_id: ea84e174f0fa3316b9923c4254454a37
domain: switch
enabled: true
- type: turn_on
device_id: c0de273e40c6f4f1cafe4db27e5a6782
entity_id: e6f4af2d9bc05634f994af34bad9a477
domain: switch
enabled: true
- type: turn_on
device_id: 389d36aa5a0ecfd3eb2e1ec8795464d0
entity_id: 0ae75b98556682467fedf0ff441346ef
domain: light
brightness_pct: 50
enabled: true
- type: turn_on
device_id: a1fdc7c838bdded4d8e90bb7941476a3
entity_id: 84d24335edc946153ed4f81ac7906f3c
domain: switch
enabled: true
- type: turn_on
device_id: a8b108e7836eeb9c66cd0169afc4a486
entity_id: d48390b30da2add2f866a8cec5dc2726
domain: switch
- type: turn_on
device_id: d371342d65df3ddbd28fb841b6ce0a13
entity_id: 7df615008eff2c537a1d6ac4be76def4
domain: switch
- type: turn_on
device_id: 6f341a97517eecd7e4befd4633bb0c3a
entity_id: 4b21efa79430d63e813137d08a8df60e
domain: light
brightness_pct: 15
- type: turn_on
device_id: 40d38ce4c97de6008962e2b3d84896c9
entity_id: d794e094113a14e6de0db3e5d3f65e88
domain: light
brightness_pct: 50
- type: turn_on
device_id: 84566a79d41ebedd4512d2418195a898
entity_id: fb7b78a1f6d3146585e2f93be33bf4c1
domain: switch
- type: turn_on
device_id: 31622aa8682d29be0d631680776131c3
entity_id: dbc4e84732121a8cc63eb1b6e95fd78b
domain: switch
enabled: false
- type: turn_on
device_id: 3096deb74c1dddf893e78fcdfa5b23aa
entity_id: aa15e9f5b3c00639b64501acd18f2081
domain: switch
enabled: false
- type: turn_on
device_id: eae417dc05ec7c9f1da72f76f32bff18
entity_id: 8ebd9a84e00e9a8a4c05860cb01767ef
domain: switch
enabled: false
- type: turn_on
device_id: 2fde35e8a4dfe850d53d35a650d05b6e
entity_id: c4ec82fc55ef125a4b39f842e06b7261
domain: switch
enabled: false
- type: turn_on
device_id: 58dfbd5f60265682b98691217b9bd038
entity_id: abf15d05be0dadf1bba145f0835d3b3a
domain: switch
enabled: false
- type: turn_on
device_id: 4f7810cf96260d17363b63542447ffe7
entity_id: 70347118804cc5d1b5770362ab369251
domain: switch
enabled: false
- type: turn_on
device_id: 88c1bfde7eba191a46899d4c59754512
entity_id: 594a0d24aa2fd2dc2156d9918a2204bf
domain: switch
enabled: false
- type: turn_on
device_id: 77092d966014d6eb6222928cce911caf
entity_id: af55f6e7e0ffa642407923fc6e8cdaf3
domain: light
enabled: false
- type: turn_on
device_id: e117bada67c4b115d445163c93d2f78c
entity_id: ee44672a0acd4c35658f69b1ec71bdf7
domain: light
enabled: false
mode: single
###########################
####### FAMILY ROOM #######
###########################
- id: '1739841110645'
alias: Turn on family room in the morning
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.meat_heater_motion
to: 'on'
conditions:
- condition: time
after: 05:00:00
before: 07:45:00
weekday:
- sun
- mon
- tue
- wed
- thu
- fri
- sat
actions:
- type: turn_on
device_id: 389d36aa5a0ecfd3eb2e1ec8795464d0
entity_id: 0ae75b98556682467fedf0ff441346ef
domain: light
brightness_pct: 20
enabled: true
- type: turn_on
device_id: 6f341a97517eecd7e4befd4633bb0c3a
entity_id: 4b21efa79430d63e813137d08a8df60e
domain: light
- type: turn_on
device_id: 77092d966014d6eb6222928cce911caf
entity_id: af55f6e7e0ffa642407923fc6e8cdaf3
domain: light
enabled: false
- type: turn_on
device_id: 3096deb74c1dddf893e78fcdfa5b23aa
entity_id: aa15e9f5b3c00639b64501acd18f2081
domain: switch
- type: turn_on
device_id: 58dfbd5f60265682b98691217b9bd038
entity_id: abf15d05be0dadf1bba145f0835d3b3a
domain: switch
- type: turn_on
device_id: 2fde35e8a4dfe850d53d35a650d05b6e
entity_id: c4ec82fc55ef125a4b39f842e06b7261
domain: switch
- type: turn_on
device_id: 4f7810cf96260d17363b63542447ffe7
entity_id: 70347118804cc5d1b5770362ab369251
domain: switch
- type: turn_on
device_id: 84566a79d41ebedd4512d2418195a898
entity_id: fb7b78a1f6d3146585e2f93be33bf4c1
domain: switch
- type: turn_on
device_id: 88c1bfde7eba191a46899d4c59754512
entity_id: 594a0d24aa2fd2dc2156d9918a2204bf
domain: switch
enabled: false
- type: turn_on
device_id: 00ce230936f351558f177d5ed229759b
entity_id: 9ef15fb77ca7e81bcb6c7551329f50aa
domain: switch
mode: single
- id: '1740929956187'
alias: Turn off family room lights when empty
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.meat_heater_motion
to: 'off'
for:
hours: 0
minutes: 30
seconds: 0
- trigger: time
at: '09:00:00'
- trigger: time
at: '22:00:00'
- trigger: event
event_type: automation_reloaded
conditions:
- condition: state
entity_id: binary_sensor.meat_heater_motion
state: 'off'
for: '00:30:00'
- condition: or
conditions:
- condition: time
after: '22:00:00'
before: 02:00:00
weekday:
- sat
- fri
- thu
- wed
- tue
- mon
- sun
- condition: time
after: 09:00:00
before: '16:00:00'
weekday:
- sat
- fri
- thu
- wed
- tue
- mon
- sun
actions:
- type: turn_off
device_id: 389d36aa5a0ecfd3eb2e1ec8795464d0
entity_id: 0ae75b98556682467fedf0ff441346ef
domain: light
mode: single
- id: '1742691696852'
alias: Family room turn off at Midnight
description: ''
triggers:
- trigger: time
at: 00:00:00
conditions: []
actions:
- type: turn_off
device_id: 389d36aa5a0ecfd3eb2e1ec8795464d0
entity_id: 0ae75b98556682467fedf0ff441346ef
domain: light
- type: turn_off
device_id: 77092d966014d6eb6222928cce911caf
entity_id: af55f6e7e0ffa642407923fc6e8cdaf3
domain: light
- type: turn_off
device_id: 6f341a97517eecd7e4befd4633bb0c3a
entity_id: 4b21efa79430d63e813137d08a8df60e
domain: light
mode: single
- id: '1765116337200'
alias: Turn off family room tv when empty
description: ''
triggers:
- entity_id:
- binary_sensor.hsjq_motion
to:
- 'off'
for:
hours: 0
minutes: 15
seconds: 0
trigger: state
conditions:
- condition: time
after: '18:00:00'
before: 06:00:00
weekday:
- sun
- mon
- tue
- wed
- thu
- fri
- sat
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.turn_off
data: {}
enabled: true
mode: single
- id: '1770433866161'
alias: Turn off kitchen sink light at Sunrise
description: ''
triggers:
- trigger: sun
event: sunrise
offset: 01:00:00
conditions: []
actions:
- type: turn_off
device_id: 40d38ce4c97de6008962e2b3d84896c9
entity_id: d794e094113a14e6de0db3e5d3f65e88
domain: light
mode: single
- id: '1788748800000'
alias: Living room nightly shutoff
description: Turn off all living-room lights and the fireplace at bedtime.
triggers:
- trigger: time
at: '22:30:00'
id: weeknight
- trigger: time
at: '00:00:00'
id: weekend_night
conditions:
- condition: or
conditions:
- condition: and
conditions:
- condition: trigger
id: weeknight
- condition: time
weekday:
- sun
- mon
- tue
- wed
- thu
- condition: and
conditions:
- condition: trigger
id: weekend_night
- condition: time
weekday:
- sat
- sun
actions:
- action: light.turn_off
target:
entity_id:
- light.living_room_light_2
- light.living_room_lamp_1
- light.small_living_room_lamp
- action: switch.turn_off
target:
entity_id: switch.fireplace
mode: single
- id: '1760000000001'
alias: Fireplace mantle wax warmer on with motion
description: Turn on the mantle wax warmer when motion is detected in the living room.
triggers:
- trigger: state
entity_id: binary_sensor.living_room_motion_2
to: 'on'
conditions: []
actions:
- action: switch.turn_on
target:
entity_id: switch.fireplace
mode: single
- id: 'fireplace_mantle_wax_warmer_off_when_empty'
alias: Fireplace mantle wax warmer off when empty
description: Turn off the mantle wax warmer after one hour without living room occupancy.
triggers:
- trigger: state
entity_id: binary_sensor.living_room_occupancy
to: 'off'
for: '01:00:00'
conditions: []
actions:
- action: switch.turn_off
target:
entity_id: switch.fireplace
mode: single
- id: 'fireplace_mantle_outlet_morning_schedule'
alias: Fireplace mantle outlet morning schedule
description: Turn the fireplace mantle outlet on each morning.
triggers:
- trigger: time
at: '05:30:00'
conditions: []
actions:
- action: switch.turn_on
target:
entity_id: switch.fireplace
mode: single
@@ -0,0 +1,163 @@
###################################################
############ SECOND FLOOR AUTOMATIONS #############
###################################################
###########
### MBR ###
###########
- id: '1756172550854'
alias: Parents ceiling fan off at 8AM
description: ''
triggers:
- trigger: time
at: 08:00:00
conditions: []
actions:
- type: turn_off
device_id: 31622aa8682d29be0d631680776131c3
entity_id: dbc4e84732121a8cc63eb1b6e95fd78b
domain: switch
mode: single
# - id: '1757038271421'
# alias: Parents ceiling fan off when it’s below 55
# description: Recheck the cold-weather cutoff when the nighttime window starts and after startup or reload.
# triggers:
# - trigger: numeric_state
# entity_id:
# - sensor.openweathermap_temperature
# below: 55
# - trigger: time
# at: '23:00:00'
# - trigger: homeassistant
# event: start
# - trigger: event
# event_type: automation_reloaded
# conditions:
# - condition: numeric_state
# entity_id: sensor.openweathermap_temperature
# below: 55
# - condition: time
# after: '23:00:00'
# before: 06:00:00
# actions:
# - type: turn_off
# device_id: 31622aa8682d29be0d631680776131c3
# entity_id: dbc4e84732121a8cc63eb1b6e95fd78b
# domain: switch
# mode: single
- id: '1757038394971'
alias: Parents ceiling fan on above 70
description: Turn on above 70 unless the nighttime cold-weather cutoff takes priority.
triggers:
- trigger: numeric_state
entity_id:
- sensor.bedroom_temperature
above: 70
conditions:
- condition: template
value_template: >-
{{ not ((now().hour >= 23 or now().hour < 6)
and is_number(states('sensor.openweathermap_temperature'))
and states('sensor.openweathermap_temperature') | float(55) < 55) }}
actions:
- type: turn_on
device_id: 31622aa8682d29be0d631680776131c3
entity_id: dbc4e84732121a8cc63eb1b6e95fd78b
domain: switch
mode: single
- id: '1739971331649'
alias: Master Bedroom bedside lights, ceiling fan and air purifier on at 8PM
description: Turn on both bedside lights, the ceiling fan outlet and the air purifier at 20:00.
triggers:
- trigger: time
at: '20:00:00'
conditions: []
actions:
- type: turn_on
device_id: a8b108e7836eeb9c66cd0169afc4a486
entity_id: d48390b30da2add2f866a8cec5dc2726
domain: switch
- type: turn_on
device_id: d371342d65df3ddbd28fb841b6ce0a13
entity_id: 7df615008eff2c537a1d6ac4be76def4
domain: switch
- type: turn_on
device_id: 8b5cc6f6d2e727522ad3069656e2020f
entity_id: ff7e86ebc87458f60b69d1599ff06d13
domain: fan
- action: switch.turn_on
target:
entity_id: switch.parents_ceiling_fan
mode: single
- id: '1739835025769'
alias: Turn on master bedroom lights with motion
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.bedroom_motion
to: 'on'
conditions:
- condition: time
after: '16:00:00'
before: '20:00:00'
weekday:
- sun
- mon
- tue
- wed
- thu
- fri
- sat
- condition: sun
after: sunset
actions:
- type: turn_on
device_id: a8b108e7836eeb9c66cd0169afc4a486
entity_id: d48390b30da2add2f866a8cec5dc2726
domain: switch
- type: turn_on
device_id: d371342d65df3ddbd28fb841b6ce0a13
entity_id: 7df615008eff2c537a1d6ac4be76def4
domain: switch
mode: single
- id: '1739840523909'
alias: Turn off master bedroom bedside lights in the evening when no one is in the room
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.bedroom_occupancy
to: 'off'
for:
hours: 0
minutes: 10
seconds: 0
conditions:
- condition: time
after: '16:00:00'
before: '20:00:00'
weekday:
- sun
- mon
- tue
- wed
- thu
- fri
- sat
actions:
- type: turn_off
device_id: a8b108e7836eeb9c66cd0169afc4a486
entity_id: d48390b30da2add2f866a8cec5dc2726
domain: switch
- type: turn_off
device_id: d371342d65df3ddbd28fb841b6ce0a13
entity_id: 7df615008eff2c537a1d6ac4be76def4
domain: switch
mode: single
@@ -0,0 +1,104 @@
################################################
############## OUTSIDE AUTOMATIONS #############
################################################
- id: '1769311377293'
alias: Front Porch outlets turn off at 10P
description: ''
triggers:
- trigger: time
at: '22:00:00'
conditions: []
actions:
- type: turn_off
device_id: e04306aa947ba9901108c363d8109ca0
entity_id: 12662d2294935b2a21500e3f6d69ba15
domain: switch
# Lamppost shutoff moved to Porch Decorations Schedule below.
# - type: turn_off
# device_id: c0de273e40c6f4f1cafe4db27e5a6782
# entity_id: e6f4af2d9bc05634f994af34bad9a477
# domain: switch
- type: turn_off
device_id: e2343a74462cc1434c4acfa2e32d1364
entity_id: ea84e174f0fa3316b9923c4254454a37
domain: switch
mode: single
- id: porch_decorations_schedule
alias: Porch Decorations Schedule
description: >-
Turn on Porch Decorations 1, Porch Decor 1, Porch Decor 2, Porch Decor 3,
Porch Decor 1 Smart Plug, and Right Porch Outlet 2 one hour before sunset.
Turn them off at 22:30 Sunday through Thursday and at 23:45 Friday and
Saturday, along with the Outside Lamppost.
mode: single
triggers:
- trigger: sun
event: sunset
offset: "-01:00:00"
id: decorations_on
- trigger: time
at: "22:30:00"
id: decorations_off_weeknight
- trigger: time
at: "23:45:00"
id: decorations_off_weekend
conditions: []
actions:
- choose:
- conditions:
- condition: trigger
id: decorations_on
sequence:
- action: switch.turn_on
continue_on_error: true
target:
entity_id:
- switch.washing_machine_socket_1
- switch.porch_fan_socket_1
- switch.big_family_room_lamp
- switch.studio_plug_1_2
- switch.porch_decorations_3
- switch.outside_right_porch_outlet_2
- conditions:
- condition: trigger
id: decorations_off_weeknight
- condition: time
weekday:
- sun
- mon
- tue
- wed
- thu
sequence:
- action: switch.turn_off
continue_on_error: true
target:
entity_id:
- switch.washing_machine_socket_1
- switch.porch_fan_socket_1
- switch.big_family_room_lamp
- switch.studio_plug_1_2
- switch.porch_decorations_3
- switch.outside_right_porch_outlet_2
- switch.outside_lamppost
- conditions:
- condition: trigger
id: decorations_off_weekend
- condition: time
weekday:
- fri
- sat
sequence:
- action: switch.turn_off
continue_on_error: true
target:
entity_id:
- switch.washing_machine_socket_1
- switch.porch_fan_socket_1
- switch.big_family_room_lamp
- switch.studio_plug_1_2
- switch.porch_decorations_3
- switch.outside_right_porch_outlet_2
- switch.outside_lamppost
@@ -0,0 +1,195 @@
###################################################################
################## HOLIDAY AUTOMATIONS ############################
###################################################################
#################
#### GENERAL ####
#################
- id: '1759631437832'
alias: 'Turn off decorations '
description: Midnight recovery sweep for decorations, including porch outlets and the Outside Lamppost already shut off by the decorations schedule.
triggers:
- trigger: time
at: 00:00:00
conditions: []
actions:
- type: turn_off
device_id: a1fdc7c838bdded4d8e90bb7941476a3
entity_id: 84d24335edc946153ed4f81ac7906f3c
domain: switch
- type: turn_off
device_id: eae417dc05ec7c9f1da72f76f32bff18
entity_id: 8ebd9a84e00e9a8a4c05860cb01767ef
domain: switch
enabled: true
- type: turn_off
device_id: 89d1cfaee4e3bfdda1bbfe6b5bc7abb9
entity_id: 416b554cc68bf1090bde534f837951dc
domain: switch
- type: turn_off
device_id: 3096deb74c1dddf893e78fcdfa5b23aa
entity_id: aa15e9f5b3c00639b64501acd18f2081
domain: switch
- type: turn_off
device_id: 58dfbd5f60265682b98691217b9bd038
entity_id: abf15d05be0dadf1bba145f0835d3b3a
domain: switch
- type: turn_off
device_id: 4f7810cf96260d17363b63542447ffe7
entity_id: 70347118804cc5d1b5770362ab369251
domain: switch
- type: turn_off
device_id: 2fde35e8a4dfe850d53d35a650d05b6e
entity_id: c4ec82fc55ef125a4b39f842e06b7261
domain: switch
- type: turn_off
device_id: e117bada67c4b115d445163c93d2f78c
entity_id: ee44672a0acd4c35658f69b1ec71bdf7
domain: light
- type: turn_off
device_id: 88c1bfde7eba191a46899d4c59754512
entity_id: 594a0d24aa2fd2dc2156d9918a2204bf
domain: switch
- type: turn_off
device_id: 84566a79d41ebedd4512d2418195a898
entity_id: fb7b78a1f6d3146585e2f93be33bf4c1
domain: switch
- type: turn_off
device_id: c0de273e40c6f4f1cafe4db27e5a6782
entity_id: e6f4af2d9bc05634f994af34bad9a477
domain: switch
mode: single
- id: '1762279134131'
alias: Turn on decorations in the AM
description: ''
triggers:
- trigger: time
at: 06:00:00
conditions: []
actions:
- type: turn_on
device_id: eae417dc05ec7c9f1da72f76f32bff18
entity_id: 8ebd9a84e00e9a8a4c05860cb01767ef
domain: switch
enabled: false
- type: turn_on
device_id: a1fdc7c838bdded4d8e90bb7941476a3
entity_id: 84d24335edc946153ed4f81ac7906f3c
domain: switch
enabled: false
- type: turn_on
device_id: 3096deb74c1dddf893e78fcdfa5b23aa
entity_id: aa15e9f5b3c00639b64501acd18f2081
domain: switch
- type: turn_on
device_id: 58dfbd5f60265682b98691217b9bd038
entity_id: abf15d05be0dadf1bba145f0835d3b3a
domain: switch
- type: turn_on
device_id: 2fde35e8a4dfe850d53d35a650d05b6e
entity_id: c4ec82fc55ef125a4b39f842e06b7261
domain: switch
- type: turn_on
device_id: 4f7810cf96260d17363b63542447ffe7
entity_id: 70347118804cc5d1b5770362ab369251
domain: switch
- type: turn_on
device_id: 84566a79d41ebedd4512d2418195a898
entity_id: fb7b78a1f6d3146585e2f93be33bf4c1
domain: switch
- type: turn_on
device_id: e117bada67c4b115d445163c93d2f78c
entity_id: ee44672a0acd4c35658f69b1ec71bdf7
domain: light
- type: turn_on
device_id: 00ce230936f351558f177d5ed229759b
entity_id: 9ef15fb77ca7e81bcb6c7551329f50aa
domain: switch
- type: turn_on
device_id: 88c1bfde7eba191a46899d4c59754512
entity_id: 594a0d24aa2fd2dc2156d9918a2204bf
domain: switch
enabled: false
mode: single
###################
#### HALLOWEEN ####
###################
- id: freddy_fazbear_mask_schedule
alias: Freddy Fazbear Mask - Sunrise on, bedtime off
description: Turn the Freddy Fazbear Mask on at sunrise; off at 10:00 PM Sun-Thu and 11:59 PM Fri-Sat.
mode: single
triggers:
- trigger: sun
event: sunrise
id: mask_on
- trigger: time
at: '22:00:00'
id: mask_off_school_night
- trigger: time
at: '23:59:00'
id: mask_off_weekend
conditions: []
actions:
- choose:
- conditions:
- condition: trigger
id: mask_on
sequence:
- action: homeassistant.turn_on
target:
entity_id: switch.freddy_fazbear_mask
- conditions:
- condition: trigger
id: mask_off_school_night
- condition: time
weekday:
- sun
- mon
- tue
- wed
- thu
sequence:
- action: homeassistant.turn_off
target:
entity_id: switch.freddy_fazbear_mask
- conditions:
- condition: trigger
id: mask_off_weekend
- condition: time
weekday:
- fri
- sat
sequence:
- action: homeassistant.turn_off
target:
entity_id: switch.freddy_fazbear_mask
###################
#### CHRISTMAS ####
###################
- id: '1763411863796'
alias: Holiday Christmas Pandora at 5PM
description: Trigger Alexa routine to play Christmas Hits Holiday Playlist on Pandora
triggers:
- at: '17:00:00'
trigger: time
conditions:
- condition: template
value_template: '{{ now().month == 11 or now().month == 12 }}
'
actions:
- target:
entity_id: media_player.family_room_show
data:
media:
media_content_id: Play Christmas Music
media_content_type: routine
metadata: {}
action: media_player.play_media
mode: single
@@ -1,213 +0,0 @@
# Safety, device, and household alert automations.
- id: '1742155540986'
alias: 'Garage Floodlight Low Battery Notification '
description: ''
triggers:
- trigger: state
entity_id: binary_sensor.garage_floodlight_battery
to: 'on'
conditions: []
actions:
- action: notify.notify_events_alerts
metadata: {}
data:
message: Garage floodlight batteries low
title: Garage floodlight batteries low
mode: single
- id: '1742841280696'
alias: Triple Threat Anti-Stink Juice Empty
description: ''
triggers:
- trigger: state
entity_id:
- sensor.triple_threat_spray_deodorant_liquid
enabled: true
to: '0'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Triple Threat's Anti-Stinky Juice is Empty
title: Triple Threat's Anti-Stinky Juice is Empty
mode: single
- id: '1742841650033'
alias: The Deuce Litter Level
description: ''
triggers:
- trigger: state
entity_id:
- sensor.the_deuce_litter_level
enabled: true
to: '65'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: The Deuce's Litter is Low
title: The Deuce's Litter is Low
mode: single
- id: '1742841709247'
alias: Triple Threat Litter Level Notification
description: ''
triggers:
- trigger: state
entity_id:
- sensor.triple_threat_litter_level
enabled: true
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Triple Threat's Litter is Low
title: Triple Threat's Litter is Low
mode: single
- id: '1742841764585'
alias: Cat Crapper 3000 Low Litter Level Notification
description: ''
triggers:
- trigger: state
entity_id:
- sensor.cat_crapper_3000_litter_level
enabled: true
to: '65'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Cat Crapper 3000's Litter is Low
title: Cat Crapper 3000's Litter is Low
mode: single
- id: '1742841838536'
alias: Poop Box Low Litter Level Notification
description: ''
triggers:
- trigger: state
entity_id:
- sensor.poop_box_litter_level
enabled: true
to: '65'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Poop Box's Litter is Low
title: Poop Box's Litter is Low
mode: single
- id: '1742842192548'
alias: Cat Crapper 3000 is full of crap notification
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.cat_crapper_3000_wastebin_filled
enabled: true
to: 'on'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Cat Crapper 3000 is full of crap
title: CLEAN Cat Crapper 3000 - it's full of crap
mode: single
- id: '1742842261527'
alias: The Deuce is full of deuces notification
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.the_deuce_wastebin_filled
enabled: true
to: 'on'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: The Deuce is full of deuces (laundry room)
title: Too many dueces have been dropped!
mode: single
- id: '1742842372471'
alias: The Poop Box has runneth over Notification
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.poop_box_wastebin_filled
enabled: true
to: 'on'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: The Poop Box has runneth over (basement, left side)
title: The poop box is bursting!!
mode: single
- id: '1742842458817'
alias: Triple Threat Full Notification
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.triple_threat_wastebin_filled
enabled: true
to: 'on'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Triple Threat can't take no more! (Basement, right side)
title: Triple Threat can't take no more!
mode: single
- id: '1782932601001'
alias: Luma Smart Litter Box Waste Bin Full Notification
description: Notify when the Petlibro Luma Smart Litter Box waste bin is full.
triggers:
- trigger: state
entity_id:
- binary_sensor.luma_smart_litter_box_waste_bin_full
enabled: true
to: 'on'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Luma Smart Litter Box is full of crap
title: CLEAN Luma Smart Litter Box - it's full of crap
mode: single
- id: '1782932601004'
alias: Luma Smart Litter Box Door Error Notification
description: Notify when the Petlibro Luma Smart Litter Box reports a door error.
triggers:
- trigger: state
entity_id:
- binary_sensor.luma_smart_litter_box_door_error
enabled: true
to: 'on'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Luma Smart Litter Box has a door error
title: Luma Smart Litter Box door error
mode: single
@@ -1,213 +0,0 @@
# Safety, device, and household alert automations.
- id: '1742155540986'
alias: 'Garage Floodlight Low Battery Notification '
description: ''
triggers:
- trigger: state
entity_id: binary_sensor.garage_floodlight_battery
to: 'on'
conditions: []
actions:
- action: script.notify_events_alerts
metadata: {}
data:
message: Garage floodlight batteries low
title: Garage floodlight batteries low
mode: single
- id: '1742841280696'
alias: Triple Threat Anti-Stink Juice Empty
description: ''
triggers:
- trigger: state
entity_id:
- sensor.triple_threat_spray_deodorant_liquid
enabled: true
to: '0'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Triple Threat's Anti-Stinky Juice is Empty
title: Triple Threat's Anti-Stinky Juice is Empty
mode: single
- id: '1742841650033'
alias: The Deuce Litter Level
description: ''
triggers:
- trigger: state
entity_id:
- sensor.the_deuce_litter_level
enabled: true
to: '65'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: The Deuce's Litter is Low
title: The Deuce's Litter is Low
mode: single
- id: '1742841709247'
alias: Triple Threat Litter Level Notification
description: ''
triggers:
- trigger: state
entity_id:
- sensor.triple_threat_litter_level
enabled: true
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Triple Threat's Litter is Low
title: Triple Threat's Litter is Low
mode: single
- id: '1742841764585'
alias: Cat Crapper 3000 Low Litter Level Notification
description: ''
triggers:
- trigger: state
entity_id:
- sensor.cat_crapper_3000_litter_level
enabled: true
to: '65'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Cat Crapper 3000's Litter is Low
title: Cat Crapper 3000's Litter is Low
mode: single
- id: '1742841838536'
alias: Poop Box Low Litter Level Notification
description: ''
triggers:
- trigger: state
entity_id:
- sensor.poop_box_litter_level
enabled: true
to: '65'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Poop Box's Litter is Low
title: Poop Box's Litter is Low
mode: single
- id: '1742842192548'
alias: Cat Crapper 3000 is full of crap notification
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.cat_crapper_3000_wastebin_filled
enabled: true
to: 'on'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Cat Crapper 3000 is full of crap
title: CLEAN Cat Crapper 3000 - it's full of crap
mode: single
- id: '1742842261527'
alias: The Deuce is full of deuces notification
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.the_deuce_wastebin_filled
enabled: true
to: 'on'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: The Deuce is full of deuces (laundry room)
title: Too many dueces have been dropped!
mode: single
- id: '1742842372471'
alias: The Poop Box has runneth over Notification
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.poop_box_wastebin_filled
enabled: true
to: 'on'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: The Poop Box has runneth over (basement, left side)
title: The poop box is bursting!!
mode: single
- id: '1742842458817'
alias: Triple Threat Full Notification
description: ''
triggers:
- trigger: state
entity_id:
- binary_sensor.triple_threat_wastebin_filled
enabled: true
to: 'on'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Triple Threat can't take no more! (Basement, right side)
title: Triple Threat can't take no more!
mode: single
- id: '1782932601001'
alias: Luma Smart Litter Box Waste Bin Full Notification
description: Notify when the Petlibro Luma Smart Litter Box waste bin is full.
triggers:
- trigger: state
entity_id:
- binary_sensor.luma_smart_litter_box_waste_bin_full
enabled: true
to: 'on'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Luma Smart Litter Box is full of crap
title: CLEAN Luma Smart Litter Box - it's full of crap
mode: single
- id: '1782932601004'
alias: Luma Smart Litter Box Door Error Notification
description: Notify when the Petlibro Luma Smart Litter Box reports a door error.
triggers:
- trigger: state
entity_id:
- binary_sensor.luma_smart_litter_box_door_error
enabled: true
to: 'on'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Luma Smart Litter Box has a door error
title: Luma Smart Litter Box door error
mode: single
@@ -0,0 +1,122 @@
- id: '1742842372471'
alias: The Poop Box has runneth over Notification
description: Notify when the litter-box alert flag turns on.
use_blueprint:
path: local/litter_binary_notification.yaml
input:
alert_sensor: binary_sensor.poop_box_wastebin_filled
title: The poop box is bursting!!
message: The Poop Box has runneth over (basement, left side)
- id: '1742842458817'
alias: Triple Threat Full Notification
description: Notify when the litter-box alert flag turns on.
use_blueprint:
path: local/litter_binary_notification.yaml
input:
alert_sensor: binary_sensor.triple_threat_wastebin_filled
title: Triple Threat can't take no more!
message: Triple Threat can't take no more! (Basement, right side)
- id: '1782932601001'
alias: Luma Smart Litter Box Waste Bin Full Notification
description: Notify when the Petlibro Luma Smart Litter Box waste bin is full.
use_blueprint:
path: local/litter_binary_notification.yaml
input:
alert_sensor: binary_sensor.luma_smart_litter_box_waste_bin_full
title: CLEAN Luma Smart Litter Box - it's full of crap
message: Luma Smart Litter Box is full of crap
- id: '1782932601004'
alias: Luma Smart Litter Box Door Error Notification
description: Notify when the Petlibro Luma Smart Litter Box reports a door error.
use_blueprint:
path: local/litter_binary_notification.yaml
input:
alert_sensor: binary_sensor.luma_smart_litter_box_door_error
title: Luma Smart Litter Box door error
message: Luma Smart Litter Box has a door error
- id: '1742841764585'
alias: Cat Crapper 3000 Low Litter Level Notification
description: Alert at or below 65 after two minutes; refill above 65 rearms the alert.
use_blueprint:
path: local/litter_low_notification.yaml
input:
litter_sensor: sensor.cat_crapper_3000_litter_level
threshold: 65
title: Cat Crapper 3000's Litter is Low
message: Cat Crapper 3000's Litter is Low
- id: '1742841838536'
alias: Poop Box Low Litter Level Notification
description: Alert at or below 65 after two minutes; refill above 65 rearms the alert.
use_blueprint:
path: local/litter_low_notification.yaml
input:
litter_sensor: sensor.poop_box_litter_level
threshold: 65
title: Poop Box's Litter is Low
message: Poop Box's Litter is Low
- id: '1742842192548'
alias: Cat Crapper 3000 is full of crap notification
description: Notify when the litter-box alert flag turns on.
use_blueprint:
path: local/litter_binary_notification.yaml
input:
alert_sensor: binary_sensor.cat_crapper_3000_wastebin_filled
title: CLEAN Cat Crapper 3000 - it's full of crap
message: Cat Crapper 3000 is full of crap
- id: '1742842261527'
alias: The Deuce is full of deuces notification
description: Notify when the litter-box alert flag turns on.
use_blueprint:
path: local/litter_binary_notification.yaml
input:
alert_sensor: binary_sensor.the_deuce_wastebin_filled
title: Too many dueces have been dropped!
message: The Deuce is full of deuces (laundry room)
- id: '1742841280696'
alias: Triple Threat Anti-Stink Juice Empty
description: ''
triggers:
- trigger: state
entity_id:
- sensor.triple_threat_spray_deodorant_liquid
enabled: true
to: '0'
conditions: []
actions:
- action: notify.notify_events_cats
metadata: {}
data:
message: Triple Threat's Anti-Stinky Juice is Empty
title: Triple Threat's Anti-Stinky Juice is Empty
mode: single
- id: '1742841650033'
alias: The Deuce Litter Level
description: Alert at or below 65 after two minutes; refill above 65 rearms the alert.
use_blueprint:
path: local/litter_low_notification.yaml
input:
litter_sensor: sensor.the_deuce_litter_level
threshold: 65
title: The Deuce's Litter is Low
message: The Deuce's Litter is Low
- id: '1742841709247'
alias: Triple Threat Litter Level Notification
description: Alert at or below 65 after two minutes; refill above 65 rearms the alert.
use_blueprint:
path: local/litter_low_notification.yaml
input:
litter_sensor: sensor.triple_threat_litter_level
threshold: 65
title: Triple Threat's Litter is Low
message: Triple Threat's Litter is Low
@@ -0,0 +1,15 @@
- id: '1742155540986'
alias: 'Garage Floodlight Low Battery Notification '
description: ''
triggers:
- trigger: state
entity_id: binary_sensor.garage_floodlight_battery
to: 'on'
conditions: []
actions:
- action: script.notify_events_alerts
metadata: {}
data:
message: Garage floodlight batteries low
title: Garage floodlight batteries low
mode: single
-107
View File
@@ -1,107 +0,0 @@
# Presence and camera/security automations.
- id: '1742155298923'
alias: Disarm cameras when Joshua comes home
description: ''
triggers:
- trigger: zone
entity_id: device_tracker.joshuas_iphone_of_pain
zone: zone.home
event: enter
conditions: []
actions:
- action: camera.disable_motion_detection
metadata: {}
data: {}
target:
device_id:
- 3df00a0796efe006e4ea349a9752faba
- 0c13483d761efe3b85e534fadd323fe3
- 5aa158596e48441ea606dd881a34402d
- d045a0a68c3c4d52a7beb2a28fdf6993
- 044c43ea05ceff58b1209f6441c24183
mode: single
- id: '1742155338014'
alias: Disable Cameras when Polly comes home
description: ''
triggers:
- trigger: zone
entity_id: device_tracker.pollys_iphone
zone: zone.home
event: enter
conditions: []
actions:
- action: camera.disable_motion_detection
metadata: {}
data: {}
target:
device_id:
- 3df00a0796efe006e4ea349a9752faba
- 0c13483d761efe3b85e534fadd323fe3
- 5aa158596e48441ea606dd881a34402d
- d045a0a68c3c4d52a7beb2a28fdf6993
- 044c43ea05ceff58b1209f6441c24183
mode: single
- id: '1754414591718'
alias: Arm Inside Blink Cameras When Everyone Leaves
description: Arm Blink 'Inside' camera group when both Joshua and Polly are not
home
triggers:
- trigger: zone
entity_id: device_tracker.joshuas_iphone_of_pain
zone: zone.home
event: leave
- trigger: zone
entity_id: device_tracker.pollys_iphone
zone: zone.home
event: leave
conditions:
- condition: device
device_id: eabafa43fa1148e4facc8e5a4151958d
domain: device_tracker
entity_id: 74ded93c7bee3648dccc7f024095b7c6
type: is_not_home
- condition: device
device_id: 638383852928fbd17397baa190c2d853
domain: device_tracker
entity_id: 0f494c69f886f56391b07f0517b0c0f3
type: is_not_home
actions:
- target:
entity_id: alarm_control_panel.blink_inside
action: alarm_control_panel.alarm_arm_away
mode: single
- id: '1758196295864'
alias: Disarm Blink when family is home
description: Arm Blink 'Inside' camera group when both Joshua and Polly are not
home
triggers:
- trigger: zone
entity_id: device_tracker.joshuas_iphone_of_pain
zone: zone.home
event: enter
- trigger: zone
entity_id: device_tracker.pollys_iphone
zone: zone.home
event: enter
conditions:
- condition: device
device_id: eabafa43fa1148e4facc8e5a4151958d
domain: device_tracker
entity_id: 74ded93c7bee3648dccc7f024095b7c6
type: is_home
- condition: device
device_id: 638383852928fbd17397baa190c2d853
domain: device_tracker
entity_id: 0f494c69f886f56391b07f0517b0c0f3
type: is_home
actions:
- action: alarm_control_panel.alarm_disarm
target:
entity_id:
- alarm_control_panel.blink_inside
data: {}
mode: single
@@ -0,0 +1,88 @@
# # One controller owns the Blink group and its cameras' motion detection.
# - id: '1758196295864'
# alias: Inside Blink Cameras - Reconcile Family Presence
# description: >-
# Disarm when either parent is home. Enable camera motion detection and arm
# only when both trackers have valid away states. Reconcile on startup,
# automation reload and Blink recovery; unknown location never means away.
# triggers:
# - trigger: state
# entity_id:
# - device_tracker.joshuas_iphone_of_pain
# - device_tracker.pollys_iphone
# to: null
# - trigger: homeassistant
# event: start
# - trigger: event
# event_type: automation_reloaded
# - trigger: state
# entity_id: alarm_control_panel.blink_inside
# to:
# - armed_away
# - disarmed
# conditions: []
# actions:
# - choose:
# - conditions:
# - condition: template
# value_template: >-
# {{ is_state('device_tracker.joshuas_iphone_of_pain', 'home')
# or is_state('device_tracker.pollys_iphone', 'home') }}
# sequence:
# - if:
# - condition: template
# value_template: >-
# {{ has_value('alarm_control_panel.blink_inside')
# and not is_state('alarm_control_panel.blink_inside', 'disarmed') }}
# then:
# - action: alarm_control_panel.alarm_disarm
# continue_on_error: true
# target:
# entity_id: alarm_control_panel.blink_inside
# # Recheck after the network action, before changing camera settings.
# - condition: template
# value_template: >-
# {{ is_state('device_tracker.joshuas_iphone_of_pain', 'home')
# or is_state('device_tracker.pollys_iphone', 'home') }}
# - action: camera.disable_motion_detection
# continue_on_error: true
# target:
# device_id:
# - 3df00a0796efe006e4ea349a9752faba
# - 0c13483d761efe3b85e534fadd323fe3
# - 5aa158596e48441ea606dd881a34402d
# - d045a0a68c3c4d52a7beb2a28fdf6993
# - 044c43ea05ceff58b1209f6441c24183
# - conditions:
# - condition: template
# value_template: >-
# {{ states('device_tracker.joshuas_iphone_of_pain')
# not in ['home', 'unknown', 'unavailable', '']
# and states('device_tracker.pollys_iphone')
# not in ['home', 'unknown', 'unavailable', '']
# and has_value('alarm_control_panel.blink_inside') }}
# sequence:
# - action: camera.enable_motion_detection
# # One unavailable camera must not prevent arming the rest of the group.
# continue_on_error: true
# target:
# device_id:
# - 3df00a0796efe006e4ea349a9752faba
# - 0c13483d761efe3b85e534fadd323fe3
# - 5aa158596e48441ea606dd881a34402d
# - d045a0a68c3c4d52a7beb2a28fdf6993
# - 044c43ea05ceff58b1209f6441c24183
# - condition: template
# value_template: >-
# {{ states('device_tracker.joshuas_iphone_of_pain')
# not in ['home', 'unknown', 'unavailable', '']
# and states('device_tracker.pollys_iphone')
# not in ['home', 'unknown', 'unavailable', ''] }}
# - if:
# - condition: template
# value_template: "{{ not is_state('alarm_control_panel.blink_inside', 'armed_away') }}"
# then:
# - action: alarm_control_panel.alarm_arm_away
# target:
# entity_id: alarm_control_panel.blink_inside
# mode: restart
-739
View File
@@ -1,739 +0,0 @@
# Kids routines, school prep, and announcement automations.
- id: '1788133654306'
alias: Declan - Non-school day afternoon medication reminder
description: Remind Declan to take his medicine at 2 PM on non-school days.
triggers:
- trigger: time
at: '14:00:00'
conditions:
- condition: or
conditions:
- condition: time
weekday:
- sat
- sun
- condition: state
entity_id: calendar.holidays_in_united_states
state: 'on'
- condition: template
value_template: >
{{ states('calendar.family_events') == 'on'
and 'no school' in (state_attr('calendar.family_events', 'message')
| default('') | lower) }}
actions:
- action: notify.alexa_media
data:
target:
- media_player.living_room
- media_player.declan_s_room
message: Declan, this is your reminder to take your afternoon medicine.
- action: notify.notify_events_alerts
data:
title: Declan medication reminder
message: Declan's afternoon medicine reminder.
mode: single
- id: '1788133654303'
alias: Set weekday announcement speakers to volume 5
description: Set the school-announcement speakers to volume level 5 each weekday.
triggers:
- trigger: time
at: '07:10:00'
conditions:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
actions:
- action: media_player.volume_set
target:
entity_id:
- media_player.family_room
- media_player.living_room
- media_player.kitchen
- media_player.dinning_room
- media_player.declan_s_room
data:
volume_level: 0.5
mode: single
- id: '1788133651043'
alias: Set Jordyns announcement speaker to volume 6
description: Set the school-announcement speakers to volume level 6 each weekday.
triggers:
- trigger: time
at: '07:35:00'
conditions:
- condition: time
weekday:
- tue
actions:
- action: media_player.volume_set
target:
entity_id:
- media_player.jordyn_s_room
data:
volume_level: 0.6
mode: single
- id: '1743380000904'
alias: Jordyn OT Wake Up Call 1
description: ''
triggers:
- at: '07:40:00'
trigger: time
enabled: true
conditions:
- condition: time
weekday:
- tue
actions:
- data:
message: Jordyn, time to wake up and get out of bed for OT! Get dressed, put on deodorant, brush your teeth, and brush your hair. You got this!
action: notify.alexa_media_jordyn_s_room
mode: single
- id: '1743380036904'
alias: Jordyn OT Wake Up Call 2
description: ''
triggers:
- at: '07:43:00'
trigger: time
enabled: true
conditions:
- condition: time
weekday:
- tue
actions:
- data:
message: Jordyn, you need to wake up and get out of bed for OT! Get dressed, put on deodorant, brush your teeth, and brush your hair. You got this!
action: notify.alexa_media_jordyn_s_room
mode: single
- id: '1743382700904'
alias: Jordyn OT Wake Up Call 3
description: ''
triggers:
- at: '07:46:00'
trigger: time
enabled: true
conditions:
- condition: time
weekday:
- tue
actions:
- data:
message: Jordyn, get out of bed! Move! Move! Move! Move! Get dressed, put on deodorant, brush your teeth, and brush your hair. You got this!
action: notify.alexa_media_jordyn_s_room
mode: single
- id: '1788133654304'
alias: Jordyn-Bedtime-Snack-Notification-Weekend
description: Triggers an Alexa notification at 8:00 PM if the Google Calendar event
is 'Girls at Home'
triggers:
- at: '20:30:00'
trigger: time
enabled: true
conditions:
- condition: time
weekday:
- fri
- sat
- condition: state
entity_id: calendar.family_events
state: 'on'
- condition: template
value_template: '{{ state_attr(''calendar.family_events'', ''message'') == ''Girls
at Home'' }}'
actions:
- data:
message: Jordyn, time to put away screens and get a bedtime snack.
action: notify.alexa_media_living_room
mode: single
- id: '1743381226135'
alias: Jordyn-Bedtime-Snack-Notification
description: Triggers an Alexa notification at 8 PM if the Google Family Calendar
event is 'Girls at Home'
triggers:
- trigger: time
at: '20:00:00'
enabled: true
conditions:
- condition: time
weekday:
- sun
- mon
- tue
- wed
- condition: state
entity_id: calendar.family_events
state: 'on'
- condition: template
value_template: '{{ state_attr(''calendar.family_events'', ''message'') == ''Girls
at Home'' }}'
actions:
- target:
entity_id:
- media_player.living_room
- media_player.jordyn_s_room
- media_player.dining_room
data:
media:
media_content_id: Jordyn Weekday Bedtime Routine
media_content_type: routine
metadata: {}
action: media_player.play_media
mode: single
- id: '1743311126135'
alias: Jordyn-Bedtime-10-minute-warning-Notification
description: Triggers an Alexa notification at 7:50 PM if the Google Family Calendar
event is 'Girls at Home'
triggers:
- trigger: time
at: '19:50:00'
enabled: true
conditions:
- condition: time
weekday:
- sun
- mon
- tue
- wed
- condition: state
entity_id: calendar.family_events
state: 'on'
- condition: template
value_template: '{{ state_attr(''calendar.family_events'', ''message'') == ''Girls
at Home'' }}'
actions:
- action: notify.alexa_media
data:
target:
- media_player.living_room
- media_player.dinning_room
message: Jordyn - ten minutes left until it's time to put away screens and get a bedtime snack.
mode: single
- id: '1743311106135'
alias: Jordyn-Bedtime-10-minute-warning-weekend-Notification
description: Triggers an Alexa notification at 8:20 PM if the Google Family Calendar
event is 'Girls at Home'
triggers:
- trigger: time
at: '20:20:00'
enabled: true
conditions:
- condition: time
weekday:
- fri
- sat
- condition: state
entity_id: calendar.family_events
state: 'on'
- condition: template
value_template: '{{ state_attr(''calendar.family_events'', ''message'') == ''Girls
at Home'' }}'
actions:
- action: notify.alexa_media
data:
target:
- media_player.living_room
- media_player.dinning_room
message: Jordyn - ten minutes left until it's time to put away screens and get a bedtime snack.
mode: single
- id: '1743385356527'
alias: Jordyn’s Light on at 7PM
description: ''
triggers:
- trigger: time
at: '19:00:00'
conditions:
- condition: time
weekday:
- mon
- tue
actions:
- type: turn_on
device_id: ef731a5e6e666bf4fc20cc321f596029
entity_id: 9629035816fddf7de8926e4e4c29aa34
domain: switch
mode: single
- id: '1743385432089'
alias: Jordyn’s light on weekend
description: ''
triggers:
- trigger: time
at: '19:00:00'
conditions:
- condition: and
conditions:
- condition: time
weekday:
- fri
- sat
- sun
- condition: template
value_template: '{{ now().strftime(''%U'') | int % 2 == 1 }}'
actions:
- type: turn_on
device_id: ef731a5e6e666bf4fc20cc321f596029
entity_id: 9629035816fddf7de8926e4e4c29aa34
domain: switch
mode: single
- id: '1744560941050'
alias: Jordyn’s Lamp on at 730
description: ''
triggers:
- trigger: time
at: '19:30:00'
conditions: []
actions:
- type: turn_on
device_id: ef731a5e6e666bf4fc20cc321f596029
entity_id: 9629035816fddf7de8926e4e4c29aa34
domain: switch
mode: single
- id: '1744503044129'
alias: Set Alexa Announcement Volume
description: Set Alexa announcement volume for structured daily routine
triggers:
- at: 08:15:00
trigger: time
conditions:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
actions:
- data:
media:
media_content_id: Set kids speakers to 6
media_content_type: routine
metadata: {}
action: media_player.play_media
target:
device_id: 7c55da01c1d0e4ee7900852bb7621b1e
mode: single
- id: '1744503044809'
alias: Daily Alexa Announcements for Kids
description: Schedule of Alexa announcements for structured daily routine
trigger:
- platform: time
at: 09:00:00
- platform: time
at: '10:00:00'
- platform: time
at: '10:15:00'
- platform: time
at: '11:00:00'
- platform: time
at: '12:00:00'
- platform: time
at: '12:30:00'
- platform: time
at: '13:00:00'
- platform: time
at: '13:30:00'
- platform: time
at: '14:00:00'
- platform: time
at: '14:30:00'
- platform: time
at: '19:29:00'
condition:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
action:
- service: calendar.get_events
target:
entity_id: calendar.family_events
data:
start_date_time: '{{ today_at(''00:00'') }}'
end_date_time: '{{ today_at(''00:00'') + timedelta(days=1) }}'
response_variable: agenda
- condition: template
value_template: "{{ (agenda['calendar.family_events']['events'] | default([]))\n
\ | selectattr('summary', 'defined')\n | map(attribute='summary')\n | map('lower')\n
\ | select('search', 'camp')\n | list | count == 0 }}\n"
- variables:
messages:
08:00:00: Time for breakfast and a little TV!
09:00:00: Outside play time! If it’s bad weather, do your learning activities
inside.
'10:00:00': Snack time!
'10:15:00': Time for quiet play or outside fun.
'11:00:00': Creative time! Draw, color, or write something cool!
'12:00:00': Lunch time! Wash up and head to the table.
'12:30:00': Happy Helper Time! Do your chores like a champ!
'13:00:00': TV break — relax and enjoy.
'13:30:00': Time to read a book or listen to a story.
'14:00:00': Let’s move! Try a Minecraft brain break!
'14:30:00': Time to relax with mindfulness or yoga.
- service: notify.alexa_media
data:
target:
- media_player.living_room
- media_player.jordyn_s_room
- media_player.chloe_s_room
- media_player.declan_s_room
- media_player.kitchen
message: '{{ messages[now().strftime(''%H:%M:%S'')] }}'
data:
type: announce
method: all
mode: single
- id: '1744503044813'
alias: Announce day camp preparation and pause TV
triggers:
- at: 08:25:00
trigger: time
conditions:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
actions:
- service: calendar.get_events
target:
entity_id: calendar.family_events
data:
start_date_time: '{{ today_at(''00:00'') }}'
end_date_time: '{{ today_at(''00:00'') + timedelta(days=1) }}'
response_variable: agenda
- condition: template
value_template: "{{ (agenda['calendar.family_events']['events'] | default([]))\n
\ | selectattr('summary', 'defined')\n | map(attribute='summary')\n | map('lower')\n
\ | select('search', 'camp')\n | list | count > 0 }}\n"
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.media_pause
data: {}
- data:
target:
- media_player.living_room
- media_player.declan_s_room
- media_player.jordyn_s_room
- media_player.kitchen
message: 'Time to prepare for day camp. Please do the following: Fill water
bottles, put on deodorant, brush teeth, and Jordyn, brush your hair.
'
action: notify.alexa_media
mode: single
- id: '1744503044814'
alias: Announce time to leave for day camp and turn off TV
triggers:
- at: 08:40:00
trigger: time
conditions:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
actions:
- service: calendar.get_events
target:
entity_id: calendar.family_events
data:
start_date_time: '{{ today_at(''00:00'') }}'
end_date_time: '{{ today_at(''00:00'') + timedelta(days=1) }}'
response_variable: agenda
- condition: template
value_template: "{{ (agenda['calendar.family_events']['events'] | default([]))\n
\ | selectattr('summary', 'defined')\n | map(attribute='summary')\n | map('lower')\n
\ | select('search', 'camp')\n | list | count > 0 }}\n"
- target:
entity_id: media_player.basement_show
data:
media_content_id: Turn Kids TV Off
media_content_type: routine
action: media_player.play_media
- data:
target:
- media_player.living_room
message: 'It is time to leave for day camp. Please do the following: Put on
shoes, get lunch, get water bottle, and grab your bag if needed.
'
action: notify.alexa_media
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.turn_off
data: {}
mode: single
- id: '1744503044145'
alias: Reset Alexa Announcement Volume - Camp Days
description: Set Alexa announcement volume for structured daily routine
trigger:
- platform: time
at: 09:15:00
condition:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
action:
- service: calendar.get_events
target:
entity_id: calendar.family_events
data:
start_date_time: '{{ today_at(''00:00'') }}'
end_date_time: '{{ today_at(''00:00'') + timedelta(days=1) }}'
response_variable: agenda
- condition: template
value_template: "{{ (agenda['calendar.family_events']['events'] | default([]))\n
\ | selectattr('summary', 'defined')\n | map(attribute='summary')\n | map('lower')\n
\ | select('search', 'camp')\n | list | count > 0 }}\n"
- service: media_player.play_media
data:
media_content_id: Reset Alexa Volume
media_content_type: routine
target:
entity_id: media_player.basement_show
mode: single
- id: '1744503044146'
alias: Reset Alexa Announcement Volume - No Camp Days
description: Set Alexa announcement volume for structured daily routine
trigger:
- platform: time
at: '19:17:00'
condition:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
action:
- service: calendar.get_events
target:
entity_id: calendar.family_events
data:
start_date_time: '{{ today_at(''00:00'') }}'
end_date_time: '{{ today_at(''00:00'') + timedelta(days=1) }}'
response_variable: agenda
- condition: template
value_template: "{{ (agenda['calendar.family_events']['events'] | default([]))\n
\ | selectattr('summary', 'defined')\n | map(attribute='summary')\n | map('lower')\n
\ | select('search', 'camp')\n | list | count == 0 }}\n"
- service: media_player.play_media
data:
media_content_id: Reset Alexa Volume
media_content_type: routine
target:
entity_id: media_player.basement_show
mode: single
- id: '1756211568290'
alias: Jordyn - School Prep Announcement
description: ''
triggers:
- at: 08:25:00
trigger: time
conditions:
- condition: or
conditions:
- condition: time
weekday:
- wed
- condition: and
conditions:
- condition: time
weekday:
- mon
- condition: template
value_template: '{{ now().strftime(''%U'') | int % 2 == 1 }}'
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.media_pause
data: {}
- data:
target:
- media_player.living_room
- media_player.jordyn_s_room
- media_player.kitchen
- media_player.dinning_room
message: Jordyn this is your reminder to take your medicine if you haven't, pack your snack and fill your water bottle. Please make sure your
hair is brushed nicely.
action: notify.alexa_media
mode: single
- id: '1788131534302'
alias: Jordyn - Time for School Announcement
description: ''
triggers:
- at: 08:38:00
trigger: time
conditions:
- condition: or
conditions:
- condition: time
weekday:
- wed
- condition: and
conditions:
- condition: time
weekday:
- mon
- condition: template
value_template: '{{ now().strftime(''%U'') | int % 2 == 1 }}'
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.turn_off
data: {}
- data:
target:
- media_player.living_room
- media_player.jordyn_s_room
- media_player.kitchen
- media_player.dinning_room
- media_player.family_room
message: Time for school! Last call to get your water bottle, snack, and put on your shoes. Good luck, have fun, don't die!!
action: notify.alexa_media
mode: single
- id: '1788133654305'
alias: Jordyn - School and OT Prep Announcement
description: ''
triggers:
- at: 07:50:00
trigger: time
conditions:
- condition: time
weekday:
- tue
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.media_pause
data: {}
- data:
target:
- media_player.living_room
- media_player.jordyn_s_room
- media_player.kitchen
- media_player.dinning_room
message: Jordyn, you have OT today. This is your reminder to take your medicine if you haven't, pack your snack and fill your water bottle. Please make sure your
hair is brushed nicely.
action: notify.alexa_media
mode: single
- id: '1756211851043'
alias: Jordyn - Time for OT & School Announcement
description: ''
triggers:
- at: 08:13:00
trigger: time
conditions:
- condition: time
weekday:
- tue
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.turn_off
data: {}
- data:
target:
- media_player.living_room
- media_player.jordyn_s_room
- media_player.kitchen
- media_player.dinning_room
message: Time to head out for OT and school! Last call to quickly get your water bottle, snack, and put on your shoes. Good luck, have fun, don't die!!
action: notify.alexa_media
mode: single
- id: '1788133654301'
alias: Declan - School Prep Announcement
description: ''
triggers:
- at: 07:20:00
trigger: time
conditions:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.media_pause
data: {}
- data:
target:
- media_player.living_room
- media_player.declan_s_room
- media_player.kitchen
- media_player.dinning_room
message: Declan this is your reminder to pack your snack and fill your water bottle.
action: notify.alexa_media
mode: single
- id: '1788133654302'
alias: Declan - Time for School Announcement
description: ''
triggers:
- at: 07:38:00
trigger: time
conditions:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.turn_off
data: {}
- data:
target:
- media_player.living_room
- media_player.declan_s_room
- media_player.kitchen
- media_player.dinning_room
message: Time for school! Last call to get your water bottle, snack, and put on your shoes. Good luck, have fun, don't die!!
action: notify.alexa_media
mode: single
@@ -0,0 +1,73 @@
####################################################################
############ DAY CAMP ANNOUNCEMENTS AND AUTOMATIONS ################
####################################################################
- id: '1744503044813'
alias: Announce day camp preparation and pause TV
triggers:
- at: 08:25:00
trigger: time
conditions:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
actions:
- condition: state
entity_id: binary_sensor.day_camp_today
state: 'on'
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.media_pause
data: {}
continue_on_error: true
- data:
message: 'Time to prepare for day camp. Please do the following: Fill water bottles, put on deodorant, brush teeth, and Jordyn, brush your hair.'
targets:
- media_player.living_room
- media_player.declan_s_room
- media_player.jordyn_s_room
- media_player.kitchen
action: script.family_announcement
mode: single
- id: '1744503044814'
alias: Announce time to leave for day camp and turn off TV
triggers:
- at: 08:40:00
trigger: time
conditions:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
actions:
- condition: state
entity_id: binary_sensor.day_camp_today
state: 'on'
- target:
entity_id: media_player.basement_show
data:
media_content_id: Turn Kids TV Off
media_content_type: routine
action: media_player.play_media
- data:
message: 'It is time to leave for day camp. Please do the following: Put on shoes, get lunch, get water bottle, and grab your bag if needed.'
targets:
- media_player.living_room
action: script.family_announcement
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.turn_off
data: {}
continue_on_error: true
mode: single
@@ -0,0 +1,101 @@
###################################################################
################ DECLAN'S ALARMS AND ANNOUNCEMENTS ################
###################################################################
- id: '1788133654306'
alias: Declan - Non-school day afternoon medication reminder
description: Remind Declan to take his medicine at 2 PM on non-school days.
triggers:
- trigger: time
at: '14:00:00'
conditions:
- condition: state
entity_id: binary_sensor.blackhawk_school_day
state: 'off'
actions:
- action: script.family_announcement
continue_on_error: true
data:
message: Declan, this is your reminder to take your afternoon medicine.
targets:
- media_player.living_room
- media_player.declan_s_room
- action: script.notify_events_alerts
data:
title: Declan medication reminder
message: Declan's afternoon medicine reminder.
mode: single
- id: '1788133654301'
alias: Declan - School Prep Announcement
description: ''
triggers:
- at: 07:20:00
trigger: time
conditions:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
- condition: state
entity_id: binary_sensor.blackhawk_school_day
state: 'on'
- condition: state
entity_id: binary_sensor.day_camp_today
state: 'off'
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.media_pause
data: {}
continue_on_error: true
- data:
message: Declan this is your reminder to pack your snack and fill your water bottle.
targets:
- media_player.living_room
- media_player.declan_s_room
- media_player.kitchen
action: script.family_announcement
mode: single
- id: '1788133654302'
alias: Declan - Time for School Announcement
description: ''
triggers:
- at: 07:38:00
trigger: time
conditions:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
- condition: state
entity_id: binary_sensor.blackhawk_school_day
state: 'on'
- condition: state
entity_id: binary_sensor.day_camp_today
state: 'off'
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.turn_off
data: {}
continue_on_error: true
- data:
message: Time for school! Last call to get your water bottle, snack, and put on your shoes. Good luck, have fun, don't die!!
targets:
- media_player.living_room
- media_player.declan_s_room
- media_player.kitchen
action: script.family_announcement
mode: single
@@ -0,0 +1,367 @@
#################################################################
############## JORDYN'S ALARMS AND ANNOUNCEMENTS ################
#################################################################
- id: '1788133651043'
alias: Set Jordyns announcement speaker to volume 6
description: Set Jordyn’s speaker to volume 6 before Tuesday OT reminders.
triggers:
- trigger: time
at: '07:35:00'
conditions:
- condition: time
weekday:
- tue
actions:
- action: media_player.volume_set
target:
entity_id:
- media_player.jordyn_s_room
data:
volume_level: 0.6
mode: single
###################################################
############### JORDYN'S OT ALARMS ################
###################################################
- id: '1743380000904'
alias: Jordyn - OT Wake Up Calls
description: Tuesday wake-up calls at 07:40, 07:43 and 07:46, each with its own message.
triggers:
- trigger: time
at: 07:40:00
id: first
- trigger: time
at: 07:43:00
id: second
- trigger: time
at: 07:46:00
id: third
conditions:
- condition: time
weekday:
- tue
actions:
- variables:
messages:
first: Jordyn, time to wake up and get out of bed for OT! Get dressed, put on deodorant, brush your teeth, and brush your hair. You got this!
second: Jordyn, you need to wake up and get out of bed for OT! Get dressed, put on deodorant, brush your teeth, and brush your hair. You got this!
third: Jordyn, get out of bed! Move! Move! Move! Move! Get dressed, put on deodorant, brush your teeth, and brush your hair. You got this!
- condition: template
value_template: '{{ trigger is defined and trigger.id in messages }}'
- action: script.family_announcement
data:
targets:
- media_player.jordyn_s_room
message: '{{ messages[trigger.id] }}'
mode: queued
- id: '1788133654305'
alias: Jordyn - School and OT Prep Announcement
description: ''
triggers:
- at: 07:50:00
trigger: time
conditions:
- condition: time
weekday:
- tue
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.media_pause
data: {}
continue_on_error: true
- data:
message: Jordyn, you have OT today. This is your reminder to take your medicine if you haven't, pack your snack and fill your water bottle. Please make sure your hair is brushed nicely.
targets:
- media_player.living_room
- media_player.jordyn_s_room
- media_player.kitchen
- media_player.family_room_show
action: script.family_announcement
mode: single
- id: '1756211851043'
alias: Jordyn - Time for OT & School Announcement
description: ''
triggers:
- at: 08:13:00
trigger: time
conditions:
- condition: time
weekday:
- tue
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.turn_off
data: {}
continue_on_error: true
- data:
message: Time to head out for OT{{ ' and school' if is_state('binary_sensor.blackhawk_school_day', 'on') else '' }}! Last call to quickly get your water bottle, snack, and put on your shoes. Good luck, have fun, don't die!!
targets:
- media_player.living_room
- media_player.jordyn_s_room
- media_player.kitchen
action: script.family_announcement
mode: single
#################################################
############### JORDYN'S SCHOOL ALARMS ##########
#################################################
- id: '1756211568290'
alias: Jordyn - School Prep Announcement
description: ''
triggers:
- at: 08:25:00
trigger: time
conditions:
- condition: or
conditions:
- condition: time
weekday:
- wed
- condition: and
conditions:
- condition: time
weekday:
- mon
- condition: template
value_template: '{{ is_state(''binary_sensor.girls_at_home'', ''on'') }}'
- condition: state
entity_id: binary_sensor.blackhawk_school_day
state: 'on'
- condition: state
entity_id: binary_sensor.day_camp_today
state: 'off'
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.media_pause
data: {}
continue_on_error: true
- data:
message: Jordyn this is your reminder to take your medicine if you haven't, pack your snack and fill your water bottle. Please make sure your hair is brushed nicely.
targets:
- media_player.living_room
- media_player.jordyn_s_room
- media_player.kitchen
action: script.family_announcement
mode: single
- id: '1788131534302'
alias: Jordyn - Time for School Announcement
description: ''
triggers:
- at: 08:41:00
trigger: time
conditions:
- condition: or
conditions:
- condition: time
weekday:
- wed
- condition: and
conditions:
- condition: time
weekday:
- mon
- condition: template
value_template: '{{ is_state(''binary_sensor.girls_at_home'', ''on'') }}'
- condition: state
entity_id: binary_sensor.blackhawk_school_day
state: 'on'
- condition: state
entity_id: binary_sensor.day_camp_today
state: 'off'
actions:
- target:
entity_id: media_player.hisense_g204x_cd41
action: media_player.turn_off
data: {}
continue_on_error: true
- data:
message: Time for school! Last call to get your water bottle, snack, and put on your shoes. Good luck, have fun, don't die!!
targets:
- media_player.living_room
- media_player.jordyn_s_room
- media_player.kitchen
- media_player.family_room_show
action: script.family_announcement
mode: single
##################################################
############### JORDYN'S BEDTIME ALARMS ##########
##################################################
- id: '1788133654304'
alias: Jordyn-Bedtime-Snack-Notification-Weekend
description: Triggers an Alexa notification at 8:30 PM if the Google Calendar event
is 'Girls at Home'
triggers:
- at: '20:30:00'
trigger: time
enabled: true
conditions:
- condition: time
weekday:
- fri
- sat
- condition: state
entity_id: binary_sensor.girls_at_home
state: 'on'
actions:
- data:
message: Jordyn, time to put away screens and get a bedtime snack.
action: notify.alexa_media_living_room
mode: single
- id: '1743381226135'
alias: Jordyn-Bedtime-Snack-Notification
description: Triggers an Alexa notification at 8 PM if the Google Family Calendar
event is 'Girls at Home'
triggers:
- trigger: time
at: '20:00:00'
enabled: true
conditions:
- condition: time
weekday:
- sun
- mon
- tue
- wed
- condition: state
entity_id: binary_sensor.girls_at_home
state: 'on'
actions:
- target:
entity_id:
- media_player.living_room
- media_player.jordyn_s_room
data:
media:
media_content_id: Jordyn Weekday Bedtime Routine
media_content_type: routine
metadata: {}
action: media_player.play_media
mode: single
- id: '1743311126135'
alias: Jordyn - Bedtime Ten Minute Warning
description: Warn at 19:50 on Sun-Wed or 20:20 on Fri-Sat while Girls at Home is active.
triggers:
- trigger: time
at: '19:50:00'
id: weekday
- trigger: time
at: '20:20:00'
id: weekend
conditions:
- condition: state
entity_id: binary_sensor.girls_at_home
state: 'on'
- condition: or
conditions:
- condition: and
conditions:
- condition: trigger
id: weekday
- condition: time
weekday:
- sun
- mon
- tue
- wed
- condition: and
conditions:
- condition: trigger
id: weekend
- condition: time
weekday:
- fri
- sat
actions:
- action: script.family_announcement
data:
message: Jordyn - ten minutes left until it's time to put away screens and get a bedtime snack.
targets:
- media_player.living_room
mode: single
- id: '1743385356527'
alias: Jordyn’s Light on at 7:30PM
description: ''
triggers:
- trigger: time
at: '19:30:00'
conditions:
- condition: time
weekday:
- mon
- tue
actions:
- type: turn_on
device_id: ef731a5e6e666bf4fc20cc321f596029
entity_id: 9629035816fddf7de8926e4e4c29aa34
domain: switch
mode: single
- id: '1743385432089'
alias: Jordyn’s light on weekend
description: ''
triggers:
- trigger: time
at: '19:30:00'
conditions:
- condition: and
conditions:
- condition: time
weekday:
- fri
- sat
- sun
- condition: state
entity_id: binary_sensor.girls_at_home
state: 'on'
actions:
- type: turn_on
device_id: ef731a5e6e666bf4fc20cc321f596029
entity_id: 9629035816fddf7de8926e4e4c29aa34
domain: switch
mode: single
- id: '1739970910638'
alias: 10PM Turn Jordyn's Lamp Off
description: ''
triggers:
- trigger: time
at: '22:00:00'
conditions: []
actions:
- type: turn_off
device_id: ef731a5e6e666bf4fc20cc321f596029
entity_id: 9629035816fddf7de8926e4e4c29aa34
domain: switch
mode: single
# - id: '1744560941050'
# alias: Jordyn’s Lamp on at 730
# description: ''
# triggers:
# - trigger: time
# at: '19:30:00'
# conditions: []
# actions:
# - type: turn_on
# device_id: ef731a5e6e666bf4fc20cc321f596029
# entity_id: 9629035816fddf7de8926e4e4c29aa34
# domain: switch
# mode: single
@@ -0,0 +1,75 @@
- id: '1744503044809'
alias: Daily Alexa Announcements for Kids
description: Weekday summer routine on days with no camp; messages use stable trigger IDs.
triggers:
- trigger: time
at: 08:00:00
id: breakfast
- trigger: time
at: 09:00:00
id: morning_play
- trigger: time
at: '10:00:00'
id: snack
- trigger: time
at: '10:15:00'
id: quiet_play
- trigger: time
at: '11:00:00'
id: creative
- trigger: time
at: '12:00:00'
id: lunch
- trigger: time
at: '12:30:00'
id: chores
- trigger: time
at: '13:00:00'
id: tv_break
- trigger: time
at: '13:30:00'
id: reading
- trigger: time
at: '14:00:00'
id: movement
- trigger: time
at: '14:30:00'
id: relax
conditions:
- condition: state
entity_id: binary_sensor.kids_summer_schedule_day
state: 'on'
- condition: state
entity_id: binary_sensor.day_camp_today
state: 'off'
actions:
- variables:
messages:
breakfast: Time for breakfast and a little TV!
morning_play: Outside play time! If it’s bad weather, do your learning activities inside.
snack: Snack time!
quiet_play: Time for quiet play or outside fun.
creative: Creative time! Draw, color, or write something cool!
lunch: Lunch time! Wash up and head to the table.
chores: Happy Helper Time! Do your chores like a champ!
tv_break: TV break — relax and enjoy.
reading: Time to read a book or listen to a story.
movement: Let’s move! Try a Minecraft brain break!
relax: Time to relax with mindfulness or yoga.
- condition: template
value_template: '{{ trigger is defined and trigger.id in messages }}'
- action: script.family_announcement
data:
message: '{{ messages[trigger.id] }}'
targets:
- media_player.living_room
- media_player.jordyn_s_room
- media_player.chloe_s_room
- media_player.declan_s_room
- media_player.kitchen
delivery:
type: announce
method: all
mode: queued
@@ -0,0 +1,320 @@
- id: '1743176311797'
alias: Bambu Lab - Spaghetti Detection
description: ''
use_blueprint:
path: nberktumer/spaghetti_detection.yaml
input:
home_assistant_host: http://192.168.1.86:8123
obico_host: http://192.168.1.85:3333
obico_auth_token: 5b3dd0ff-7e41-47f4-9027-9e609813ae66
printer_current_stage_sensor: sensor.p1s_01p09c470102673_current_stage
printer_print_status_sensor: sensor.p1s_01p09c470102673_print_status
printer_camera: camera.p1s_01p09c470102673_camera
printer_pause_button: button.p1s_01p09c470102673_pause_printing
printer_resume_button: button.p1s_01p09c470102673_resume_printing
printer_stop_button: button.p1s_01p09c470102673_stop_printing
printer_chamber_light: light.p1s_01p09c470102673_chamber_light
detection_frequency: /30
notification_settings: standard
notification_service: notify.mobile_app_joshuas_iphone_of_pain
### Status Notifications ###
- id: '1767118574570'
alias: Centauri Carbon Status Updates
description: Centauri Carbon progress notifications; intentionally disabled at startup.
initial_state: false
use_blueprint:
path: danielcherubini/elegoo_printer_progress.yaml
input:
error_status_reason_entity: sensor.centauri_carbon_error_status_reason
notify_device: eabafa43fa1148e4facc8e5a4151958d
current_status_entity: sensor.centauri_carbon_current_status
print_status_entity: sensor.centauri_carbon_print_status
percent_complete_entity: sensor.centauri_carbon_percent_complete
### Printer Light Automations ###
- id: '1770938475431'
alias: Jordyn-A1-lights-on-during-print
description: ''
triggers:
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
domain: bambu_lab
type: event_print_started
trigger: device
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
domain: bambu_lab
type: event_print_error_cleared
trigger: device
conditions: []
actions:
- action: light.turn_on
metadata: {}
target:
entity_id: light.jordyns_layer_slayer_chamber_light
data: {}
- action: switch.turn_on
target:
entity_id: switch.jordyn_s_a1_camera_jordyn_a1_camera_flash
mode: single
- id: '1770938581699'
alias: D’s turd flinger Light on during print
description: ''
triggers:
- device_id: 299743e9ce66334a0f3bac10eb24cf31
domain: bambu_lab
type: event_print_started
trigger: device
- device_id: 299743e9ce66334a0f3bac10eb24cf31
domain: bambu_lab
type: event_print_error_cleared
trigger: device
conditions: []
actions:
- action: light.turn_on
metadata: {}
target:
entity_id:
- light.a1_03919d540806387_chamber_light
data:
color_temp_kelvin: 6500
brightness_pct: 100
- action: switch.turn_on
target:
entity_id: switch.declan_s_a1_camera_declan_a1_camera_flash
mode: single
- id: '1770938855253'
alias: Jordyn-a1-light-off-print-send
description: ''
triggers:
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
domain: bambu_lab
type: event_print_finished
trigger: device
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
domain: bambu_lab
type: event_print_canceled
trigger: device
conditions: []
actions:
- action: light.turn_off
metadata: {}
target:
entity_id:
- light.jordyns_layer_slayer_chamber_light
- light.jordyn_s_led_controller_jordyn_s_led_controller
data:
transition: 60
- action: switch.turn_off
target:
entity_id: switch.jordyn_s_a1_camera_jordyn_a1_camera_flash
mode: single
- id: '1770943298493'
alias: D’s turd flinger Light Off after print
description: ''
triggers:
- device_id: 299743e9ce66334a0f3bac10eb24cf31
domain: bambu_lab
type: event_print_finished
trigger: device
- device_id: 299743e9ce66334a0f3bac10eb24cf31
domain: bambu_lab
type: event_print_canceled
trigger: device
conditions: []
actions:
- action: light.turn_off
metadata: {}
target:
entity_id:
- light.a1_03919d540806387_chamber_light
data: {}
- action: switch.turn_off
target:
entity_id: switch.declan_s_a1_camera_declan_a1_camera_flash
mode: single
- id: '1770944104889'
alias: Prints-Charming-Lights-On-Print-Started
description: 'Repaired by agent: replaced deleted Bambu device trigger with P1S
print_status entity trigger.'
triggers:
- trigger: state
entity_id: sensor.p1s_01p09c470102673_print_status
to:
- printing
- running
conditions: []
actions:
- action: light.turn_on
metadata: {}
target:
entity_id: light.p1s_01p09c470102673_chamber_light
data: {}
mode: single
- id: '1770944453024'
alias: Prints-Charming-Light-Off-Print-Completes
description: 'Repaired by agent: replaced deleted Bambu device trigger with P1S
print_status entity trigger.'
triggers:
- trigger: state
entity_id: sensor.p1s_01p09c470102673_print_status
to:
- finish
- complete
- completed
- idle
- failed
- cancelled
conditions:
- condition: template
value_template: '{{ trigger.from_state is not none and trigger.from_state.state
in [''printing'',''running'',''paused'',''pause'',''prepare'',''preparing''] }}'
actions:
- action: light.turn_off
metadata: {}
data: {}
target:
entity_id: light.p1s_01p09c470102673_chamber_light
mode: single
- id: '1770944259063'
alias: Constipation-Orion-Light-On-When-Print-Starts
description: ''
triggers:
- device_id: 615a0b19447425670a6b61ec2343c791
domain: bambu_lab
type: event_print_started
trigger: device
conditions: []
actions:
- action: light.turn_on
metadata: {}
target:
entity_id: light.constipation_orion_chamber_light
data: {}
mode: single
- id: '1770944373316'
alias: Constipation-Orion-Light-Off-Print-Finishes
description: Turn the chamber light off after a completed or cancelled print.
triggers:
- device_id: 615a0b19447425670a6b61ec2343c791
domain: bambu_lab
type: event_print_finished
trigger: device
- device_id: 615a0b19447425670a6b61ec2343c791
domain: bambu_lab
type: event_print_canceled
trigger: device
conditions: []
actions:
- action: light.turn_off
metadata: {}
target:
entity_id: light.constipation_orion_chamber_light
data: {}
mode: single
- id: '1771272500745'
alias: polly-pocket-a1-mini-lights-on-during-print
description: ''
triggers:
- device_id: 1cf63ab34db5ac50291c1477cddf2b9a
domain: bambu_lab
type: event_print_started
trigger: device
conditions: []
actions:
- action: light.turn_on
metadata: {}
target:
entity_id:
- light.a1mini_0309ca580403653_chamber_light
data:
color_temp_kelvin: 6500
brightness_pct: 100
- action: switch.turn_on
target:
entity_id: switch.polly_a1_mini_camera_polly_a1_mini_cam_flash
mode: single
- id: '1771272773906'
alias: polly-pocket-a1-mini-lights-off-during-print
description: ''
triggers:
- device_id: 1cf63ab34db5ac50291c1477cddf2b9a
domain: bambu_lab
type: event_print_finished
trigger: device
- device_id: 1cf63ab34db5ac50291c1477cddf2b9a
domain: bambu_lab
type: event_print_canceled
trigger: device
conditions: []
actions:
- action: light.turn_off
metadata: {}
target:
entity_id:
- light.a1mini_0309ca580403653_chamber_light
data: {}
- action: switch.turn_off
target:
entity_id: switch.polly_a1_mini_camera_polly_a1_mini_cam_flash
mode: single
### Printer Exhaust Fan Control ###
- id: fdm_printer_exhaust_fans
alias: FDM Printer Exhaust Fans During Prints
description: >-
Turn on the FDM Printer Exhaust Fans outlet when Constipation Orion,
Prints Charming, or Printney Spears (Centauri Carbon) is printing. Leave it
on until all three have been finished for 30 minutes. A pause keeps the
fans on. An unknown printer status does not start the shutoff timer.
mode: restart
triggers:
- trigger: state
entity_id:
- sensor.constipation_orion_print_status
- sensor.p1s_01p09c470102673_print_status
- sensor.centauri_carbon_print_status
- trigger: homeassistant
event: start
- trigger: event
event_type: automation_reloaded
conditions: []
actions:
- variables:
active_states:
- printing
- running
- pause
- paused
- pausing
- prepare
- preparing
printers:
- sensor.constipation_orion_print_status
- sensor.p1s_01p09c470102673_print_status
- sensor.centauri_carbon_print_status
- if:
- condition: template
value_template: >-
{{ expand(printers) | selectattr('state', 'in', active_states) | list | count > 0 }}
then:
- action: switch.turn_on
target:
entity_id: switch.studio_plug_2
else:
- condition: template
value_template: "{{ trigger.platform == 'state' }}"
- condition: template
value_template: >-
{{ expand(printers) | selectattr('state', 'in', ['unknown', 'unavailable']) | list | count == 0 }}
- delay:
minutes: 30
- condition: template
value_template: >-
{{ expand(printers) | selectattr('state', 'in', active_states) | list | count == 0
and expand(printers) | selectattr('state', 'in', ['unknown', 'unavailable']) | list | count == 0 }}
- action: switch.turn_off
target:
entity_id: switch.studio_plug_2
@@ -17,77 +17,24 @@
detection_frequency: /30
notification_settings: standard
notification_service: notify.mobile_app_joshuas_iphone_of_pain
### Status Notifications ###
- id: '1767118574570'
alias: Centauri Carbon Status Updates
description: ''
description: Centauri Carbon progress notifications; intentionally disabled at startup.
initial_state: false
use_blueprint:
path: danielcherubini/elegoo_printer_progress.yaml
input:
error_status_reason_entity: sensor.saturn_iv_error_status_reason
error_status_reason_entity: sensor.centauri_carbon_error_status_reason
notify_device: eabafa43fa1148e4facc8e5a4151958d
current_status_entity: sensor.saturn_iv_current_status
print_status_entity: sensor.saturn_iv_print_status
percent_complete_entity: sensor.saturn_iv_percent_complete
- id: '1767118623336'
alias: Saturn Status Notifications
description: ''
use_blueprint:
path: danielcherubini/elegoo_printer_progress.yaml
input:
percent_complete_entity: sensor.saturn_iv_percent_complete
print_status_entity: sensor.saturn_iv_print_status
current_status_entity: sensor.saturn_iv_current_status
error_status_reason_entity: sensor.saturn_iv_error_status_reason
notify_device: eabafa43fa1148e4facc8e5a4151958d
enable_status_notifications: false
- id: '1767120051057'
alias: Turn off Heater and Exhaust Fan after Resin Print
description: ''
triggers:
- trigger: state
entity_id:
- sensor.saturn_iv_print_status
from: null
to:
- complete
conditions: []
actions:
- delay:
hours: 0
minutes: 5
seconds: 0
milliseconds: 0
- type: turn_off
device_id: fc9f0c9a71eaa4bd6aa14417425f61cb
entity_id: a91e4bedcc7c5d57563d88bd58d67c9c
domain: switch
- delay:
hours: 0
minutes: 5
seconds: 0
milliseconds: 0
- type: turn_off
device_id: 1b1350195291ecd20cf7c69604c09699
entity_id: 74c861175e7d5036c2253583f59d1100
domain: switch
mode: single
- id: '1767816737180'
alias: Saturn 4 Complete Notification
description: ''
triggers:
- trigger: state
entity_id:
- sensor.saturn_iv_print_status
to:
- complete
conditions: []
actions:
- action: notify.mobile_app_joshuas_iphone_of_pain
metadata: {}
data:
message: Saturn 4 print complete.
mode: single
current_status_entity: sensor.centauri_carbon_current_status
print_status_entity: sensor.centauri_carbon_print_status
percent_complete_entity: sensor.centauri_carbon_percent_complete
### Printer Light Automations ###
- id: '1770938475431'
alias: Jordyn-A1-lights-on-during-print
description: ''
@@ -111,13 +58,6 @@
device_id: d174bbc498cde1d02e455cb133c04675
entity_id: 0736793ae0d143db76ea7c64d86f4268
domain: switch
- action: light.turn_on
metadata: {}
target:
entity_id: light.jordyn_s_led_controller_jordyn_s_led_controller
data:
color_temp_kelvin: 6500
brightness_pct: 100
mode: single
- id: '1770938581699'
alias: D’s turd flinger Light on during print
@@ -138,7 +78,6 @@
target:
entity_id:
- light.a1_03919d540806387_chamber_light
- light.led_controller_polly_pocket_declan_s_led_controller
data:
color_temp_kelvin: 6500
brightness_pct: 100
@@ -193,48 +132,12 @@
target:
entity_id:
- light.a1_03919d540806387_chamber_light
- light.led_controller_polly_pocket_declan_s_led_controller
data: {}
- type: turn_off
device_id: c63f4513655bdd5b44b998f45b491136
entity_id: fe6d0eee24eb1b397d37b7389137b5b5
domain: switch
mode: single
- id: '1770943760193'
alias: Jordyn-A1-Flash-Red-When-Print-Error
description: ''
triggers:
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
domain: bambu_lab
type: event_print_error
trigger: device
- device_id: 6d6eebb0e8ea6f46ac2ec3a3a5a5eae3
domain: bambu_lab
type: event_print_failed
trigger: device
conditions: []
actions:
- action: light.turn_on
metadata: {}
target:
entity_id: light.jordyn_s_led_controller_jordyn_s_led_controller
data:
rgb_color:
- 255
- 0
- 0
effect: Flash
brightness_pct: 100
- type: turn_off
device_id: d174bbc498cde1d02e455cb133c04675
entity_id: 0736793ae0d143db76ea7c64d86f4268
domain: switch
- action: light.turn_off
metadata: {}
target:
entity_id: light.jordyns_layer_slayer_chamber_light
data: {}
mode: single
- id: '1770944104889'
alias: Prints-Charming-Lights-On-Print-Started
description: 'Repaired by agent: replaced deleted Bambu device trigger with P1S
@@ -253,6 +156,31 @@
entity_id: light.p1s_01p09c470102673_chamber_light
data: {}
mode: single
- id: '1770944453024'
alias: Prints-Charming-Light-Off-Print-Completes
description: 'Repaired by agent: replaced deleted Bambu device trigger with P1S
print_status entity trigger.'
triggers:
- trigger: state
entity_id: sensor.p1s_01p09c470102673_print_status
to:
- finish
- complete
- completed
- idle
- failed
- cancelled
conditions:
- condition: template
value_template: '{{ trigger.from_state is not none and trigger.from_state.state
in [''printing'',''running'',''paused'',''pause'',''prepare'',''preparing''] }}'
actions:
- action: light.turn_off
metadata: {}
data: {}
target:
entity_id: light.p1s_01p09c470102673_chamber_light
mode: single
- id: '1770944259063'
alias: Constipation-Orion-Light-On-When-Print-Starts
description: ''
@@ -271,12 +199,16 @@
mode: single
- id: '1770944373316'
alias: Constipation-Orion-Light-Off-Print-Finishes
description: ''
description: Turn the chamber light off after a completed or cancelled print.
triggers:
- device_id: 615a0b19447425670a6b61ec2343c791
domain: bambu_lab
type: event_print_finished
trigger: device
- device_id: 615a0b19447425670a6b61ec2343c791
domain: bambu_lab
type: event_print_canceled
trigger: device
conditions: []
actions:
- action: light.turn_off
@@ -285,31 +217,6 @@
entity_id: light.constipation_orion_chamber_light
data: {}
mode: single
- id: '1770944453024'
alias: Prints-Charming-Light-Off-Print-Completes
description: 'Repaired by agent: replaced deleted Bambu device trigger with P1S
print_status entity trigger.'
triggers:
- trigger: state
entity_id: sensor.p1s_01p09c470102673_print_status
to:
- finish
- complete
- completed
- idle
- failed
- cancelled
conditions:
- condition: template
value_template: '{{ trigger.from_state is not none and trigger.from_state.state
in [''printing'',''running''] }}'
actions:
- action: light.turn_off
metadata: {}
data: {}
target:
entity_id: light.p1s_01p09c470102673_chamber_light
mode: single
- id: '1771272500745'
alias: polly-pocket-a1-mini-lights-on-during-print
description: ''
@@ -325,7 +232,6 @@
target:
entity_id:
- light.a1mini_0309ca580403653_chamber_light
- light.polly_s_led_controller_polly_s_led_controller
data:
color_temp_kelvin: 6500
brightness_pct: 100
@@ -353,101 +259,68 @@
target:
entity_id:
- light.a1mini_0309ca580403653_chamber_light
- light.polly_s_led_controller_polly_s_led_controller
data: {}
- type: turn_off
device_id: a78c9554b983d1e2459194cc0fee22d3
entity_id: d430d19aa2f118038cb01a3157865ed0
domain: switch
mode: single
- id: '1772979000001'
alias: Constipation-Orion 80mm Exhaust Fan On During Print
description: ''
triggers:
- trigger: device
device_id: 615a0b19447425670a6b61ec2343c791
domain: bambu_lab
type: event_print_started
conditions: []
actions:
- action: fan.turn_on
target:
entity_id: fan.fdm_exhaust_fan_control_80mm_exhaust_fan
mode: single
- id: '1772979000002'
alias: Turn Off 80mm Exhaust Fan 15 Minutes After Constipation-Orion Print Finishes
description: ''
triggers:
- trigger: device
device_id: 615a0b19447425670a6b61ec2343c791
domain: bambu_lab
type: event_print_finished
- trigger: device
device_id: 615a0b19447425670a6b61ec2343c791
domain: bambu_lab
type: event_print_canceled
conditions: []
actions:
- action: fan.turn_on
target:
entity_id: fan.fdm_exhaust_fan_control_80mm_exhaust_fan
- delay:
hours: 0
minutes: 15
seconds: 0
- condition: template
value_template: '{{ states(''sensor.constipation_orion_print_status'') != ''printing''
}}'
- action: fan.turn_off
target:
entity_id: fan.fdm_exhaust_fan_control_80mm_exhaust_fan
### Printer Exhaust Fan Control ###
- id: fdm_printer_exhaust_fans
alias: FDM Printer Exhaust Fans During Prints
description: >-
Turn on the FDM Printer Exhaust Fans outlet when Constipation Orion,
Prints Charming, or Printney Spears (Centauri Carbon) is printing. Leave it
on until all three have been finished for 30 minutes. A pause keeps the
fans on. An unknown printer status does not start the shutoff timer.
mode: restart
- id: '1772979000003'
alias: Prints Charming Or Printney Spears 120mm Exhaust Fan On During Print
description: 'Repaired by agent: replaced deleted Bambu device trigger with P1S
print_status entity trigger.'
triggers:
- trigger: state
entity_id: sensor.p1s_01p09c470102673_print_status
to:
- printing
- running
entity_id:
- sensor.constipation_orion_print_status
- sensor.p1s_01p09c470102673_print_status
- sensor.centauri_carbon_print_status
- trigger: homeassistant
event: start
- trigger: event
event_type: automation_reloaded
conditions: []
actions:
- action: fan.turn_on
target:
entity_id: fan.fdm_exhaust_fan_control_120mm_exhaust_fan
mode: single
- id: '1772979000004'
alias: Turn Off 120mm Exhaust Fan 15 Minutes After Print Completion
description: 'Repaired by agent: replaced deleted Bambu device triggers with P1S
print_status entity trigger.'
triggers:
- trigger: state
entity_id: sensor.p1s_01p09c470102673_print_status
to:
- finish
- complete
- completed
- idle
- failed
- cancelled
conditions:
- condition: template
value_template: '{{ trigger.from_state is not none and trigger.from_state.state
in [''printing'',''running''] }}'
actions:
- action: fan.turn_on
target:
entity_id: fan.fdm_exhaust_fan_control_120mm_exhaust_fan
- delay:
hours: 0
minutes: 15
seconds: 0
- condition: template
value_template: '{{ states(''sensor.p1s_01p09c470102673_print_status'') != ''printing''
}}'
- action: fan.turn_off
target:
entity_id: fan.fdm_exhaust_fan_control_120mm_exhaust_fan
mode: restart
- variables:
active_states:
- printing
- running
- pause
- paused
- pausing
- prepare
- preparing
printers:
- sensor.constipation_orion_print_status
- sensor.p1s_01p09c470102673_print_status
- sensor.centauri_carbon_print_status
- if:
- condition: template
value_template: >-
{{ expand(printers) | selectattr('state', 'in', active_states) | list | count > 0 }}
then:
- action: switch.turn_on
target:
entity_id: switch.studio_plug_2
else:
- condition: template
value_template: "{{ trigger.platform == 'state' }}"
- condition: template
value_template: >-
{{ expand(printers) | selectattr('state', 'in', ['unknown', 'unavailable']) | list | count == 0 }}
- delay:
minutes: 30
- condition: template
value_template: >-
{{ expand(printers) | selectattr('state', 'in', active_states) | list | count == 0
and expand(printers) | selectattr('state', 'in', ['unknown', 'unavailable']) | list | count == 0 }}
- action: switch.turn_off
target:
entity_id: switch.studio_plug_2
@@ -0,0 +1,71 @@
- id: '1767118623336'
alias: Saturn Status Notifications
description: ''
use_blueprint:
path: danielcherubini/elegoo_printer_progress.yaml
input:
percent_complete_entity: sensor.saturn_iv_percent_complete
print_status_entity: sensor.saturn_iv_print_status
current_status_entity: sensor.saturn_iv_current_status
error_status_reason_entity: sensor.saturn_iv_error_status_reason
notify_device: eabafa43fa1148e4facc8e5a4151958d
enable_status_notifications: false
- id: '1767120051057'
alias: Turn off Resin Heater after Print
description: >-
Turn off the heater five minutes after a terminal print status. Any status
change cancels the pending shutoff; busy or unavailable printers never pass
the shutdown check. Exhaust fan control is disabled pending replacement.
triggers:
- trigger: state
entity_id:
- sensor.saturn_iv_print_status
- sensor.saturn_iv_current_status
to: null
- trigger: homeassistant
event: start
- trigger: event
event_type: automation_reloaded
conditions: []
actions:
- condition: template
value_template: >-
{{ states('sensor.saturn_iv_print_status')
in ['complete', 'completed', 'finish', 'idle', 'failed', 'cancelled', 'canceled', 'stopped']
and states('sensor.saturn_iv_current_status')
in ['idle', 'complete', 'completed', 'finish', 'stopped'] }}
- delay:
minutes: 5
- condition: template
value_template: >-
{{ states('sensor.saturn_iv_print_status')
in ['complete', 'completed', 'finish', 'idle', 'failed', 'cancelled', 'canceled', 'stopped']
and states('sensor.saturn_iv_current_status')
in ['idle', 'complete', 'completed', 'finish', 'stopped'] }}
- type: turn_off
device_id: fc9f0c9a71eaa4bd6aa14417425f61cb
entity_id: a91e4bedcc7c5d57563d88bd58d67c9c
domain: switch
- type: turn_off
device_id: 1b1350195291ecd20cf7c69604c09699
entity_id: 74c861175e7d5036c2253583f59d1100
domain: switch
enabled: false
mode: restart
- id: '1767816737180'
alias: Saturn 4 Complete Notification
description: Disabled because the Saturn progress blueprint already sends completion notifications.
initial_state: false
triggers:
- trigger: state
entity_id:
- sensor.saturn_iv_print_status
to:
- complete
conditions: []
actions:
- action: notify.mobile_app_joshuas_iphone_of_pain
metadata: {}
data:
message: Saturn 4 print complete.
mode: single
@@ -47,6 +47,7 @@
# data:
# cycle: true
# mode: single
# - id: '1772978826069'
# alias: Kiosk Navigate Display
# description: ''
@@ -60,52 +61,3 @@
# path: /{{ states('input_select.kiosk_view') }}
# action: browser_mod.navigate
# mode: restart
# - id: '1774407470375'
# alias: ESPHome Fleet Update
# description: ''
# triggers:
# - at: 03:00:00
# trigger: time
# - event_type: esphome_fleet_update_trigger
# trigger: event
# conditions:
# - condition: template
# value_template: "{{ expand(integration_entities('esphome'))\n | selectattr('entity_id',\
# \ 'contains', 'update')\n | selectattr('state', 'eq', 'on')\n | list | count\
# \ > 0 }}\n"
# actions:
# - variables:
# pending_devices: "{{ expand(integration_entities('esphome'))\n | selectattr('entity_id',\
# \ 'contains', 'update')\n | selectattr('state', 'eq', 'on')\n | map(attribute='entity_id')\
# \ | list }}\n"
# pending_names: "{{ expand(integration_entities('esphome'))\n | selectattr('entity_id',\
# \ 'contains', 'update')\n | selectattr('state', 'eq', 'on')\n | map(attribute='name')\
# \ | join(', ') }}\n"
# - action: notify.persistent_notification
# data:
# title: ESPHome Fleet Update Starting
# message: 'Updating {{ pending_devices | count }} device(s): {{ pending_names
# }}
# '
# - repeat:
# for_each: '{{ pending_devices }}'
# sequence:
# - action: update.install
# target:
# entity_id: '{{ repeat.item }}'
# continue_on_error: true
# - wait_template: '{{ not is_state(repeat.item, ''on'') }}
# '
# timeout: 00:05:00
# continue_on_timeout: true
# - delay:
# minutes: 2
# - action: notify.persistent_notification
# data:
# title: ESPHome Fleet Update Complete
# message: 'Finished updating {{ pending_devices | count }} device(s): {{ pending_names
# }}
# '
@@ -0,0 +1,49 @@
# - id: '1774407470375'
# alias: ESPHome Fleet Update
# description: ''
# triggers:
# - at: 03:00:00
# trigger: time
# - event_type: esphome_fleet_update_trigger
# trigger: event
# conditions:
# - condition: template
# value_template: "{{ expand(integration_entities('esphome'))\n | selectattr('entity_id',\
# \ 'contains', 'update')\n | selectattr('state', 'eq', 'on')\n | list | count\
# \ > 0 }}\n"
# actions:
# - variables:
# pending_devices: "{{ expand(integration_entities('esphome'))\n | selectattr('entity_id',\
# \ 'contains', 'update')\n | selectattr('state', 'eq', 'on')\n | map(attribute='entity_id')\
# \ | list }}\n"
# pending_names: "{{ expand(integration_entities('esphome'))\n | selectattr('entity_id',\
# \ 'contains', 'update')\n | selectattr('state', 'eq', 'on')\n | map(attribute='name')\
# \ | join(', ') }}\n"
# - action: notify.persistent_notification
# data:
# title: ESPHome Fleet Update Starting
# message: 'Updating {{ pending_devices | count }} device(s): {{ pending_names
# }}
# '
# - repeat:
# for_each: '{{ pending_devices }}'
# sequence:
# - action: update.install
# target:
# entity_id: '{{ repeat.item }}'
# continue_on_error: true
# - wait_template: '{{ not is_state(repeat.item, ''on'') }}
# '
# timeout: 00:05:00
# continue_on_timeout: true
# - delay:
# minutes: 2
# - action: notify.persistent_notification
# data:
# title: ESPHome Fleet Update Complete
# message: 'Finished updating {{ pending_devices | count }} device(s): {{ pending_names
# }}
# '
-111
View File
@@ -1,111 +0,0 @@
# Climate and plug automations.
- id: ac_controls_matter_plugs
alias: AC controls Matter plugs
description: Turn the four Tapo Matter plugs on while the Ecobee is actively cooling, and off when cooling stops.
mode: restart
triggers:
- trigger: state
entity_id: climate.meat_heater
attribute: hvac_action
to: cooling
id: ac_on
- trigger: state
entity_id: climate.meat_heater
attribute: hvac_action
from: cooling
id: ac_off
conditions: []
actions:
- choose:
- conditions:
- condition: trigger
id: ac_on
sequence:
- action: switch.turn_on
continue_on_error: true
target:
entity_id:
- switch.smart_wi_fi_plug
- switch.smart_wi_fi_plug_2
- switch.smart_wi_fi_plug_3
- switch.smart_wi_fi_plug_4
- conditions:
- condition: trigger
id: ac_off
sequence:
- action: switch.turn_off
continue_on_error: true
target:
entity_id:
- switch.smart_wi_fi_plug
- switch.smart_wi_fi_plug_2
- switch.smart_wi_fi_plug_3
- switch.smart_wi_fi_plug_4
- id: dehumidifier_tank_full_shutoff
alias: Dehumidifier Tank Full - Shut Off Parents Window Fan
description: When the dehumidifier tank level sensor detects water, turn off the Parents Window Fan outlet and send an alerts-channel notification.
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.water_leak_sensor_moisture
to: 'on'
conditions: []
actions:
- action: switch.turn_off
target:
entity_id: switch.parents_window_fan
- action: notify.notify_events_alerts
data:
title: Dehumidifier tank is full
message: Dehumidifier tank is full — Dehumidifer outlet has been turned off.
- id: ac_tank_full_shutoff
alias: AC Tank Full - Shut Off Office AC Power
description: When the AC Tank Sensor detects water, turn off the Office AC Power outlet and send an alerts-channel notification.
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.ac_tank_sensor_moisture
to: 'on'
conditions: []
actions:
- action: switch.turn_off
target:
entity_id: switch.studio_plug_2
- action: notify.notify_events_alerts
data:
title: AC tank is full
message: AC Tank Sensor detected water — Office AC Power outlet has been turned off.
- id: dehumidifier_tank_sensor_low_battery
alias: Dehumidifier Tank Sensor Low Battery
description: Alert when the dehumidifier tank level sensor battery is at 10% or lower.
mode: single
triggers:
- trigger: numeric_state
entity_id: sensor.water_leak_sensor_battery
below: 10.1
conditions: []
actions:
- action: notify.notify_events_alerts
data:
title: Dehumidifier tank sensor battery low
message: Dehumidifier tank level sensor battery is at {{ states('sensor.water_leak_sensor_battery') }}%. Replace soon.
- id: ac_tank_sensor_low_battery
alias: AC Tank Sensor Low Battery
description: Alert when the AC tank sensor battery is at 10% or lower.
mode: single
triggers:
- trigger: numeric_state
entity_id: sensor.ac_tank_sensor_battery
below: 10.1
conditions: []
actions:
- action: notify.notify_events_alerts
data:
title: AC tank sensor battery low
message: AC Tank Sensor battery is at {{ states('sensor.ac_tank_sensor_battery') }}%. Replace soon.
@@ -1,62 +0,0 @@
# Climate and plug automations.
- id: hermes_ac_controls_matter_plugs
alias: AC controls Matter plugs
description: Turn the four Tapo Matter plugs on while the Ecobee is actively cooling, and off when cooling stops.
mode: restart
triggers:
- trigger: state
entity_id: climate.meat_heater
attribute: hvac_action
to: cooling
id: ac_on
- trigger: state
entity_id: climate.meat_heater
attribute: hvac_action
from: cooling
id: ac_off
conditions: []
actions:
- choose:
- conditions:
- condition: trigger
id: ac_on
sequence:
- action: switch.turn_on
continue_on_error: true
target:
entity_id:
- switch.smart_wi_fi_plug
- switch.smart_wi_fi_plug_2
- switch.smart_wi_fi_plug_3
- switch.smart_wi_fi_plug_4
- conditions:
- condition: trigger
id: ac_off
sequence:
- action: switch.turn_off
continue_on_error: true
target:
entity_id:
- switch.smart_wi_fi_plug
- switch.smart_wi_fi_plug_2
- switch.smart_wi_fi_plug_3
- switch.smart_wi_fi_plug_4
- id: dehumidifier_tank_full_shutoff
alias: Dehumidifier Tank Full - Shut Off Parents Window Fan
description: When the dehumidifier tank level sensor detects water, turn off the Parents Window Fan outlet and send an alerts-channel notification.
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.water_leak_sensor_moisture
to: 'on'
conditions: []
actions:
- action: switch.turn_off
target:
entity_id: switch.parents_window_fan
- action: script.notify_events_alerts
data:
title: Dehumidifier tank is full
message: Dehumidifier tank is full — Parents Window Fan outlet has been turned off.
@@ -1,62 +0,0 @@
# Climate and plug automations.
- id: hermes_ac_controls_matter_plugs
alias: AC controls Matter plugs
description: Turn the four Tapo Matter plugs on while the Ecobee is actively cooling, and off when cooling stops.
mode: restart
triggers:
- trigger: state
entity_id: climate.meat_heater
attribute: hvac_action
to: cooling
id: ac_on
- trigger: state
entity_id: climate.meat_heater
attribute: hvac_action
from: cooling
id: ac_off
conditions: []
actions:
- choose:
- conditions:
- condition: trigger
id: ac_on
sequence:
- action: switch.turn_on
continue_on_error: true
target:
entity_id:
- switch.smart_wi_fi_plug
- switch.smart_wi_fi_plug_2
- switch.smart_wi_fi_plug_3
- switch.smart_wi_fi_plug_4
- conditions:
- condition: trigger
id: ac_off
sequence:
- action: switch.turn_off
continue_on_error: true
target:
entity_id:
- switch.smart_wi_fi_plug
- switch.smart_wi_fi_plug_2
- switch.smart_wi_fi_plug_3
- switch.smart_wi_fi_plug_4
- id: dehumidifier_tank_full_shutoff
alias: Dehumidifier Tank Full - Shut Off Parents Window Fan
description: When the dehumidifier tank level sensor detects water, turn off the Parents Window Fan outlet and send an alerts-channel notification.
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.water_leak_sensor_moisture
to: 'on'
conditions: []
actions:
- action: switch.turn_off
target:
entity_id: switch.parents_window_fan
- action: notify.notify_events_alerts
data:
title: Dehumidifier tank is full
message: Dehumidifier tank is full — Parents Window Fan outlet has been turned off.
@@ -0,0 +1,48 @@
- id: ac_controls_matter_plugs
alias: AC controls Matter plugs
description: Reconcile each plug with current cooling activity after HVAC changes, startup, reload and plug recovery. Keep existing plug states if climate status is unknown.
mode: restart
triggers:
- trigger: state
entity_id: climate.meat_heater
attribute: hvac_action
to: null
- trigger: state
entity_id: climate.meat_heater
to: null
- trigger: state
entity_id:
- switch.smart_wi_fi_plug
- switch.smart_wi_fi_plug_2
- switch.smart_wi_fi_plug_3
- switch.smart_wi_fi_plug_4
to: null
- trigger: homeassistant
event: start
- trigger: event
event_type: automation_reloaded
conditions: []
actions:
- condition: template
value_template: >-
{{ has_value('climate.meat_heater')
and (is_state('climate.meat_heater', 'off')
or state_attr('climate.meat_heater', 'hvac_action')
not in [none, 'unknown', 'unavailable', '']) }}
- variables:
desired_state: "{{ 'on' if is_state_attr('climate.meat_heater', 'hvac_action', 'cooling') and not is_state('climate.meat_heater', 'off') else 'off' }}"
- repeat:
for_each:
- switch.smart_wi_fi_plug
- switch.smart_wi_fi_plug_2
- switch.smart_wi_fi_plug_3
- switch.smart_wi_fi_plug_4
sequence:
- if:
- condition: template
value_template: "{{ has_value(repeat.item) and not is_state(repeat.item, desired_state) }}"
then:
- action: "{{ 'switch.turn_on' if desired_state == 'on' else 'switch.turn_off' }}"
continue_on_error: true
target:
entity_id: "{{ repeat.item }}"
@@ -0,0 +1,25 @@
- id: dehumidifier_tank_full_shutoff
alias: Dehumidifier Tank Full - Shut Off Parents Window Fan
description: Keep the outlet off while wet and report whether shutdown was confirmed.
use_blueprint:
path: local/tank_full_interlock.yaml
input:
moisture_sensor: binary_sensor.water_leak_sensor_moisture
outlet: switch.parents_window_fan
title: Dehumidifier tank is full
equipment_name: Dehumidifier
- id: dehumidifier_tank_sensor_low_battery
alias: Dehumidifier Tank Sensor Low Battery
description: Alert when the dehumidifier tank level sensor battery is at 10% or lower.
mode: single
triggers:
- trigger: numeric_state
entity_id: sensor.water_leak_sensor_battery
below: 10.1
conditions: []
actions:
- action: script.notify_events_alerts
data:
title: Dehumidifier tank sensor battery low
message: Dehumidifier tank level sensor battery is at {{ states('sensor.water_leak_sensor_battery') }}%. Replace soon.
@@ -0,0 +1,29 @@
- id: ac_tank_full_shutoff
alias: AC Tank Full
description: Alert when the AC tank sensor is wet. This does not switch an outlet.
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.ac_tank_sensor_moisture
to: "on"
conditions: []
actions:
- action: script.notify_events_alerts
data:
title: AC tank is full
message: AC tank sensor detected water.
- id: ac_tank_sensor_low_battery
alias: AC Tank Sensor Low Battery
description: Alert when the AC tank sensor battery is at 10% or lower.
mode: single
triggers:
- trigger: numeric_state
entity_id: sensor.ac_tank_sensor_battery
below: 10.1
conditions: []
actions:
- action: script.notify_events_alerts
data:
title: AC tank sensor battery low
message: AC Tank Sensor battery is at {{ states('sensor.ac_tank_sensor_battery') }}%. Replace soon.
@@ -0,0 +1,79 @@
- id: '1744503044129'
alias: Set Alexa Announcement Volume
description: Set Alexa announcement volume for structured daily routine
triggers:
- at: 08:15:00
trigger: time
conditions:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
- condition: state
entity_id: binary_sensor.day_camp_today
state: 'on'
actions:
- data:
media:
media_content_id: Set kids speakers to 6
media_content_type: routine
metadata: {}
action: media_player.play_media
target:
device_id: 7c55da01c1d0e4ee7900852bb7621b1e
mode: single
- id: '1744503044145'
alias: Reset Alexa Announcement Volume - Camp Days
description: Set Alexa announcement volume for structured daily routine
trigger:
- platform: time
at: 09:15:00
condition:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
action:
- condition: state
entity_id: binary_sensor.day_camp_today
state: 'on'
- service: media_player.play_media
data:
media_content_id: Reset Alexa Volume
media_content_type: routine
target:
entity_id: media_player.basement_show
mode: single
- id: '1744503044146'
alias: Reset Alexa Announcement Volume - No Camp Days
description: Set Alexa announcement volume for structured daily routine
trigger:
- platform: time
at: '19:17:00'
condition:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
action:
- condition: state
entity_id: binary_sensor.day_camp_today
state: 'off'
- service: media_player.play_media
data:
media_content_id: Reset Alexa Volume
media_content_type: routine
target:
entity_id: media_player.basement_show
mode: single
@@ -0,0 +1,28 @@
- id: '1788133654303'
alias: Set weekday announcement speakers to volume 5
description: Set the school-announcement speakers to volume level 5 each weekday.
triggers:
- trigger: time
at: '07:10:00'
conditions:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
- condition: state
entity_id: binary_sensor.blackhawk_school_day
state: 'on'
actions:
- action: media_player.volume_set
target:
entity_id:
- media_player.family_room_show
- media_player.living_room
- media_player.kitchen
- media_player.declan_s_room
data:
volume_level: 0.5
mode: single
@@ -0,0 +1,67 @@
blueprint:
name: NestQuest afternoon reminder
description: 'Sends an afternoon nudge listing ONLY the children who still have
open quests today. Children who are present and all clear are omitted (the nudge
is for stragglers, not a roll call), children who are not present today are omitted
entirely, and the household rollup sensors are excluded by the same present guard.
When every present child is already done the automation sends NOTHING at all:
a template condition stops the run before the notification. The notify service
is chosen by the household when importing this blueprint — NestQuest never hard-codes
a personal notify target.'
domain: automation
input:
afternoon_time:
name: Afternoon time
description: Local time of day the reminder is sent.
default: '15:00'
selector:
time: {}
notify_target:
name: Notify service
description: The Home Assistant notify service to call for the reminder, for
example notify.mobile_app_your_phone — a placeholder; pick the notify service
for your own device.
selector:
text:
multiline: false
multiple: false
send_reminder:
name: Send the reminder
description: Turn off to keep the automation installed but skip the notification
entirely.
default: true
selector:
boolean: {}
source_url: https://github.com/Maniacs-Mansion/NestQuest/blob/main/custom_components/nestquest/blueprints/automation/afternoon_reminder.yaml
mode: single
variables:
send_reminder: !input send_reminder
triggers:
- trigger: time
at: !input afternoon_time
conditions:
- condition: template
value_template: '{{ send_reminder | bool }}'
- condition: template
value_template: "{%- set ns = namespace(any_open=false) -%}\n{%- for due in states.sensor\n
\ | selectattr('entity_id', 'search', '^sensor\\\\.nestquest_.*_quests_due_today(_\\\\d+)?$')
-%}\n {%- if due.attributes.get('present') -%}\n {%- set remaining_values
= states.sensor\n | selectattr('entity_id', 'search', '^sensor\\\\.nestquest_.*_quests_remaining_today(_\\\\d+)?$')\n
\ | selectattr('attributes.child_id', 'eq', due.attributes.child_id)\n
\ | map(attribute='state')\n | list -%}\n {%- if remaining_values
and (remaining_values | first | int) > 0 -%}\n {%- set ns.any_open = true
-%}\n {%- endif -%}\n {%- endif -%}\n{%- endfor -%}\n{{- ns.any_open -}}\n"
actions:
- action: !input notify_target
data:
title: NestQuest afternoon reminder
message: "{%- set ns = namespace(lines=[]) -%}\n{%- for due in states.sensor\n
\ | selectattr('entity_id', 'search', '^sensor\\\\.nestquest_.*_quests_due_today(_\\\\d+)?$')
-%}\n {%- if due.attributes.get('present') -%}\n {%- set remaining_values
= states.sensor\n | selectattr('entity_id', 'search', '^sensor\\\\.nestquest_.*_quests_remaining_today(_\\\\d+)?$')\n
\ | selectattr('attributes.child_id', 'eq', due.attributes.child_id)\n
\ | map(attribute='state')\n | list -%}\n {%- if remaining_values
and (remaining_values | first | int) > 0 -%}\n {%- set line = due.attributes.child_name
~ ' still has ' ~ (remaining_values | first | int) ~ ' quest(s) open.' -%}\n
\ {%- set ns.lines = ns.lines + [line] -%}\n {%- endif -%}\n {%- endif
-%}\n{%- endfor -%}\n{{- ns.lines | join('\\n') -}}"
@@ -0,0 +1,87 @@
blueprint:
name: NestQuest day complete celebration
description: 'Celebrates the moment a child clears every quest for the day: the
automation is driven by the nestquest_child_day_complete event, which NestQuest
only fires when quests were owed and all of them are complete — a zero-quest day
can never trigger this celebration. Once-per-child-per-day dedupe: point last_celebrated_helper
at ONE input_text helper (create it under Settings, Devices and Services, Helpers
— plain text). The automation records each celebrated child as "<child_id>:<today>"
inside that helper, and a child who un-completes a quest and completes it again
the same day is celebrated once, not twice — while a SECOND child clearing their
day the same date is still celebrated in full. The helper is pruned to today''s
entries on every celebration, so it never grows past one entry per child per day.
A helper that does not exist yet, or has never been set, lets the first celebration
through. The notification message may use the event payload''s child_name, quests_due
and quests_completed. The notify service is chosen by the household when importing
this blueprint — NestQuest never hard-codes a personal notify target.'
domain: automation
input:
notify_target:
name: Notify service
description: The Home Assistant notify service to call for the celebration,
for example notify.mobile_app_your_phone — a placeholder; pick the notify
service for your own device.
selector:
text:
multiline: false
multiple: false
celebration_message:
name: Celebration message
description: 'The message sent when a child clears every quest today. May use
the event payload: trigger.event.data.child_name, trigger.event.data.quests_due
and trigger.event.data.quests_completed.'
default: Legendary! {{ trigger.event.data.child_name }} cleared every quest
today!
selector:
text:
multiline: false
multiple: false
celebrate:
name: Send the celebration
description: Turn off to keep the automation installed but skip the notification
entirely.
default: true
selector:
boolean: {}
last_celebrated_helper:
name: Last-celebrated helper
description: A single input_text helper remembering which children were already
celebrated today, as "<child_id>:<date>" entries separated by commas. The
automation skips a child whose entry for today is already stored, and rewrites
the helper with only today's entries plus the new one after each celebration.
selector:
entity:
filter:
- domain:
- input_text
multiple: false
reorder: false
source_url: https://github.com/Maniacs-Mansion/NestQuest/blob/main/custom_components/nestquest/blueprints/automation/day_complete_celebration.yaml
mode: queued
variables:
celebrate: !input celebrate
last_celebrated_helper: !input last_celebrated_helper
triggers:
- trigger: event
event_type: nestquest_child_day_complete
conditions:
- condition: template
value_template: '{{ celebrate | bool }}'
- condition: template
value_template: "{{ (trigger.event.data.child_id ~ ':' ~ now().strftime('%Y-%m-%d'))\n
\ not in ((states(last_celebrated_helper) or '')\n .split(',')\n |
select('ne', '')\n | list) }}"
actions:
- action: !input notify_target
data:
title: NestQuest day complete
message: !input celebration_message
- action: input_text.set_value
target:
entity_id: !input last_celebrated_helper
data:
value: "{%- set today = now().strftime('%Y-%m-%d') -%} {%- set ns = namespace(entries=[])
-%} {%- for entry in (states(last_celebrated_helper) or '').split(',') -%}\n
\ {%- if entry and entry.endswith(':' ~ today) -%}\n {%- set ns.entries =
ns.entries + [entry] -%}\n {%- endif -%}\n{%- endfor -%} {{ (ns.entries + [trigger.event.data.child_id
~ ':' ~ today]) | join(',') }}"
@@ -0,0 +1,89 @@
blueprint:
name: NestQuest end-of-day report
description: Sends the parent an end-of-day report on how the day went. It runs
deliberately BEFORE NestQuest's midnight missed-quest sweep, so it reads the same
day's data (what is still open at report time, and how much of the day was completed)
rather than tomorrow-morning's hindsight. Every PRESENT child gets a completion
line, so the parent always receives the day's summary; children who still owe
quests get their open count called out on that line, and children who are not
present today are omitted entirely. A child whose remaining-count sensor is unavailable
is reported with the completion line only. By default the report is skipped when
every present child is already done — turn on send-even-when-clear to always get
the day's completion summary. The notify service is chosen by the household when
importing this blueprint — NestQuest never hard-codes a personal notify target.
domain: automation
input:
report_time:
name: Report time
description: Local time of day the report is sent — deliberately before the
midnight missed-quest sweep, so the report reads the same day's data.
default: '20:00'
selector:
time: {}
notify_target:
name: Notify service
description: The Home Assistant notify service to call for the report, for example
notify.mobile_app_your_phone — a placeholder; pick the notify service for
your own device.
selector:
text:
multiline: false
multiple: false
send_report:
name: Send the report
description: Turn off to keep the automation installed but skip the notification
entirely.
default: true
selector:
boolean: {}
send_even_when_clear:
name: Send even when clear
description: Send the report even when every present child has already finished
their quests today. When off (the default), an all-clear day sends nothing.
default: false
selector:
boolean: {}
source_url: https://github.com/Maniacs-Mansion/NestQuest/blob/main/custom_components/nestquest/blueprints/automation/end_of_day_report.yaml
mode: single
variables:
send_report: !input send_report
send_even_when_clear: !input send_even_when_clear
triggers:
- trigger: time
at: !input report_time
conditions:
- condition: template
value_template: '{{ send_report | bool }}'
- condition: template
value_template: "{%- set ns = namespace(any_open=false) -%}\n{%- for due in states.sensor\n
\ | selectattr('entity_id', 'search', '^sensor\\\\.nestquest_.*_quests_due_today(_\\\\d+)?$')
-%}\n {%- if due.attributes.get('present') -%}\n {%- set remaining_values
= states.sensor\n | selectattr('entity_id', 'search', '^sensor\\\\.nestquest_.*_quests_remaining_today(_\\\\d+)?$')\n
\ | selectattr('attributes.child_id', 'eq', due.attributes.child_id)\n
\ | map(attribute='state')\n | list -%}\n {%- if not remaining_values
or (remaining_values | first | int(-1)) != 0 -%}\n {%- set ns.any_open =
true -%}\n {%- endif -%}\n {%- endif -%}\n{%- endfor -%}\n{{- send_even_when_clear
| bool or ns.any_open -}}\n"
actions:
- action: !input notify_target
data:
title: NestQuest end-of-day report
message: "{%- set ns = namespace(lines=[]) -%}\n{%- for due in states.sensor\n
\ | selectattr('entity_id', 'search', '^sensor\\\\.nestquest_.*_quests_due_today(_\\\\d+)?$')
-%}\n {%- if due.attributes.get('present') -%}\n {%- set due_count = due.state
| int -%}\n {%- set completed_values = states.sensor\n | selectattr('entity_id',
'search', '^sensor\\\\.nestquest_.*_quests_completed_today(_\\\\d+)?$')\n |
selectattr('attributes.child_id', 'eq', due.attributes.child_id)\n |
map(attribute='state')\n | list -%}\n {%- set remaining_values =
states.sensor\n | selectattr('entity_id', 'search', '^sensor\\\\.nestquest_.*_quests_remaining_today(_\\\\d+)?$')\n
\ | selectattr('attributes.child_id', 'eq', due.attributes.child_id)\n
\ | map(attribute='state')\n | list -%}\n {%- set completed_count
= (completed_values | first | int) if completed_values else 0 -%}\n {%- set
remaining_count = (remaining_values | first | int) if remaining_values else
none -%}\n {%- if remaining_count is not none and remaining_count > 0 -%}\n
\ {%- set line = due.attributes.child_name ~ ': ' ~ remaining_count ~ '
still open (' ~ completed_count ~ '/' ~ due_count ~ ' done).' -%}\n {%- else
-%}\n {%- set line = due.attributes.child_name ~ ': ' ~ completed_count
~ '/' ~ due_count ~ ' done.' -%}\n {%- endif -%}\n {%- set ns.lines =
ns.lines + [line] -%}\n {%- endif -%}\n{%- endfor -%}\n{{- ns.lines | join('\\n')
-}}"
@@ -0,0 +1,92 @@
blueprint:
name: NestQuest UniFi quest access
description: One automation per child. Completely block the selected UniFi clients
after the local weekday/weekend cutoff while that child has quests remaining TODAY.
Restore network access immediately when the count reaches zero, and at midnight
even if yesterday was incomplete. A missing or unavailable quest sensor is treated
as still open after the cutoff; verify the sensor before enabling this automation.
The selected UniFi client switches are ON when access is allowed and OFF when
blocked. Do not select infrastructure or adult devices.
domain: automation
input:
remaining_sensor:
name: Child's quests remaining today
description: Select this child's NestQuest quests remaining today sensor.
selector:
entity:
filter:
- integration: nestquest
domain:
- sensor
multiple: false
reorder: false
client_switches:
name: Child's UniFi client access switches
description: Select every UniFi client access switch for this child. These switches
are ON when network access is allowed and OFF when blocked.
selector:
entity:
multiple: true
filter:
- integration: unifi
domain:
- switch
reorder: false
weekday_cutoff:
name: Monday-Friday cutoff
default: '18:25:00'
selector:
time: {}
weekend_cutoff:
name: Saturday-Sunday cutoff
default: '14:00:00'
selector:
time: {}
source_url: https://github.com/Maniacs-Mansion/NestQuest/blob/main/custom_components/nestquest/blueprints/automation/unifi_quest_access.yaml
mode: restart
variables:
remaining_sensor: !input remaining_sensor
client_switches: !input client_switches
weekday_cutoff: !input weekday_cutoff
weekend_cutoff: !input weekend_cutoff
triggers:
- trigger: state
entity_id: !input remaining_sensor
- trigger: time
at: !input weekday_cutoff
- trigger: time
at: !input weekend_cutoff
- trigger: time
at: 00:00:00
- trigger: homeassistant
event: start
- trigger: time_pattern
minutes: /5
conditions:
- condition: template
value_template: '{{ client_switches | count > 0 }}'
actions:
- variables:
should_block: "{{ now().strftime('%H:%M') >=\n (weekday_cutoff[:5] if now().weekday()
< 5 else weekend_cutoff[:5])\n and (states(remaining_sensor) | int(-1)) !=
0 }}"
- repeat:
for_each: '{{ client_switches }}'
sequence:
- choose:
- conditions:
- condition: template
value_template: '{{ should_block | bool and is_state(repeat.item, ''on'')
}}'
sequence:
- action: switch.turn_off
target:
entity_id: '{{ repeat.item }}'
- conditions:
- condition: template
value_template: '{{ not (should_block | bool) and is_state(repeat.item,
''off'') }}'
sequence:
- action: switch.turn_on
target:
entity_id: '{{ repeat.item }}'
@@ -0,0 +1,29 @@
blueprint:
name: Litter box - binary alert notification
description: Notify the cats channel when a litter-box waste-bin or error flag turns on.
domain: automation
input:
alert_sensor:
name: Alert sensor
selector:
entity:
domain: binary_sensor
title:
name: Notification title
selector:
text: {}
message:
name: Notification message
selector:
text: {}
triggers:
- trigger: state
entity_id: !input alert_sensor
to: 'on'
conditions: []
actions:
- action: notify.notify_events_cats
data:
title: !input title
message: !input message
mode: queued
@@ -0,0 +1,41 @@
blueprint:
name: Litter level - low notification
description: Notify once after a numeric litter level stays at or below its threshold for two minutes. Refilling above the threshold rearms the alert.
domain: automation
input:
litter_sensor:
name: Litter level sensor
selector:
entity:
domain: sensor
threshold:
name: Low level threshold (inclusive)
default: 65
selector:
number:
min: 0
max: 100
title:
name: Notification title
selector:
text: {}
message:
name: Notification message
selector:
text: {}
trigger_variables:
litter_sensor: !input litter_sensor
threshold: !input threshold
triggers:
- trigger: template
value_template: >-
{{ is_number(states(litter_sensor))
and states(litter_sensor) | float(101) <= threshold | float(65) }}
for: '00:02:00'
conditions: []
actions:
- action: notify.notify_events_cats
data:
title: !input title
message: !input message
mode: single
@@ -0,0 +1,73 @@
blueprint:
name: Tank full - outlet interlock and verified notification
description: >-
Request outlet shutdown when a tank is wet, including attempts to turn it
back on, startup and recovery. Verify the outlet reports off before claiming
success. Retry unconfirmed shutdowns every five minutes while wet.
domain: automation
input:
moisture_sensor:
name: Tank moisture sensor
selector:
entity:
domain: binary_sensor
outlet:
name: Protected outlet
selector:
entity:
domain: switch
title:
name: Alert title
selector:
text: {}
equipment_name:
name: Equipment name for the notification
selector:
text: {}
variables:
outlet: !input outlet
equipment_name: !input equipment_name
triggers:
- trigger: state
entity_id: !input moisture_sensor
to: 'on'
id: wet
- trigger: state
entity_id: !input outlet
to: 'on'
id: outlet_on
- trigger: homeassistant
event: start
id: startup
- trigger: event
event_type: automation_reloaded
id: reload
- trigger: time_pattern
minutes: '/5'
id: retry
conditions:
- condition: state
entity_id: !input moisture_sensor
state: 'on'
- condition: template
value_template: "{{ trigger.id != 'retry' or not is_state(outlet, 'off') }}"
actions:
- action: switch.turn_off
continue_on_error: true
target:
entity_id: !input outlet
- wait_template: "{{ is_state(outlet, 'off') }}"
timeout: '00:00:10'
continue_on_timeout: true
- action: script.notify_events_alerts
data:
title: !input title
message: >-
{{ equipment_name }} tank sensor detected water.
{% if is_state(outlet, 'off') %}
The outlet reports off.
{% else %}
Shutoff could not be confirmed (outlet state: {{ states(outlet) }}).
Check the equipment and outlet.
{% endif %}
mode: restart
@@ -0,0 +1,90 @@
blueprint:
name: NestQuest afternoon reminder
description: >-
Sends an afternoon nudge listing ONLY the children who still have
open quests today. Children who are present and all clear are
omitted (the nudge is for stragglers, not a roll call), children
who are not present today are omitted entirely, and the household
rollup sensors are excluded by the same present guard. When every
present child is already done the automation sends NOTHING at all:
a template condition stops the run before the notification. The
notify service is chosen by the household when importing this
blueprint — NestQuest never hard-codes a personal notify target.
domain: automation
input:
afternoon_time:
name: Afternoon time
description: Local time of day the reminder is sent.
default: "15:00"
selector:
time: {}
notify_target:
name: Notify service
description: >-
The Home Assistant notify service to call for the reminder,
for example notify.mobile_app_your_phone — a placeholder;
pick the notify service for your own device.
selector:
text: {}
send_reminder:
name: Send the reminder
description: >-
Turn off to keep the automation installed but skip the
notification entirely.
default: true
selector:
boolean: {}
mode: single
variables:
send_reminder: !input send_reminder
triggers:
- trigger: time
at: !input afternoon_time
conditions:
- condition: template
value_template: "{{ send_reminder | bool }}"
# Send nothing when every present child is already done: the guard is
# false unless at least one present child has remaining quests today.
- condition: template
value_template: |
{%- set ns = namespace(any_open=false) -%}
{%- for due in states.sensor
| selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_due_today(_\\d+)?$') -%}
{%- if due.attributes.get('present') -%}
{%- set remaining_values = states.sensor
| selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_remaining_today(_\\d+)?$')
| selectattr('attributes.child_id', 'eq', due.attributes.child_id)
| map(attribute='state')
| list -%}
{%- if remaining_values and (remaining_values | first | int) > 0 -%}
{%- set ns.any_open = true -%}
{%- endif -%}
{%- endif -%}
{%- endfor -%}
{{- ns.any_open -}}
actions:
- action: !input notify_target
data:
title: NestQuest afternoon reminder
message: |
{%- set ns = namespace(lines=[]) -%}
{%- for due in states.sensor
| selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_due_today(_\\d+)?$') -%}
{%- if due.attributes.get('present') -%}
{%- set remaining_values = states.sensor
| selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_remaining_today(_\\d+)?$')
| selectattr('attributes.child_id', 'eq', due.attributes.child_id)
| map(attribute='state')
| list -%}
{%- if remaining_values and (remaining_values | first | int) > 0 -%}
{%- set line = due.attributes.child_name ~ ' still has ' ~ (remaining_values | first | int) ~ ' quest(s) open.' -%}
{%- set ns.lines = ns.lines + [line] -%}
{%- endif -%}
{%- endif -%}
{%- endfor -%}
{{- ns.lines | join('\n') -}}
@@ -0,0 +1,109 @@
blueprint:
name: NestQuest day complete celebration
description: >-
Celebrates the moment a child clears every quest for the day: the
automation is driven by the nestquest_child_day_complete event,
which NestQuest only fires when quests were owed and all of them
are complete — a zero-quest day can never trigger this
celebration. Once-per-child-per-day dedupe: point
last_celebrated_helper at ONE input_text helper (create it under
Settings, Devices and Services, Helpers — plain text). The
automation records each celebrated child as
"<child_id>:<today>" inside that helper, and a child who
un-completes a quest and completes it again the same day is
celebrated once, not twice — while a SECOND child clearing their
day the same date is still celebrated in full. The helper is
pruned to today's entries on every celebration, so it never
grows past one entry per child per day. A helper that does not
exist yet, or has never been set, lets the first celebration
through. The notification message may use the event payload's
child_name, quests_due and quests_completed. The notify service
is chosen by the household when importing this blueprint —
NestQuest never hard-codes a personal notify target.
domain: automation
input:
notify_target:
name: Notify service
description: >-
The Home Assistant notify service to call for the celebration,
for example notify.mobile_app_your_phone — a placeholder;
pick the notify service for your own device.
selector:
text: {}
celebration_message:
name: Celebration message
description: >-
The message sent when a child clears every quest today. May
use the event payload: trigger.event.data.child_name,
trigger.event.data.quests_due and trigger.event.data.quests_completed.
default: "Legendary! {{ trigger.event.data.child_name }} cleared every quest today!"
selector:
text: {}
celebrate:
name: Send the celebration
description: >-
Turn off to keep the automation installed but skip the
notification entirely.
default: true
selector:
boolean: {}
last_celebrated_helper:
name: Last-celebrated helper
description: >-
A single input_text helper remembering which children were
already celebrated today, as "<child_id>:<date>" entries
separated by commas. The automation skips a child whose entry
for today is already stored, and rewrites the helper with only
today's entries plus the new one after each celebration.
selector:
entity:
filter:
- domain: input_text
mode: queued
variables:
celebrate: !input celebrate
last_celebrated_helper: !input last_celebrated_helper
triggers:
- trigger: event
event_type: nestquest_child_day_complete
conditions:
- condition: template
value_template: "{{ celebrate | bool }}"
# Once-per-child-per-day dedupe: celebrate only when THIS child has
# no equal entry for today in the helper. Entries compare as whole
# comma-separated tokens — a substring check would wrongly suppress
# child 1 after child 11 was celebrated. An unset or missing helper
# reads as '' and celebrates.
- condition: template
value_template: >-
{{ (trigger.event.data.child_id ~ ':' ~ now().strftime('%Y-%m-%d'))
not in ((states(last_celebrated_helper) or '')
.split(',')
| select('ne', '')
| list) }}
actions:
- action: !input notify_target
data:
title: NestQuest day complete
message: !input celebration_message
# Record this child's celebration, keeping only today's entries so the
# helper never outgrows its text limit and stale entries cannot block
# next week's celebrations.
- action: input_text.set_value
target:
entity_id: !input last_celebrated_helper
data:
value: >-
{%- set today = now().strftime('%Y-%m-%d') -%}
{%- set ns = namespace(entries=[]) -%}
{%- for entry in (states(last_celebrated_helper) or '').split(',') -%}
{%- if entry and entry.endswith(':' ~ today) -%}
{%- set ns.entries = ns.entries + [entry] -%}
{%- endif -%}
{%- endfor -%}
{{ (ns.entries + [trigger.event.data.child_id ~ ':' ~ today]) | join(',') }}
@@ -0,0 +1,121 @@
blueprint:
name: NestQuest end-of-day report
description: >-
Sends the parent an end-of-day report on how the day went. It runs
deliberately BEFORE NestQuest's midnight missed-quest sweep, so it
reads the same day's data (what is still open at report time, and
how much of the day was completed) rather than tomorrow-morning's
hindsight. Every PRESENT child gets a completion line, so the
parent always receives the day's summary; children who still owe
quests get their open count called out on that line, and children
who are not present today are omitted entirely. A child whose
remaining-count sensor is unavailable is reported with the
completion line only. By default the report is skipped when every
present child is already done — turn on send-even-when-clear to
always get the day's completion summary. The notify service is
chosen by the household when importing this blueprint — NestQuest
never hard-codes a personal notify target.
domain: automation
input:
report_time:
name: Report time
description: >-
Local time of day the report is sent — deliberately before the
midnight missed-quest sweep, so the report reads the same
day's data.
default: "20:00"
selector:
time: {}
notify_target:
name: Notify service
description: >-
The Home Assistant notify service to call for the report,
for example notify.mobile_app_your_phone — a placeholder;
pick the notify service for your own device.
selector:
text: {}
send_report:
name: Send the report
description: >-
Turn off to keep the automation installed but skip the
notification entirely.
default: true
selector:
boolean: {}
send_even_when_clear:
name: Send even when clear
description: >-
Send the report even when every present child has already
finished their quests today. When off (the default), an
all-clear day sends nothing.
default: false
selector:
boolean: {}
mode: single
variables:
send_report: !input send_report
send_even_when_clear: !input send_even_when_clear
triggers:
- trigger: time
at: !input report_time
conditions:
- condition: template
value_template: "{{ send_report | bool }}"
# Skip an all-clear day unless the household opted into
# send-even-when-clear: the guard is false only when every present
# child has a usable remaining count of zero. A MISSING or
# non-numeric remaining sensor reads as -1 and counts as open —
# a degraded state must reach the parent, not look like all-clear.
- condition: template
value_template: |
{%- set ns = namespace(any_open=false) -%}
{%- for due in states.sensor
| selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_due_today(_\\d+)?$') -%}
{%- if due.attributes.get('present') -%}
{%- set remaining_values = states.sensor
| selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_remaining_today(_\\d+)?$')
| selectattr('attributes.child_id', 'eq', due.attributes.child_id)
| map(attribute='state')
| list -%}
{%- if not remaining_values or (remaining_values | first | int(-1)) != 0 -%}
{%- set ns.any_open = true -%}
{%- endif -%}
{%- endif -%}
{%- endfor -%}
{{- send_even_when_clear | bool or ns.any_open -}}
actions:
- action: !input notify_target
data:
title: NestQuest end-of-day report
message: |
{%- set ns = namespace(lines=[]) -%}
{%- for due in states.sensor
| selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_due_today(_\\d+)?$') -%}
{%- if due.attributes.get('present') -%}
{%- set due_count = due.state | int -%}
{%- set completed_values = states.sensor
| selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_completed_today(_\\d+)?$')
| selectattr('attributes.child_id', 'eq', due.attributes.child_id)
| map(attribute='state')
| list -%}
{%- set remaining_values = states.sensor
| selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_remaining_today(_\\d+)?$')
| selectattr('attributes.child_id', 'eq', due.attributes.child_id)
| map(attribute='state')
| list -%}
{%- set completed_count = (completed_values | first | int) if completed_values else 0 -%}
{%- set remaining_count = (remaining_values | first | int) if remaining_values else none -%}
{%- if remaining_count is not none and remaining_count > 0 -%}
{%- set line = due.attributes.child_name ~ ': ' ~ remaining_count ~ ' still open (' ~ completed_count ~ '/' ~ due_count ~ ' done).' -%}
{%- else -%}
{%- set line = due.attributes.child_name ~ ': ' ~ completed_count ~ '/' ~ due_count ~ ' done.' -%}
{%- endif -%}
{%- set ns.lines = ns.lines + [line] -%}
{%- endif -%}
{%- endfor -%}
{{- ns.lines | join('\n') -}}
@@ -0,0 +1,78 @@
blueprint:
name: NestQuest morning summary
description: >-
Sends a morning notification listing what each child still owes
today. Only children whose NestQuest presence sensor is on are
included; children who are not present today are omitted entirely.
The notify service is chosen by the household when importing this
blueprint — NestQuest never hard-codes a personal notify target.
domain: automation
input:
morning_time:
name: Morning time
description: Local time of day the summary is sent.
default: "08:00"
selector:
time: {}
notify_target:
name: Notify service
description: >-
The Home Assistant notify service to call for the summary,
for example notify.mobile_app_your_phone — a placeholder;
pick the notify service for your own device.
selector:
text: {}
send_summary:
name: Send the summary
description: >-
Turn off to keep the automation installed but skip the
notification entirely.
default: true
selector:
boolean: {}
mode: single
variables:
send_summary: !input send_summary
triggers:
- trigger: time
at: !input morning_time
conditions:
- condition: template
value_template: "{{ send_summary | bool }}"
actions:
- action: !input notify_target
data:
title: NestQuest morning summary
message: |
{%- set ns = namespace(lines=[]) -%}
{%- for due in states.sensor
| selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_due_today(_\\d+)?$') -%}
{%- if due.attributes.get('present') -%}
{%- set remaining_values = states.sensor
| selectattr('entity_id', 'search', '^sensor\\.nestquest_.*_quests_remaining_today(_\\d+)?$')
| selectattr('attributes.child_id', 'eq', due.attributes.child_id)
| map(attribute='state')
| list -%}
{%- if remaining_values -%}
{%- set remaining = remaining_values | first | int -%}
{%- if remaining > 0 -%}
{%- set line = due.attributes.child_name ~ ' owes ' ~ remaining ~ ' quest(s) today.' -%}
{%- else -%}
{%- set line = due.attributes.child_name ~ ' is all clear today (0 quests remaining).' -%}
{%- endif -%}
{%- else -%}
{%- set line = due.attributes.child_name ~ "'s quest count is unavailable today." -%}
{%- endif -%}
{%- set ns.lines = ns.lines + [line] -%}
{%- endif -%}
{%- endfor -%}
{%- if ns.lines -%}
{{- ns.lines | join('\n') -}}
{%- else -%}
No children present today.
{%- endif -%}
@@ -0,0 +1,96 @@
blueprint:
name: NestQuest UniFi quest access
description: >-
One automation per child. Completely block the selected UniFi clients
after the local weekday/weekend cutoff while that child has quests
remaining TODAY. Restore network access immediately when the count
reaches zero, and at midnight even if yesterday was incomplete.
A missing or unavailable quest sensor is treated as still open after
the cutoff; verify the sensor before enabling this automation. The
selected UniFi client switches are ON when access is allowed and
OFF when blocked. Do not select infrastructure or adult devices.
domain: automation
input:
remaining_sensor:
name: Child's quests remaining today
description: Select this child's NestQuest quests remaining today sensor.
selector:
entity:
filter:
- integration: nestquest
domain: sensor
client_switches:
name: Child's UniFi client access switches
description: >-
Select every UniFi client access switch for this child. These
switches are ON when network access is allowed and OFF when blocked.
selector:
entity:
multiple: true
filter:
- integration: unifi
domain: switch
weekday_cutoff:
name: Monday-Friday cutoff
default: "18:25:00"
selector:
time: {}
weekend_cutoff:
name: Saturday-Sunday cutoff
default: "14:00:00"
selector:
time: {}
mode: restart
variables:
remaining_sensor: !input remaining_sensor
client_switches: !input client_switches
weekday_cutoff: !input weekday_cutoff
weekend_cutoff: !input weekend_cutoff
triggers:
- trigger: state
entity_id: !input remaining_sensor
- trigger: time
at: !input weekday_cutoff
- trigger: time
at: !input weekend_cutoff
- trigger: time
at: "00:00:00"
- trigger: homeassistant
event: start
# Reconcile after missed events or a temporary UniFi outage.
- trigger: time_pattern
minutes: "/5"
conditions:
- condition: template
value_template: "{{ client_switches | count > 0 }}"
actions:
- variables:
should_block: >-
{{ now().strftime('%H:%M') >=
(weekday_cutoff[:5] if now().weekday() < 5 else weekend_cutoff[:5])
and (states(remaining_sensor) | int(-1)) != 0 }}
- repeat:
for_each: "{{ client_switches }}"
sequence:
- choose:
- conditions:
- condition: template
value_template: >-
{{ should_block | bool and is_state(repeat.item, 'on') }}
sequence:
- action: switch.turn_off
target:
entity_id: "{{ repeat.item }}"
- conditions:
- condition: template
value_template: >-
{{ not (should_block | bool) and is_state(repeat.item, 'off') }}
sequence:
- action: switch.turn_on
target:
entity_id: "{{ repeat.item }}"
+3 -1
View File
@@ -27,7 +27,9 @@ frontend:
lovelace:
dashboards: !include dashboards/dashboards.yaml
automation: !include_dir_merge_list automations/
automation manual: !include_dir_merge_list automations/
automation ui: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
input_boolean: !include helpers/input_boolean.yaml
input_number: !include helpers/input_number.yaml
File diff suppressed because it is too large Load Diff
+219
View File
@@ -0,0 +1,219 @@
# Bambu and Elegoo fleet display
ESPHome/LVGL monitor for the **Sunton ESP32-8048S043C**, with an 800×480 landscape RGB
display, GT911 capacitive touch, 16 MB flash and 8 MB octal PSRAM. The hardware pins
and LCD timings follow the [ESPHome device reference](https://devices.esphome.io/devices/sunton-esp32-8048s043c/).
This configuration is for the **C** capacitive-touch version.
Inspired by [BambuHelper](https://github.com/Keralots/BambuHelper), this implementation
subscribes to the existing Home Assistant Bambu Lab and Elegoo Printer entities over
a single encrypted ESPHome API connection. Home Assistant handles the printer connections. No Bambu
access codes, cloud credentials or additional Home Assistant automations are needed.
Home Assistant must be running and its printer integrations must have current printer data.
## Pages and navigation
![LVGL preview with synthetic printer telemetry](previews/prints_charming.png)
The configured swipe order is:
1. Print Status Overview — one steam gauge per printer. The filled part of
each ring is that printer's print progress, drawn in the active filament
color. Black filament keeps a grey ring behind the gauge so it stays visible
on the black page. The center shows the percent complete in white, with the
printer name and estimated finish time underneath.
The local time is in the top-right corner. Tap a gauge to open that printer.
2. PrintsCharming — P1S, Office, both four-slot AMS units.
3. Polly-Pocket — A1 mini, Office, external spool.
4. Ds-Turd-Flinger — A1, Upstairs, four-slot AMS.
5. Jordyns-Layer-Slayer — A1, Upstairs, four-slot AMS.
6. Constipation-Orion — Office, left/right nozzles and both four-slot AMS units.
AMS A uses `constipation_orion_ams_1`; AMS B retains its original
`h2c_31b8ap5a2200288_ams_2` entity prefix.
7. Centauri Carbon — Elegoo FDM, Office, nozzle/bed current and target temperatures
and model/auxiliary/enclosure fan speeds.
8. Saturn — Elegoo resin, Garage, progress, UV LED temperature, current/total layers,
elapsed time, remaining time and estimated finish.
![Saturn resin page with synthetic telemetry](previews/saturn.png)
Swipe left for the next page or right for the previous page. Navigation wraps
from the last printer back to the fleet page. Previous/Next buttons provide the
same navigation. Swipes propagate through the fleet gauges and filament
slots. A swipe consumes the rest of that touch so dragging across a gauge or a
navigation button does not also click it.
FDM pages show print status, job name, progress, nozzle and bed current/target
temperatures, remaining time, estimated finish time, current/total layers, print
stage, fan speeds and filament colors/types. The layout follows BambuHelper's
classic gauges: black background, bright green accents, six arc gauges and a
vertical AMS column. Nozzle arcs are orange and the bed arc is cyan. For dual-nozzle
printers the auxiliary-fan position becomes the right-nozzle gauge. The active
filament slot has a green outline. Print errors use red; paused prints use amber.
**HMS means Health Management System.** Its binary sensor means the Bambu integration
has one or more printer health notifications, which can include information or
maintenance messages as well as faults. Tap the status in the upper-right corner
or the scrolling message above the footer to open the health-details panel. It
shows the codes, severities and descriptions reported by Home Assistant, for up to
three current HMS notices, plus print-error details. Scroll vertically for long
messages and tap Close to return. If more than three notices exist, the panel says
how many additional notices are available in Home Assistant.
Elegoo pages use the same status-tap details panel for `print_error` and
`error_status_reason` enum values. `none` and `ok` are normal states; unknown or
unavailable errors do not imply a fault. Elegoo pages use `sdcp_status` for printer
connectivity and do not subscribe to Bambu HMS or AMS entities. The Saturn page
omits nozzle, bed, fan and filament widgets.
The status uses **HEALTH INFO** (cyan) for information-only notifications,
**HEALTH NOTICE** (amber) for common/unknown severities or incomplete details, and
**HEALTH ERROR** (red) for fatal/serious severities. Severity comes from the Bambu
integration's `N-Severity` attributes. Missing or unloaded messages never establish
an information-only status. The display does not dismiss or clear printer alerts.
The [`hms_errors` sensor attributes](https://github.com/greghesp/ha-bambulab/blob/main/custom_components/bambu_lab/pybambu/models.py)
provide `Count`, `1-Code`, `1-Error`, `1-Severity`, and the corresponding numbered
fields for subsequent notices. Stale attributes are ignored when the sensor is off.
When Home Assistant disconnects or a printer is offline, its live metrics are cleared
to `--` rather than continuing to show cached values as live. Unknown individual
values also show `--`. Remaining time respects the HA sensor's `h`, `min`, `s` or `ms`
unit, and temperature labels use the imported HA temperature unit. Only the visible
page is refreshed each second; every printer's subscriptions remain connected.
Auxiliary and exhaust fans are imported only for printers with those sensors;
unsupported fan readings show `--`. Finish time is estimated from HA's remaining
time and the HA-synchronized clock. Set `time.timezone` in the main YAML if your
timezone differs from `America/New_York`.
This version provides monitoring and local page navigation. Camera feeds, cover
images, printer pause/stop controls, power switching, cloud login and automatic
printer rotation are outside its current scope.
## Install
1. Keep both device files and the entire `bambu-display/` folder together in the
ESPHome configuration directory. `bambu-printer-display.yaml` is the first board
and `bambu-printer-display-2.yaml` is the second. They share `board.yaml` and the
generated fleet package, which is checked in, so the ESPHome dashboard can compile
either file without running the generator first.
2. Supply the existing `api_encryption_key`, `wifi_iot_ssid` and `wifi_password`
entries in that directory's `secrets.yaml`.
3. Use ESPHome **2026.9.0 or newer**. Compile and install the matching device file
over that board's USB-to-UART bridge for the first flash; subsequent installs can
use encrypted OTA. Flash `bambu-printer-display.yaml` to the first panel and
`bambu-printer-display-2.yaml` to the second. Each file has its own ESPHome name,
so both panels can be online together.
4. Add **Bambu Printer Display** or **Bambu Printer Display 2** through Home
Assistant's ESPHome integration using the same API encryption key. Each display
only subscribes to entities, so it does not require permission to perform Home
Assistant actions.
5. Confirm all pages receive data and verify touch direction and color order on the
physical panel. Entity IDs match the repository's `ha-entities.txt` snapshot;
renamed or disabled entities in live HA must be corrected/enabled there.
The USB D+/D− GPIO19/20 pins are used by the board's touch bus. Logging therefore
uses UART0. ESPHome may still warn about these two pins; they are intentional for
this board. If GT911 is not found at `0x5D`, inspect the I²C scan and use its reported
address (some hardware revisions select `0x14`). Backlight is GPIO2; RGB data pins
and timings are explicit in `board.yaml` rather than borrowed from a 5″ board.
The display and LVGL both use **big-endian RGB565**. With `mipi_rgb`'s named
`red`/`green`/`blue` pin groups, its generated bus pin map already exchanges the
two bytes. Selecting little-endian pixels corrupts colors on the physical panel
even though a host preview can look correct. Keep both byte-order settings aligned
with the bus mapping; changing the RGB/BGR label alone is insufficient for this
RPI panel's direct framebuffer path.
To check the physical colors, open the device's **Display Color Test** diagnostic
button in Home Assistant. It shows labeled red, green, blue, white, cyan and magenta
blocks. Swipe to return to the printers. This test page is skipped during normal
printer navigation. Its expected image is [previews/color_test.png](previews/color_test.png).
CLI checks from the repository root:
```sh
esphome config esphome/bambu-printer-display.yaml
esphome config esphome/bambu-printer-display-2.yaml
esphome compile esphome/bambu-printer-display.yaml
esphome compile esphome/bambu-printer-display-2.yaml
```
## Change or expand the fleet
Edit **`printers.yaml`**, then regenerate and flash the display:
```sh
python esphome/bambu-display/generate.py
python esphome/bambu-display/generate.py --check
```
The generator requires PyYAML (already included in ESPHome's Python environment).
Do not hand-edit `generated.yaml`. There is no two-printer limit in the generator;
adding a printer creates its own subscriptions, widgets, swipe page and fleet gauge. The practical
limit depends on the board's available RAM and flash, and adding printers requires
regenerating and installing the firmware. Seven printers are configured; generation
is also tested with nine. Each Bambu printer's filament strip supports up to eight slots.
```yaml
- id: another_printer
name: Another Printer
subtitle: Workshop
entity_prefix: my_bambu_printer
entities:
progress: sensor.renamed_progress
time_left: sensor.renamed_remaining_time
filament_banks:
- label: A
entity_prefix: my_bambu_printer_ams_1
```
For a dual-nozzle printer add `dual_nozzle: true`. For external filament add
`external_spools: [{label: EXT, entity: sensor.my_external_spool}]`. Per-metric
`entities` overrides use the keys in `NUMERIC`, `DUAL`, `TEXT` and `BINARY` inside
`generate.py`. Use `extra_fans: [aux_fan, exhaust_fan]` only where those HA entities
exist; their mappings are in `EXTRA_FANS`. A remaining-time override also updates its unit subscription.
Filament entries use the Bambu integration's `type`, `color` and `active` attributes.
For Elegoo printers, set `profile: elegoo_fdm` or `profile: elegoo_resin` (the default
profile is `bambu`). These profiles map progress to `percent_complete`. The Centauri Carbon leaves
that sensor at 0 for most of a job, so an active Elegoo print falls back to the
layer count and then to elapsed time divided by `total_print_time`. Job name maps to
`file_name`, remaining time to `remaining_print_time`, and total layers to
`total_layers`. The FDM profile reads targets from the HA `number` entities
`target_nozzle_temp` and `target_bed_temp`; it only monitors their state. Per-metric
`entities` overrides support `sensor`, `binary_sensor` and `number` domains.
Sensor types, duration units and enum states follow the
[Elegoo integration definitions](https://github.com/danielcherubini/elegoo-homeassistant/blob/main/custom_components/elegoo_printer/definitions.py).
The configured Elegoo prefixes are `centauri_carbon` and `saturn`, matching the
repository entity snapshot. Some older automations refer to `saturn_iv`; if your
live integration still uses that prefix, change the Saturn `entity_prefix` before
regenerating. If individual entities have different names in your integration
version, use `entities` overrides, for example:
```yaml
entities:
print_error_reason: sensor.saturn_current_print_error_status_reason
```
No new HA helpers are required. `dual_nozzle`, filament slots and `extra_fans` are
Bambu options; the Elegoo FDM profile includes its three fan subscriptions.
## Development checks and screenshots
```sh
python -m unittest discover -s esphome/bambu-display -v
clang++ -std=c++17 -Wall -Wextra -Werror esphome/bambu-display/test_dashboard.cpp -o /tmp/bambu-dashboard-test
/tmp/bambu-dashboard-test
python esphome/bambu-display/preview.py
```
`preview.py` compiles the same generated LVGL widgets for ESPHome's SDL host with
clearly synthetic telemetry, then captures all seven pages and a synthetic health
notice into `previews/`. The demo notice is not a diagnosis of any real printer. It needs
ESPHome, Pillow, a C++ compiler and SDL2 (`sdl2-config` on PATH). It runs headlessly,
without connecting to Home Assistant or any printer. It also injects LVGL pointer
samples to check left/right swipes, page wraparound and consumption of a swipe that
starts on a navigation button. These are software layout
previews; physical panel timings, touch and Wi-Fi still need a hardware check.
The preview also checks Elegoo telemetry, error text and clearing of offline data.
+114
View File
@@ -0,0 +1,114 @@
# Shared Sunton ESP32-8048S043C board for every fleet display.
# Device name and friendly name come from the top-level YAML substitutions.
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
min_version: 2026.9.0
includes:
- bambu-display/dashboard.h
esp32:
board: esp32-s3-devkitc-1
flash_size: 16MB
flash_mode: dio
cpu_frequency: 240MHz
framework:
type: esp-idf
sdkconfig_options:
CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y"
CONFIG_SPIRAM_RODATA: "y"
psram:
mode: octal
speed: 80MHz
logger:
hardware_uart: UART0 # GPIO19/20 are the touch bus, not native USB.
api:
id: ha_api
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
encryption:
key: !secret api_encryption_key
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
i2c:
id: touch_bus
sda: GPIO19
scl: GPIO20
scan: true
display:
- platform: mipi_rgb
id: panel_display
model: RPI
dimensions: 800x480
color_order: RGB
# Named RGB data_pins are byte-swapped by mipi_rgb's pin mapping.
# Send big-endian RGB565 pixels to match that wiring transformation.
byte_order: big_endian
de_pin: GPIO40
hsync_pin: GPIO39
vsync_pin: GPIO41
pclk_pin: GPIO42
pclk_inverted: true
pclk_frequency: 16MHz
hsync_front_porch: 8
hsync_pulse_width: 4
hsync_back_porch: 8
vsync_front_porch: 8
vsync_pulse_width: 4
vsync_back_porch: 8
data_pins:
red: [GPIO45, GPIO48, GPIO47, GPIO21, GPIO14]
green: [GPIO5, GPIO6, GPIO7, GPIO15, GPIO16, GPIO4]
blue: [GPIO8, GPIO3, GPIO46, GPIO9, GPIO1]
auto_clear_enabled: false
update_interval: never
touchscreen:
- platform: gt911
id: panel_touch
display: panel_display
i2c_id: touch_bus
address: 0x5D
# Polling avoids the GT911 INT pin's boot/address-selection behaviour.
update_interval: 20ms
output:
- platform: ledc
id: backlight_output
pin: GPIO2
light:
- platform: monochromatic
id: panel_backlight
name: Backlight
output: backlight_output
restore_mode: ALWAYS_ON
default_transition_length: 0s
time:
- platform: homeassistant
id: ha_time
timezone: America/New_York
lvgl:
id: dashboard
displays: [panel_display]
touchscreens: [panel_touch]
buffer_size: 12%
color_depth: 16
byte_order: big_endian # Must match mipi_rgb's named RGB pin mapping above.
bg_color: 0x000000
text_color: 0xEEEEEE
default_font: montserrat_18
+438
View File
@@ -0,0 +1,438 @@
#pragma once
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdint>
#include <cstdlib>
#include <string>
#include <utility>
#include <vector>
namespace bambu_display {
constexpr uint32_t GREEN = 0x00FF00;
constexpr uint32_t MUTED = 0xAEB3B7;
constexpr uint32_t RED = 0xFF737B;
constexpr uint32_t AMBER = 0xF2C572;
constexpr uint32_t TRACK = 0x19232B;
constexpr uint32_t RING_BACKING = 0x6E767E;
inline std::string lower(std::string text) {
for (char &c : text) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return text;
}
inline bool valid_text(const std::string &text) {
const auto value = lower(text);
return !value.empty() && value != "unknown" && value != "unavailable" && value != "none";
}
inline bool is_on(const std::string &text) {
const auto value = lower(text);
return value == "on" || value == "true" || value == "1";
}
inline bool elegoo_fault(const std::string &text) {
const auto value = lower(text);
return valid_text(value) && value != "ok" && value != "0" && value != "off" && value != "false";
}
inline std::string humanize(std::string text) {
if (!valid_text(text)) return "--";
std::replace(text.begin(), text.end(), '_', ' ');
if (!text.empty()) text[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(text[0])));
return text;
}
inline int gauge_value(float value, int maximum) {
return std::isfinite(value) ? static_cast<int>(std::clamp(value, 0.0f, float(maximum))) : 0;
}
inline std::string number(float value) {
if (!std::isfinite(value) || value < 0 || value > 1000000) return "--";
char buffer[24];
snprintf(buffer, sizeof(buffer), "%.0f", value);
return buffer;
}
inline std::string temperature(float current, float target, const std::string &unit) {
return number(current) + " / " + number(target) + (valid_text(unit) ? " " + unit : "");
}
inline std::string remaining(float value, const std::string &unit) {
if (!std::isfinite(value) || value < 0) return "--";
double minutes;
if (unit == "h") minutes = double(value) * 60;
else if (unit == "min") minutes = value;
else if (unit == "s") minutes = double(value) / 60;
else if (unit == "ms") minutes = double(value) / 60000;
else return "--"; // Never silently interpret hours as minutes.
if (minutes > 1000000) return "--";
const auto rounded = static_cast<int>(std::ceil(minutes - 0.001));
char buffer[32];
snprintf(buffer, sizeof(buffer), "%dh %02dm", rounded / 60, rounded % 60);
return buffer;
}
inline int duration_seconds(float value, const std::string &unit) {
if (!std::isfinite(value) || value < 0) return -1;
double seconds;
if (unit == "h") seconds = double(value) * 3600;
else if (unit == "min") seconds = double(value) * 60;
else if (unit == "s") seconds = value;
else if (unit == "ms") seconds = double(value) / 1000;
else return -1;
return seconds <= 60000000 ? static_cast<int>(std::round(seconds)) : -1;
}
inline float celsius(float value, const std::string &unit) {
return unit == "°F" || unit == "F" ? (value - 32.0f) * 5.0f / 9.0f : value;
}
inline uint32_t filament_color(std::string value) {
if (!value.empty() && value[0] == '#') value.erase(0, 1);
// Bambu publishes RGB or RGBA, with alpha at the end.
if (value.size() != 6 && value.size() != 8) return TRACK;
uint32_t rgb = 0;
for (size_t i = 0; i < value.size(); ++i) {
const char c = static_cast<char>(std::tolower(static_cast<unsigned char>(value[i])));
const int digit = c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1;
if (digit < 0) return TRACK;
if (i < 6) rgb = (rgb << 4) | digit;
}
return rgb;
}
// A black ring disappears on the black page. Anything this dark needs a grey arc behind it.
inline bool filament_is_black(uint32_t color) {
return ((color >> 16) & 0xFF) <= 0x28 && ((color >> 8) & 0xFF) <= 0x28 && (color & 0xFF) <= 0x28;
}
struct Tray {
std::string label, type, color, active;
};
struct HealthNotice { std::string code, message, severity; };
struct Snapshot {
bool connected = false;
std::string online, status, task, stage, print_error, hms_error;
std::string hms_count, print_error_message, print_error_code;
std::string print_error_reason, uv_unit, elapsed_unit;
bool elegoo = false, resin = false;
float uv = NAN, elapsed = NAN, total_time = NAN;
std::vector<HealthNotice> health_notices;
std::string remaining_unit, nozzle_unit, bed_unit, total_time_unit;
std::string finish_time;
float progress = NAN, nozzle = NAN, nozzle_target = NAN, bed = NAN, bed_target = NAN;
float time_left = NAN, layer = NAN, total_layers = NAN, fan = NAN;
float aux_fan = NAN, exhaust_fan = NAN;
bool dual_nozzle = false;
float left_nozzle = NAN, left_target = NAN, right_nozzle = NAN, right_target = NAN;
std::vector<Tray> trays;
bool live() const { return connected && is_on(online) && valid_text(status); }
// Centauri firmware parks on milestone names such as preheating_completed for
// most of a job, so any status that is not a known idle or finished state is active.
bool job_active() const {
auto idle = [](const std::string &value) {
return value.empty() || value == "unknown" || value == "unavailable" || value == "none" ||
value == "idle" || value == "standby" || value == "ready" || value == "complete" ||
value == "completed" || value == "finish" || value == "finished" || value == "cancelled" ||
value == "canceled" || value == "stopped" || value == "stop" || value == "failed";
};
const auto state = lower(status);
const auto machine = lower(stage);
if (machine == "printing" || machine == "paused" || machine == "busy" || machine == "heating" ||
machine == "preheating" || machine == "running")
return true;
return !idle(state);
}
// The Centauri reports percent_complete as 0 while CurrentLayer stays 0.
// Layers, then elapsed time against the total, still describe the job.
float completion() const {
if (!elegoo) return progress;
if (std::isfinite(progress) && progress > 0.0f) return std::clamp(progress, 0.0f, 100.0f);
if (!job_active()) return std::isfinite(progress) ? 0.0f : NAN;
if (std::isfinite(layer) && std::isfinite(total_layers) && layer > 0.0f && total_layers > 0.0f)
return std::clamp(100.0f * layer / total_layers, 0.0f, 100.0f);
const int elapsed_s = duration_seconds(elapsed, elapsed_unit);
const int total_s = duration_seconds(total_time, total_time_unit);
if (elapsed_s > 0 && total_s > 0)
return std::clamp(100.0f * static_cast<float>(elapsed_s) / static_cast<float>(total_s), 0.0f, 100.0f);
return std::isfinite(progress) ? 0.0f : NAN;
}
bool has_print_error() const {
if (!elegoo) return is_on(print_error);
return elegoo_fault(print_error) || elegoo_fault(print_error_reason) || lower(status) == "error";
}
std::string error_message() const {
if (!elegoo) return valid_text(print_error_message) ? print_error_message : "Print error reported; tap status for details";
std::string message;
for (const auto &text : {print_error, print_error_reason}) {
if (!elegoo_fault(text)) continue;
if (!message.empty()) message += " / ";
message += humanize(text);
}
return message.empty() ? "Print error reported; tap status for details" : message;
}
int notice_count() const {
int loaded = 0;
for (const auto &notice : health_notices)
if (valid_text(notice.code) || valid_text(notice.message)) ++loaded;
char *end;
const long count = std::strtol(hms_count.c_str(), &end, 10);
return end != hms_count.c_str() && *end == '\0' && count >= 0 && count < 10000
? std::max(loaded, static_cast<int>(count)) : loaded;
}
int health_severity_rank() const {
int rank = 1, loaded = 0;
for (const auto &notice : health_notices) {
if (!valid_text(notice.code) && !valid_text(notice.message)) continue;
++loaded;
const auto severity = lower(notice.severity);
rank = std::max(rank, severity == "fatal" || severity == "serious" ? 3 : severity == "info" ? 1 : 2);
}
// Missing/unloaded messages cannot establish an info-only notification.
return loaded == 0 || notice_count() > loaded ? std::max(rank, 2) : rank;
}
std::string alert_summary() const {
if (has_print_error()) return error_message();
if (is_on(hms_error)) {
for (const auto &notice : health_notices)
if (valid_text(notice.message)) return notice.message;
return "Printer health notification; tap status for details";
}
return "";
}
std::string health_details() const {
if (!live()) return "Printer health data is unavailable. Waiting for a live connection.";
std::string details;
if (has_print_error()) {
details = "PRINT ERROR\n";
if (valid_text(print_error_code)) details += print_error_code + "\n";
details += error_message();
details += "\n\n";
}
if (is_on(hms_error)) {
details += "HMS = Health Management System\n";
int loaded = 0;
for (const auto &notice : health_notices) {
if (!valid_text(notice.code) && !valid_text(notice.message)) continue;
details += "\nNotice " + std::to_string(++loaded);
if (valid_text(notice.severity)) details += " / " + notice.severity;
details += "\n";
if (valid_text(notice.code)) details += notice.code + "\n";
details += valid_text(notice.message) ? notice.message : "Message not supplied by Home Assistant.";
details += "\n";
}
if (loaded == 0) details += "\nNotification is active; details have not arrived from Home Assistant.\n";
if (notice_count() > loaded)
details += "\n" + std::to_string(notice_count() - loaded) + " more notification(s). View all in Home Assistant.\n";
}
return details.empty() ? "No current printer health notifications." : details;
}
std::string status_text() const {
if (!connected) return "HA OFFLINE";
if (lower(online) == "off" || lower(online) == "false") return "PRINTER OFFLINE";
if (!live()) return "SYNCING";
if (has_print_error()) return "PRINT ERROR";
if (is_on(hms_error))
return health_severity_rank() >= 3 ? "HEALTH ERROR" : health_severity_rank() == 1 ? "HEALTH INFO" : "HEALTH NOTICE";
const auto state = lower(status);
if (state == "running" || state == "printing") return "PRINTING";
if (state == "pause" || state == "paused") return "PAUSED";
if (state == "finish" || state == "finished" || state == "complete" || state == "completed") return "COMPLETE";
auto text = humanize(status);
for (char &c : text) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
return text;
}
std::string overview_task() const {
if (!live()) return connected ? "Waiting for printer data" : "Waiting for Home Assistant";
return valid_text(task) ? task : "No active print";
}
std::string overview_meta() const {
if (!live()) return "--";
const float percent = completion();
const std::string progress_text = std::isfinite(percent)
? number(std::clamp(percent, 0.0f, 100.0f)) + "%" : "--";
const std::string time_line = remaining(time_left, remaining_unit);
if (!finish_time.empty() && time_line != "--")
return progress_text + " " + finish_time + " " + time_line;
return progress_text + " " + time_line;
}
// Active tray is the spool the printer loaded most recently. Without one, keep green.
uint32_t active_filament_color() const {
for (const auto &tray : trays)
if (is_on(tray.active)) {
const uint32_t color = filament_color(tray.color);
if (color != TRACK) return color;
}
return GREEN;
}
uint32_t status_color() const {
if (!live()) return MUTED;
const auto state = lower(status);
if (has_print_error() || state == "failed") return RED;
if (is_on(hms_error))
return health_severity_rank() >= 3 ? RED : health_severity_rank() == 1 ? 0x00E5E5 : AMBER;
if (state == "pause" || state == "paused" || state == "pausing") return AMBER;
return GREEN;
}
};
struct FleetCounts {
int attention = 0, printing = 0, paused = 0, idle = 0, offline = 0, syncing = 0;
};
inline void tally(FleetCounts &counts, const Snapshot &data) {
if (!data.connected || lower(data.online) == "off" || lower(data.online) == "false") {
++counts.offline;
return;
}
if (!data.live()) {
++counts.syncing;
return;
}
if (data.has_print_error() || is_on(data.hms_error) || lower(data.status) == "failed") {
++counts.attention;
return;
}
const auto state = lower(data.status);
if (state == "running" || state == "printing") ++counts.printing;
else if (state == "pause" || state == "paused" || state == "pausing") ++counts.paused;
else ++counts.idle;
}
inline std::string fleet_summary(const FleetCounts &counts) {
const std::pair<int, const char *> items[] = {
{counts.attention, "attention"}, {counts.printing, "printing"}, {counts.paused, "paused"},
{counts.idle, "idle"}, {counts.offline, "offline"}, {counts.syncing, "syncing"}};
std::string text;
for (const auto &item : items) {
if (!item.first) continue;
if (!text.empty()) text += ", ";
text += std::to_string(item.first) + " " + item.second;
}
return text.empty() ? "Syncing" : text;
}
} // namespace bambu_display
#ifdef USE_LVGL
#include <lvgl.h>
namespace bambu_display {
struct TrayWidgets { lv_obj_t *card, *swatch, *label; };
struct GaugeWidgets { lv_obj_t *arc, *value, *detail; };
struct Widgets {
lv_obj_t *status, *task, *top_progress, *remaining, *layers, *stage, *health_details;
GaugeWidgets progress, nozzle, bed, fan, aux, exhaust;
std::vector<TrayWidgets> trays;
GaugeWidgets uv{}, layer{};
lv_obj_t *elapsed = nullptr;
};
struct OverviewWidgets {
lv_obj_t *arc, *value, *finish, *backing;
};
inline void set_text(lv_obj_t *label, const std::string &text) {
if (text != lv_label_get_text(label)) lv_label_set_text(label, text.c_str());
}
inline void render_gauge(const GaugeWidgets &widgets, float value, float position,
int maximum, const std::string &detail) {
lv_arc_set_value(widgets.arc, gauge_value(position, maximum));
set_text(widgets.value, number(value));
set_text(widgets.detail, detail);
}
inline void render(const Snapshot &data, const Widgets &widgets) {
const bool live = data.live();
const uint32_t color = data.status_color();
set_text(widgets.status, data.status_text());
lv_obj_set_style_text_color(widgets.status, lv_color_hex(color), 0);
lv_obj_set_style_arc_color(widgets.progress.arc, lv_color_hex(color), LV_PART_INDICATOR);
const float percent = data.completion();
lv_bar_set_value(widgets.top_progress, live ? gauge_value(percent, 100) : 0, LV_ANIM_OFF);
render_gauge(widgets.progress, live && std::isfinite(percent)
? std::clamp(percent, 0.0f, 100.0f) : NAN, live ? percent : NAN, 100,
live ? remaining(data.time_left, data.remaining_unit) : "--");
set_text(widgets.task, live ? (valid_text(data.task) ? data.task : "No active print")
: data.connected ? "Waiting for printer data" : "Waiting for Home Assistant");
if (data.resin) {
render_gauge(widgets.uv, live ? data.uv : NAN,
live ? celsius(data.uv, data.uv_unit) : NAN, 100,
live && valid_text(data.uv_unit) ? data.uv_unit : "--");
const float layer_percent = std::isfinite(data.layer) && std::isfinite(data.total_layers)
&& data.layer >= 0 && data.total_layers > 0 ? 100 * data.layer / data.total_layers : NAN;
render_gauge(widgets.layer, live ? data.layer : NAN, live ? layer_percent : NAN, 100,
live ? "/ " + number(data.total_layers) : "--");
set_text(widgets.elapsed, "Elapsed: " + (live ? remaining(data.elapsed, data.elapsed_unit) : "--"));
} else {
const float nozzle = data.dual_nozzle ? data.left_nozzle : data.nozzle;
const float target = data.dual_nozzle ? data.left_target : data.nozzle_target;
render_gauge(widgets.nozzle, live ? nozzle : NAN, live ? celsius(nozzle, data.nozzle_unit) : NAN,
300, live ? "/ " + number(target) + " " + data.nozzle_unit : "--");
render_gauge(widgets.bed, live ? data.bed : NAN, live ? celsius(data.bed, data.bed_unit) : NAN,
120, live ? "/ " + number(data.bed_target) + " " + data.bed_unit : "--");
render_gauge(widgets.fan, live ? data.fan : NAN, live ? data.fan : NAN, 100, "%");
if (data.dual_nozzle)
render_gauge(widgets.aux, live ? data.right_nozzle : NAN,
live ? celsius(data.right_nozzle, data.nozzle_unit) : NAN,
300, live ? "/ " + number(data.right_target) + " " + data.nozzle_unit : "--");
else
render_gauge(widgets.aux, live ? data.aux_fan : NAN, live ? data.aux_fan : NAN, 100, "%");
render_gauge(widgets.exhaust, live ? data.exhaust_fan : NAN, live ? data.exhaust_fan : NAN, 100, "%");
}
std::string time_line = live ? remaining(data.time_left, data.remaining_unit) : "--";
if (live && !data.finish_time.empty()) time_line = data.finish_time + " | " + time_line;
set_text(widgets.remaining, time_line);
set_text(widgets.layers, live ? "Layer " + number(data.layer) + " / " + number(data.total_layers) : "--");
std::string stage = live ? humanize(data.stage) : "Telemetry unavailable";
if (live && (data.has_print_error() || is_on(data.hms_error))) stage = data.alert_summary();
set_text(widgets.stage, stage);
lv_obj_set_style_text_color(widgets.stage, lv_color_hex(live && !data.alert_summary().empty() ? color : MUTED), 0);
set_text(widgets.health_details, data.health_details());
for (size_t i = 0; i < std::min(data.trays.size(), widgets.trays.size()); ++i) {
const auto &tray = data.trays[i];
const auto &widget = widgets.trays[i];
set_text(widget.label, tray.label + "\n" + (live && valid_text(tray.type) ? tray.type : "--"));
lv_obj_set_style_bg_color(widget.swatch, lv_color_hex(live ? filament_color(tray.color) : TRACK), 0);
lv_obj_set_style_border_color(widget.card, lv_color_hex(live && is_on(tray.active) ? GREEN : TRACK), 0);
}
}
inline void render_overview(const Snapshot &data, const OverviewWidgets &widgets) {
const bool live = data.live();
const uint32_t filament = data.active_filament_color();
const float percent = data.completion();
const float shown = live && std::isfinite(percent)
? std::clamp(percent, 0.0f, 100.0f) : NAN;
const bool black = filament_is_black(filament);
lv_obj_set_style_arc_color(widgets.arc, lv_color_hex(black ? RING_BACKING : TRACK), LV_PART_MAIN);
lv_obj_set_style_arc_color(widgets.arc, lv_color_hex(filament), LV_PART_INDICATOR);
lv_arc_set_value(widgets.arc, live ? gauge_value(percent, 100) : 0);
if (black) {
lv_obj_set_style_arc_color(widgets.backing, lv_color_hex(RING_BACKING), LV_PART_MAIN);
lv_obj_set_style_arc_color(widgets.backing, lv_color_hex(RING_BACKING), LV_PART_INDICATOR);
lv_obj_clear_flag(widgets.backing, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(widgets.backing, LV_OBJ_FLAG_HIDDEN);
}
set_text(widgets.value, number(shown));
lv_obj_set_style_text_color(widgets.value, lv_color_hex(0xFFFFFF), 0);
set_text(widgets.finish, live && !data.finish_time.empty() ? data.finish_time : "--");
}
inline void render_fleet_summary(lv_obj_t *label, const FleetCounts &counts) {
set_text(label, fleet_summary(counts));
const uint32_t color = counts.attention ? RED : counts.printing ? GREEN : MUTED;
lv_obj_set_style_text_color(label, lv_color_hex(color), 0);
}
} // namespace bambu_display
#endif
+485
View File
@@ -0,0 +1,485 @@
#!/usr/bin/env python3
"""Generate the ESPHome fleet package from printers.yaml. Requires PyYAML."""
import argparse
import json
from pathlib import Path
import re
import yaml
HERE = Path(__file__).resolve().parent
NUMERIC = {
'progress': 'print_progress', 'nozzle': 'nozzle_temperature',
'nozzle_target': 'nozzle_target_temperature', 'bed': 'bed_temperature',
'bed_target': 'bed_target_temperature', 'time_left': 'remaining_time',
'layer': 'current_layer', 'total_layers': 'total_layer_count',
'fan': 'cooling_fan_speed',
}
DUAL = {
'left_nozzle': 'left_nozzle_temperature', 'left_target': 'left_nozzle_target_temperature',
'right_nozzle': 'right_nozzle_temperature', 'right_target': 'right_nozzle_target_temperature',
}
EXTRA_FANS = {'aux_fan': 'aux_fan_speed', 'exhaust_fan': 'chamber_fan_speed'}
TEXT = {'status': 'print_status', 'task': 'task_name', 'stage': 'current_stage'}
BINARY = {'online': 'online', 'print_error': 'print_error', 'hms_error': 'hms_errors'}
ELEGOO_COMMON = {'progress': 'percent_complete', 'time_left': 'remaining_print_time',
'layer': 'current_layer', 'total_layers': 'total_layers',
'total_time': 'total_print_time'}
ELEGOO_FDM = {'nozzle': 'nozzle_temperature', 'bed': 'bed_temperature',
'elapsed': 'current_print_time',
'nozzle_target': ('number', 'target_nozzle_temp'),
'bed_target': ('number', 'target_bed_temp'), 'fan': 'model_fan_speed',
'aux_fan': 'auxiliary_fan_speed', 'exhaust_fan': 'enclosure_fan_speed'}
ELEGOO_RESIN = {'uv': 'uv_led_temp', 'elapsed': 'current_print_time'}
BG, CARD, TRACK, TEXT_COLOR, MUTED, GREEN = (
0x000000, 0x000000, 0x19232B, 0xEEEEEE, 0xAEB3B7, 0x00FF00)
RING_BACKING = 0x6E767E
ORANGE, CYAN = 0xFFAA00, 0x00E5E5
class Lambda(str):
"""A C++ lambda serialized with ESPHome's YAML tag."""
class Dumper(yaml.SafeDumper):
def ignore_aliases(self, data):
return True
Dumper.add_representer(Lambda, lambda dumper, text: dumper.represent_scalar('!lambda', text, style='|'))
def entity(printer, key, suffix, domain='sensor'):
return printer.get('entities', {}).get(key, f'{domain}.{printer["entity_prefix"]}_{suffix}')
def trays_for(printer):
result = []
for bank in printer.get('filament_banks', []):
for slot in range(1, 5):
result.append({'label': f'{bank["label"]}{slot}',
'entity': f'sensor.{bank["entity_prefix"]}_tray_{slot}'})
return result + printer.get('external_spools', [])
def validate_printers(printers):
if not isinstance(printers, list) or not printers:
raise ValueError('printers must be a nonempty list')
ids = set()
for printer in printers:
for key in ('id', 'name', 'subtitle', 'entity_prefix'):
if not isinstance(printer.get(key), str) or not printer[key]:
raise ValueError(f'Each printer needs a nonempty {key}')
pid = printer['id']
if not re.fullmatch(r'[a-z][a-z0-9_]*', pid) or pid in ids:
raise ValueError(f'Invalid or duplicate printer id: {pid}')
ids.add(pid)
profile = printer.get('profile', 'bambu')
if profile not in ('bambu', 'elegoo_fdm', 'elegoo_resin'):
raise ValueError(f'{pid}: unsupported profile: {profile}')
if profile != 'bambu' and (printer.get('dual_nozzle') or trays_for(printer) or printer.get('extra_fans')):
raise ValueError(f'{pid}: dual_nozzle, filament slots and extra_fans require the bambu profile')
if len(trays_for(printer)) > 8:
raise ValueError(f'{pid}: the filament strip fits up to eight slots')
if 'dual_nozzle' in printer and not isinstance(printer['dual_nozzle'], bool):
raise ValueError(f'{pid}: dual_nozzle must be true or false')
if any(key not in EXTRA_FANS for key in printer.get('extra_fans', [])):
raise ValueError(f'{pid}: extra_fans supports aux_fan and exhaust_fan')
for value in printer.get('entities', {}).values():
if not isinstance(value, str) or not re.fullmatch(r'(sensor|binary_sensor|number)\.[a-z0-9_]+', value):
raise ValueError(f'{pid}: invalid entity override: {value}')
def label(text, x, y, width, font=18, **kwargs):
return {'label': {'text': text, 'x': x, 'y': y, 'width': width,
'height': font + 8, 'text_color': TEXT_COLOR,
'text_font': f'montserrat_{font}', 'long_mode': 'DOT',
'gesture_bubble': True, **kwargs}}
def gauge(pid, key, title, x, y, color, maximum):
return [
{'arc': {'id': f'{pid}_{key}_arc', 'x': x + 8, 'y': y, 'width': 154, 'height': 154,
'min_value': 0, 'max_value': maximum, 'value': 0,
'start_angle': 135, 'end_angle': 45, 'arc_width': 13,
'arc_color': TRACK, 'indicator': {'arc_color': color, 'arc_width': 13},
'adjustable': False, 'gesture_bubble': True}},
label('--', x + 14, y + 47, 142, 32, id=f'{pid}_{key}_label', text_align='CENTER'),
label('--', x + 14, y + 86, 142, 16, id=f'{pid}_{key}_detail', text_align='CENTER', text_color=MUTED),
label(title, x, y + 137, 170, 18, text_align='CENTER', text_color=color),
]
def fdm_gauges(printer):
pid = printer['id']
widgets = gauge(pid, 'nozzle', 'Nozzle L' if printer.get('dual_nozzle') else 'Nozzle',
202, 62, ORANGE, 300)
widgets += gauge(pid, 'bed', 'Bed', 388, 62, CYAN, 120)
widgets += gauge(pid, 'fan', 'Part', 16, 229, 0xEEEEEE, 100)
widgets += gauge(pid, 'aux', 'Nozzle R' if printer.get('dual_nozzle') else 'Aux',
202, 229, ORANGE if printer.get('dual_nozzle') else 0xEEEEEE,
300 if printer.get('dual_nozzle') else 100)
widgets += gauge(pid, 'exhaust', 'Exhaust', 388, 229, GREEN, 100)
return widgets
def page_for(printer, display_index, total):
pid = printer['id']
resin = printer.get('profile') == 'elegoo_resin'
widgets = [
{'bar': {'id': f'{pid}_top_progress', 'x': 0, 'y': 0, 'width': 800, 'height': 5,
'min_value': 0, 'max_value': 100, 'value': 0, 'bg_color': TRACK,
'indicator': {'bg_color': GREEN}, 'gesture_bubble': True}},
label(printer['name'], 16, 17, 485, 26, text_color=GREEN),
label(f'{display_index} / {total}', 520, 24, 70, 14, text_color=MUTED, text_align='CENTER'),
label('SYNCING', 598, 21, 186, 16, id=f'{pid}_status_label', text_align='RIGHT', text_color=MUTED,
clickable=True, on_click=[{'lvgl.widget.show': f'{pid}_health_panel'}]),
{'obj': {'x': 578, 'y': 64, 'width': 1, 'height': 362, 'bg_color': TRACK,
'border_width': 0, 'pad_all': 0, 'scrollable': False, 'gesture_bubble': True}},
]
widgets += gauge(pid, 'progress', 'Progress', 16, 62, GREEN, 100)
if resin:
widgets += gauge(pid, 'uv', 'UV LED', 202, 62, ORANGE, 100)
widgets += gauge(pid, 'layer', 'Layer', 388, 62, CYAN, 100)
widgets += [label('FINISH / TIME REMAINING', 16, 260, 544, 18,
text_align='CENTER', text_color=MUTED),
label('--', 16, 354, 544, 20, id=f'{pid}_elapsed_label',
text_align='CENTER', text_color=MUTED)]
else:
widgets += fdm_gauges(printer)
widgets += [
label('--', 16, 300 if resin else 401, 544, 26, id=f'{pid}_remaining_label', text_color=GREEN, text_align='CENTER'),
label('Telemetry unavailable', 16, 430, 544, 12, id=f'{pid}_stage_label', text_color=MUTED,
text_align='CENTER', long_mode='SCROLL_CIRCULAR', clickable=True,
on_click=[{'lvgl.widget.show': f'{pid}_health_panel'}]),
label('--', 594, 397, 190, 16, id=f'{pid}_layers_label', text_color=MUTED, text_align='CENTER'),
]
trays = trays_for(printer)
for i, tray in enumerate(trays):
bank = i // 4
slot = i % 4
if slot == 0:
title = 'AMS ' + tray['label'][:-1] if tray['label'][-1:].isdigit() else 'EXTERNAL SPOOL'
widgets.append(label(title, 594, 70 + bank * 145, 190, 20,
text_align='CENTER', text_color=MUTED))
widgets.append({'obj': {
'id': f'{pid}_tray_{i}_card', 'x': 594 + 48 * slot, 'y': 102 + bank * 145,
'width': 44, 'height': 82, 'bg_color': BG, 'radius': 5,
'border_width': 1, 'border_color': TRACK, 'pad_all': 0,
'scrollable': False, 'gesture_bubble': True,
'widgets': [
{'obj': {'id': f'{pid}_tray_{i}_swatch', 'x': 3, 'y': 3, 'width': 36,
'height': 40, 'bg_color': TRACK, 'radius': 4, 'border_width': 0,
'pad_all': 0, 'scrollable': False, 'gesture_bubble': True}},
label(f'{tray["label"]}\n--', 1, 46, 40, 12, height=34,
id=f'{pid}_tray_{i}_label', text_align='CENTER'),
]}})
if not trays:
widgets.append(label('ELEGOO' if printer.get('profile', '').startswith('elegoo_') else 'No AMS', 594, 150, 190, 20,
text_color=MUTED, text_align='CENTER'))
widgets.append(label(printer['subtitle'], 594, 185, 190, 16,
text_color=MUTED, text_align='CENTER'))
for direction, x, text in (('previous', 16, '<'), ('next', 720, '>')):
widgets.append({'button': {'x': x, 'y': 446, 'width': 64, 'height': 30,
'bg_color': TRACK, 'radius': 8, 'border_width': 0,
'pad_all': 0, 'gesture_bubble': True,
'widgets': [{'label': {'text': text, 'align': 'CENTER',
'text_color': TEXT_COLOR, 'text_font': 'montserrat_18'}}],
'on_click': [{f'lvgl.page.{direction}': {}}]}})
widgets.append(label('Waiting for Home Assistant', 94, 449, 612, 14,
id=f'{pid}_task_label', text_align='CENTER'))
widgets.append({'obj': {
'id': f'{pid}_health_panel', 'x': 24, 'y': 62, 'width': 752, 'height': 366,
'hidden': True, 'bg_color': BG, 'bg_opa': 'COVER', 'border_width': 2,
'border_color': MUTED, 'radius': 10, 'pad_all': 0, 'scrollable': False,
'gesture_bubble': False,
'widgets': [
label('Printer health / ' + printer['name'], 16, 14, 716, 20),
{'obj': {'x': 12, 'y': 50, 'width': 724, 'height': 246,
'bg_color': BG, 'border_width': 0, 'pad_all': 0,
'scroll_dir': 'VER', 'scrollbar_mode': 'AUTO', 'gesture_bubble': False,
'widgets': [label('No current health notifications.', 0, 0, 700, 18,
id=f'{pid}_health_details', long_mode='WRAP', height='SIZE_CONTENT')]}},
{'button': {'x': 624, 'y': 310, 'width': 108, 'height': 40, 'bg_color': TRACK,
'radius': 6, 'widgets': [{'label': {'text': 'CLOSE', 'align': 'CENTER'}}],
'on_click': [{'lvgl.widget.hide': f'{pid}_health_panel'}]}},
]}})
return {'id': f'{pid}_page', 'bg_color': BG, 'bg_opa': 'COVER',
'pad_all': 0, 'scrollable': False, 'scrollbar_mode': 'OFF',
'on_swipe_left': [
{'lambda': Lambda('if (auto *input = lv_indev_active()) lv_indev_wait_release(input);')},
{'lvgl.page.next': {'animation': 'MOVE_LEFT', 'time': '180ms'}}],
'on_swipe_right': [
{'lambda': Lambda('if (auto *input = lv_indev_active()) lv_indev_wait_release(input);')},
{'lvgl.page.previous': {'animation': 'MOVE_RIGHT', 'time': '180ms'}}],
'on_load': [{'script.execute': f'{pid}_refresh'}], 'widgets': widgets}
def snapshot_lines(printer, expressions, trays):
profile = printer.get('profile', 'bambu')
elegoo = profile.startswith('elegoo_')
resin = profile == 'elegoo_resin'
lines = ['bambu_display::Snapshot data;',
'data.connected = id(ha_api).is_connected_with_state_subscription();']
for key, expr in expressions.items():
if not key.startswith(('tray_', 'hms_1_', 'hms_2_', 'hms_3_')):
lines.append(f'data.{key} = {expr};')
if elegoo:
lines.append('data.elegoo = true;')
else:
for number in range(1, 4):
lines.append('data.health_notices.push_back({' + ', '.join(
expressions[f'hms_{number}_{key}'] for key in ('code', 'message', 'severity')) + '});')
if resin:
lines.append('data.resin = true;')
if printer.get('dual_nozzle'):
lines.append('data.dual_nozzle = true;')
lines += [
'const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit);',
'const auto clock = id(ha_time).now();',
'if (clock.is_valid() && seconds_left >= 0)',
' data.finish_time = esphome::ESPTime::from_epoch_local(clock.timestamp + seconds_left).strftime("%H:%M");',
]
for i, tray in enumerate(trays):
lines.append('data.trays.push_back({' + ', '.join([
json.dumps(tray['label']), expressions[f'tray_{i}_type'],
expressions[f'tray_{i}_color'], expressions[f'tray_{i}_active']]) + '});')
return lines
def overview_slots(count):
"""Place one steam gauge per printer under the page title."""
columns = 4 if count > 4 else max(count, 1)
rows = (count + columns - 1) // columns
top, bottom = 46, 432
row_h = (bottom - top) // rows
arc = min(154, (800 // columns) - 24, row_h - 48)
col_w = 800 // columns
slots = []
for index in range(count):
row, col = divmod(index, columns)
in_row = min(columns, count - row * columns)
x0 = (800 - in_row * col_w) // 2
gauge_w = arc + 16
slots.append((x0 + col * col_w + (col_w - gauge_w) // 2,
top + row * row_h, arc))
return slots
def overview_gauge(printer, x, y, arc):
pid = printer['id']
scale = arc / 154
value_font = 32 if arc >= 130 else 18
open_page = [{'lvgl.page.show': f'{pid}_page'}]
inner = max(10, int(round(13 * scale)))
inner_x = x + int(round(8 * scale))
number_y = y + int(round(arc * 0.30))
pad = 8
return [
{'arc': {'id': f'{pid}_overview_ring', 'x': inner_x - pad // 2, 'y': y - pad // 2,
'width': arc + pad, 'height': arc + pad, 'min_value': 0, 'max_value': 100,
'value': 100, 'start_angle': 135, 'end_angle': 45,
'arc_width': inner + pad, 'arc_color': RING_BACKING,
'indicator': {'arc_color': RING_BACKING, 'arc_width': inner + pad},
'adjustable': False, 'hidden': True, 'gesture_bubble': True, 'clickable': True,
'on_click': open_page}},
{'arc': {'id': f'{pid}_overview_arc', 'x': inner_x, 'y': y,
'width': arc, 'height': arc, 'min_value': 0, 'max_value': 100, 'value': 0,
'start_angle': 135, 'end_angle': 45, 'arc_width': inner, 'arc_color': TRACK,
'indicator': {'arc_color': GREEN, 'arc_width': inner}, 'adjustable': False,
'gesture_bubble': True, 'clickable': True, 'on_click': open_page}},
label('--', inner_x, number_y, arc, value_font, id=f'{pid}_overview_value',
text_align='CENTER', text_color=0xFFFFFF, clickable=True, on_click=open_page),
label('%', inner_x, number_y + (34 if value_font >= 28 else 20), arc, 16,
id=f'{pid}_overview_percent', text_align='CENTER', text_color=0xFFFFFF,
clickable=True, on_click=open_page),
label(printer['name'], x, y + arc + 2, arc + 16, 14, text_align='CENTER',
text_color=TEXT_COLOR, clickable=True, on_click=open_page),
label('--', x, y + arc + 22, arc + 16, 14, id=f'{pid}_overview_finish',
text_align='CENTER', text_color=TEXT_COLOR, clickable=True, on_click=open_page),
]
def overview_page(printers):
widgets = [
label('Print Status Overview', 16, 10, 520, 22, text_color=GREEN),
label('--:--', 548, 12, 236, 18, id='fleet_clock', text_align='RIGHT', text_color=TEXT_COLOR),
]
for printer, (x, y, arc) in zip(printers, overview_slots(len(printers))):
widgets += overview_gauge(printer, x, y, arc)
for direction, x, text in (('previous', 16, '<'), ('next', 720, '>')):
widgets.append({'button': {'x': x, 'y': 446, 'width': 64, 'height': 30,
'bg_color': TRACK, 'radius': 8, 'border_width': 0,
'pad_all': 0, 'gesture_bubble': True,
'widgets': [{'label': {'text': text, 'align': 'CENTER',
'text_color': TEXT_COLOR, 'text_font': 'montserrat_18'}}],
'on_click': [{f'lvgl.page.{direction}': {}}]}})
widgets.append(label(f'1 / {len(printers) + 1}', 94, 449, 612, 14,
text_align='CENTER', text_color=MUTED))
return {'id': 'fleet_page', 'bg_color': BG, 'bg_opa': 'COVER',
'pad_all': 0, 'scrollable': False, 'scrollbar_mode': 'OFF',
'on_swipe_left': [
{'lambda': Lambda('if (auto *input = lv_indev_active()) lv_indev_wait_release(input);')},
{'lvgl.page.next': {'animation': 'MOVE_LEFT', 'time': '180ms'}}],
'on_swipe_right': [
{'lambda': Lambda('if (auto *input = lv_indev_active()) lv_indev_wait_release(input);')},
{'lvgl.page.previous': {'animation': 'MOVE_RIGHT', 'time': '180ms'}}],
'on_load': [{'script.execute': 'fleet_refresh'}], 'widgets': widgets}
def fleet_refresh_lines(snapshots):
lines = []
for pid, snapshot in snapshots:
lines.append('{')
lines.extend(snapshot)
lines.append('bambu_display::render_overview(data, {' + ', '.join([
f'id({pid}_overview_arc)', f'id({pid}_overview_value)',
f'id({pid}_overview_finish)', f'id({pid}_overview_ring)']) + '});')
lines.append('}')
lines += [
'{',
'const auto clock = id(ha_time).now();',
'std::string clock_text = "--:--";',
'if (clock.is_valid()) {',
' clock_text = esphome::ESPTime::from_epoch_local(clock.timestamp).strftime("%I:%M %p");',
' if (!clock_text.empty() && clock_text[0] == \'0\') clock_text.erase(0, 1);',
'}',
'bambu_display::set_text(id(fleet_clock), clock_text);',
'}',
]
return lines
def color_test_page():
widgets = [label('DISPLAY COLOR TEST', 16, 18, 768, 28),
label('Each block must match its label. Swipe to return to the printers.',
16, 62, 768, 18)]
colors = [('RED', 0xFF0000), ('GREEN', 0x00FF00), ('BLUE', 0x0000FF),
('WHITE', 0xFFFFFF), ('CYAN', 0x00FFFF), ('MAGENTA', 0xFF00FF)]
for i, (name, color) in enumerate(colors):
widgets += [
{'obj': {'x': 16 + i * 129, 'y': 118, 'width': 122, 'height': 200,
'bg_color': color, 'bg_opa': 'COVER', 'border_width': 0,
'scrollable': False, 'gesture_bubble': True}},
label(name, 16 + i * 129, 332, 122, 18, text_align='CENTER'),
]
return {'id': 'color_test_page', 'skip': True, 'bg_color': BG, 'bg_opa': 'COVER',
'pad_all': 0, 'scrollable': False,
'on_swipe_left': [{'lvgl.page.next': {}}],
'on_swipe_right': [{'lvgl.page.previous': {}}], 'widgets': widgets}
def generate(printers):
validate_printers(printers)
package = {'sensor': [], 'text_sensor': [], 'script': [],
'interval': [{'interval': '1s', 'then': []}], 'lvgl': {'pages': []}}
snapshots = []
page_total = len(printers) + 1
for index, printer in enumerate(printers):
pid = printer['id']
profile = printer.get('profile', 'bambu')
elegoo = profile.startswith('elegoo_')
resin = profile == 'elegoo_resin'
fields = {**ELEGOO_COMMON, **(ELEGOO_RESIN if resin else ELEGOO_FDM)} if elegoo else dict(NUMERIC)
if printer.get('dual_nozzle'):
fields.update(DUAL)
fields.update({key: EXTRA_FANS[key] for key in printer.get('extra_fans', [])})
expressions = {}
for key, suffix in fields.items():
sid = f'{pid}_{key}'
domain, suffix = suffix if isinstance(suffix, tuple) else ('sensor', suffix)
eid = entity(printer, key, suffix, domain)
package['sensor'].append({'platform': 'homeassistant', 'id': sid,
'entity_id': eid, 'internal': True})
expressions[key] = f'id({sid}).state'
if elegoo:
text_fields = {key: (entity(printer, key, suffix), None) for key, suffix in {
'status': 'print_status', 'task': 'file_name', 'stage': 'current_status',
'print_error': 'print_error', 'print_error_reason': 'error_status_reason'}.items()}
text_fields['online'] = (entity(printer, 'online', 'sdcp_status', 'binary_sensor'), None)
else:
text_fields = {key: (entity(printer, key, suffix), None) for key, suffix in TEXT.items()}
text_fields.update({key: (entity(printer, key, suffix, 'binary_sensor'), None)
for key, suffix in BINARY.items()})
hms_entity = entity(printer, 'hms_error', BINARY['hms_error'], 'binary_sensor')
text_fields['hms_count'] = (hms_entity, 'Count')
for number in range(1, 4):
for key, attribute in [('message', 'Error'), ('code', 'Code'), ('severity', 'Severity')]:
text_fields[f'hms_{number}_{key}'] = (hms_entity, f'{number}-{attribute}')
print_error_entity = entity(printer, 'print_error', BINARY['print_error'], 'binary_sensor')
text_fields['print_error_message'] = (print_error_entity, 'error')
text_fields['print_error_code'] = (print_error_entity, 'code')
for key, numeric_key in [('remaining_unit', 'time_left'), ('nozzle_unit', 'nozzle'),
('bed_unit', 'bed'), ('uv_unit', 'uv'), ('elapsed_unit', 'elapsed'),
('total_time_unit', 'total_time')]:
if numeric_key in fields:
suffix = fields[numeric_key]
domain, suffix = suffix if isinstance(suffix, tuple) else ('sensor', suffix)
text_fields[key] = (entity(printer, numeric_key, suffix, domain), 'unit_of_measurement')
trays = trays_for(printer)
for i, tray in enumerate(trays):
for key, attr in [('type', 'type'), ('color', 'color'), ('active', 'active')]:
text_fields[f'tray_{i}_{key}'] = (tray['entity'], attr)
for key, (eid, attribute) in text_fields.items():
sid = f'{pid}_{key}'
subscription = {'platform': 'homeassistant', 'id': sid, 'entity_id': eid, 'internal': True}
if attribute:
subscription['attribute'] = attribute
package['text_sensor'].append(subscription)
expressions[key] = f'id({sid}).state'
lines = snapshot_lines(printer, expressions, trays)
snapshots.append((pid, list(lines)))
tray_widgets = ['{' + ', '.join(f'id({pid}_tray_{i}_{part})'
for part in ('card', 'swatch', 'label')) + '}'
for i in range(len(trays))]
refs = [f'id({pid}_{key})' for key in (
'status_label', 'task_label', 'top_progress', 'remaining_label', 'layers_label', 'stage_label',
'health_details')]
for key in ('progress', 'nozzle', 'bed', 'fan', 'aux', 'exhaust'):
refs.append('{' + ', '.join(f'id({pid}_{key}_{part})' for part in ('arc', 'label', 'detail')) + '}'
if not resin or key == 'progress' else '{}')
refs.append('{' + ', '.join(tray_widgets) + '}')
if resin:
for key in ('uv', 'layer'):
refs.append('{' + ', '.join(f'id({pid}_{key}_{part})' for part in ('arc', 'label', 'detail')) + '}')
refs.append(f'id({pid}_elapsed_label)')
lines.append('bambu_display::render(data, {' + ', '.join(refs) + '});')
package['script'].append({'id': f'{pid}_refresh', 'then': [{'lambda': Lambda('\n'.join(lines))}]})
package['interval'][0]['then'].append({'if': {
'condition': {'lvgl.page.is_showing': f'{pid}_page'},
'then': [{'script.execute': f'{pid}_refresh'}]}})
package['lvgl']['pages'].append(page_for(printer, index + 2, page_total))
package['script'].append({'id': 'fleet_refresh', 'then': [
{'lambda': Lambda('\n'.join(fleet_refresh_lines(snapshots)))}]})
package['interval'][0]['then'].append({'if': {
'condition': {'lvgl.page.is_showing': 'fleet_page'},
'then': [{'script.execute': 'fleet_refresh'}]}})
package['lvgl']['pages'].insert(0, overview_page(printers))
package['lvgl']['pages'].append(color_test_page())
package['button'] = [{'platform': 'template', 'name': 'Display Color Test',
'entity_category': 'diagnostic',
'on_press': [{'lvgl.page.show': 'color_test_page'}]}]
return package
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--check', action='store_true', help='Fail if generated.yaml is stale')
args = parser.parse_args()
printers = yaml.safe_load((HERE / 'printers.yaml').read_text())['printers']
package = generate(printers)
content = ('# AUTO-GENERATED by bambu-display/generate.py. Edit printers.yaml or generate.py.\n'
+ yaml.dump(package, Dumper=Dumper, sort_keys=False, allow_unicode=True, width=110))
destination = HERE / 'generated.yaml'
if args.check:
if not destination.exists() or destination.read_text() != content:
raise SystemExit('generated.yaml is stale; run generate.py')
print(f'Generated configuration is current ({len(printers)} printer pages plus fleet overview).')
else:
destination.write_text(content)
print(f'Generated {destination.name} with {len(printers)} printer pages plus fleet overview.')
if __name__ == '__main__':
main()
File diff suppressed because it is too large Load Diff
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""Render the real LVGL pages with demo telemetry using ESPHome's headless SDL host."""
import argparse
import json
from pathlib import Path
import subprocess
import tempfile
from PIL import Image
import yaml
from generate import Dumper, HERE, Lambda, generate
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, default=HERE / 'previews')
args = parser.parse_args()
printers = yaml.safe_load((HERE / 'printers.yaml').read_text())['printers']
package = generate(printers)
demo_values = {'progress': 67, 'nozzle': 218, 'nozzle_target': 220, 'bed': 60,
'bed_target': 60, 'time_left': 1.75, 'layer': 134, 'total_layers': 200,
'fan': 80, 'aux_fan': 45, 'exhaust_fan': 30, 'left_nozzle': 219, 'left_target': 220,
'right_nozzle': 37, 'right_target': 0, 'uv': 42, 'elapsed': 30,
'total_time': 200}
demo_text = {'status': 'running', 'task': 'Workshop organizer - plate 2',
'stage': 'printing', 'online': 'on', 'print_error': 'off', 'hms_error': 'off',
'hms_count': '0', 'print_error_message': '', 'print_error_code': '',
'remaining_unit': 'h', 'nozzle_unit': '°C', 'bed_unit': '°C',
'uv_unit': '°C', 'elapsed_unit': 'min', 'total_time_unit': 'min',
'print_error_reason': 'ok'}
colors = ['#60DE9AFF', '#F4C96BFF', '#74A9E8FF', '#F8F4ECFF',
'#DF8BBFFF', '#FA936BFF', '#8E79CAFF', '#4CC9C1FF']
for printer in printers:
prefix = printer['id'] + '_'
for section in ('sensor', 'text_sensor'):
for entry in package[section]:
if not entry['id'].startswith(prefix):
continue
key = entry['id'][len(prefix):]
if section == 'sensor':
value = str(demo_values[key])
if key == 'time_left' and printer.get('profile', '').startswith('elegoo_'):
value = '105'
else:
if key.startswith('tray_'):
_, index, attribute = key.split('_')
text = {'type': 'PLA', 'color': colors[int(index) % len(colors)],
'active': 'on' if index == '0' else 'off'}[attribute]
elif key.startswith(('hms_1_', 'hms_2_', 'hms_3_')):
text = ''
else:
text = demo_text[key]
if printer.get('profile', '').startswith('elegoo_'):
text = {'status': 'printing', 'print_error': 'none',
'remaining_unit': 'min'}.get(key, text)
value = 'std::string(' + json.dumps(text, ensure_ascii=False) + ')'
entry.clear()
entry.update({'platform': 'template', 'id': prefix + key, 'internal': True,
'lambda': Lambda('return ' + value + ';'), 'update_interval': '60s'})
for script in package['script']:
code = script['then'][0]['lambda']
script['then'][0]['lambda'] = Lambda(code.replace(
'id(ha_api).is_connected_with_state_subscription()', 'true'))
args.output.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix='bambu-lvgl-preview-') as work:
root = Path(work)
# Inject real LVGL pointer samples; test bubbling through cards/buttons
# without a GUI window or a physical touchscreen.
touch_header = root / 'preview_touch.h'
touch_header.write_text('''#pragma once
#include <lvgl.h>
namespace bambu_preview {
inline int x = 0, y = 0;
inline bool pressed = false;
inline void setup() {
auto *input = lv_indev_create();
lv_indev_set_type(input, LV_INDEV_TYPE_POINTER);
lv_indev_set_display(input, lv_display_get_default());
lv_indev_set_read_cb(input, [](lv_indev_t *, lv_indev_data_t *data) {
data->point.x = x;
data->point.y = y;
data->state = pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
});
}
}
''')
steps = [{'component.update': entry['id']}
for section in ('sensor', 'text_sensor') for entry in package[section]]
steps += [{'delay': '1s'},
{'lvgl.page.show': 'fleet_page'},
{'script.execute': 'fleet_refresh'}, {'delay': '300ms'},
{'snapshot.take': {'id': 'panel_display', 'filename': 'fleet.bmp'}},
{'lambda': Lambda(
'if (std::string(lv_label_get_text(id(fleet_clock))) == "--:--") '
'{ ESP_LOGE("preview", "Fleet clock render failed"); exit(7); }')}]
for printer in printers:
pid = printer['id']
steps.append({'lambda': Lambda(
f'if (std::string(lv_label_get_text(id({pid}_overview_value))) != "67" || '
f'lv_arc_get_value(id({pid}_overview_arc)) != 67 || '
f'std::string(lv_label_get_text(id({pid}_overview_percent))) != "%" || '
f'std::string(lv_label_get_text(id({pid}_overview_finish))) == "--") '
f'{{ ESP_LOGE("preview", "Fleet gauge render failed: {pid}"); exit(7); }}')})
for printer in printers:
steps += [{'lvgl.page.show': printer['id'] + '_page'},
{'script.execute': printer['id'] + '_refresh'}, {'delay': '300ms'},
{'snapshot.take': {'id': 'panel_display', 'filename': printer['id'] + '.bmp'}}]
if printer.get('profile', '').startswith('elegoo_'):
pid = printer['id']
checks = {f'{pid}_status_label': 'PRINTING', f'{pid}_progress_detail': '1h 45m'}
if printer['profile'] == 'elegoo_resin':
checks.update({f'{pid}_uv_label': '42', f'{pid}_layer_label': '134',
f'{pid}_elapsed_label': 'Elapsed: 0h 30m'})
for widget, expected in checks.items():
steps.append({'lambda': Lambda(f'if (std::string(lv_label_get_text(id({widget}))) != {json.dumps(expected)}) {{ ESP_LOGE("preview", "Elegoo telemetry render failed: {widget}"); exit(4); }}')})
steps += [
{'text_sensor.template.publish': {'id': f'{pid}_print_error_reason', 'state': 'filament_runout'}},
{'script.execute': f'{pid}_refresh'},
{'lambda': Lambda(f'if (std::string(lv_label_get_text(id({pid}_status_label))) != "PRINT ERROR" || std::string(lv_label_get_text(id({pid}_health_details))).find("Filament runout") == std::string::npos) {{ ESP_LOGE("preview", "Elegoo error render failed"); exit(5); }}')},
{'text_sensor.template.publish': {'id': f'{pid}_online', 'state': 'off'}},
{'script.execute': f'{pid}_refresh'},
{'lambda': Lambda(f'if (std::string(lv_label_get_text(id({pid}_status_label))) != "PRINTER OFFLINE" || std::string(lv_label_get_text(id({pid}_progress_label))) != "--") {{ ESP_LOGE("preview", "Elegoo offline render failed"); exit(6); }}')},
{'text_sensor.template.publish': {'id': f'{pid}_print_error_reason', 'state': 'ok'}},
{'text_sensor.template.publish': {'id': f'{pid}_online', 'state': 'on'}},
{'script.execute': f'{pid}_refresh'},
]
steps += [{'lvgl.page.show': 'color_test_page'}, {'delay': '300ms'},
{'snapshot.take': {'id': 'panel_display', 'filename': 'color_test.bmp'}}]
demo_printer = printers[0]['id']
for key, value in {'hms_error': 'on', 'hms_count': '1', 'hms_1_code': 'DEMO_CODE',
'hms_1_severity': 'common',
'hms_1_message': 'DEMO: A printer maintenance notification. This is synthetic preview data, not an actual fault diagnosis.'}.items():
steps.append({'text_sensor.template.publish': {'id': f'{demo_printer}_{key}', 'state': value}})
steps += [{'lvgl.page.show': demo_printer + '_page'},
{'script.execute': demo_printer + '_refresh'},
{'lvgl.widget.show': demo_printer + '_health_panel'}, {'delay': '300ms'},
{'snapshot.take': {'id': 'panel_display', 'filename': 'health_notice.bmp'}},
{'lvgl.widget.hide': demo_printer + '_health_panel'},
{'text_sensor.template.publish': {'id': demo_printer + '_hms_error', 'state': 'off'}},
{'script.execute': demo_printer + '_refresh'}]
steps += [{'lvgl.page.show': 'fleet_page'}, {'delay': '250ms'},
{'lambda': Lambda('bambu_preview::setup();')}]
for x, y, expect_hidden in [(690, 30, False), (700, 394, True)]:
steps += [
{'lambda': Lambda(f'bambu_preview::x = {x}; bambu_preview::y = {y}; bambu_preview::pressed = true;')},
{'delay': '70ms'}, {'lambda': Lambda('bambu_preview::pressed = false;')}, {'delay': '150ms'},
{'lambda': Lambda(f'if (lv_obj_has_flag(id({demo_printer}_health_panel), LV_OBJ_FLAG_HIDDEN) != {str(expect_hidden).lower()}) {{ ESP_LOGE("preview", "Health panel tap failed"); exit(3); }}')},
]
if len(printers) > 1:
# First -> second -> first -> last -> first, then a swipe starting
# on NEXT must advance exactly once even when released over PREV.
# Fleet -> first printer -> fleet -> last printer -> fleet, then a
# swipe starting on NEXT advances exactly once.
gestures = [(650, 180, 180, printers[0]['id']),
(180, 650, 180, 'fleet'),
(180, 650, 180, printers[-1]['id']),
(650, 180, 180, 'fleet'),
(730, 60, 460, printers[0]['id'])]
for start, end, y, expected in gestures:
steps += [
{'lambda': Lambda(f'bambu_preview::x = {start}; bambu_preview::y = {y}; bambu_preview::pressed = true;')},
{'delay': '70ms'},
{'lambda': Lambda(f'bambu_preview::x = {end};')}, {'delay': '70ms'},
{'lambda': Lambda('bambu_preview::pressed = false;')}, {'delay': '350ms'},
{'lambda': Lambda(f'if (!id({expected}_page).is_showing()) {{ ESP_LOGE("preview", "Swipe navigation failed"); exit(2); }}')},
]
steps += [{'lambda': Lambda('exit(0);')}]
config = {'esphome': {'name': 'bambu-display-preview',
'includes': [str(HERE / 'dashboard.h'), str(touch_header)],
'on_boot': {'priority': -100, 'then': steps}},
'host': {}, 'logger': {'level': 'WARN'},
'time': [{'platform': 'host', 'id': 'ha_time', 'timezone': 'America/New_York'}],
'display': [{'platform': 'sdl', 'id': 'panel_display', 'dimensions': '800x480',
'headless': True, 'auto_clear_enabled': False, 'update_interval': 'never'}],
'lvgl': {'id': 'dashboard', 'displays': ['panel_display'], 'buffer_size': '100%',
'color_depth': 16, 'bg_color': 0x000000, 'text_color': 0xEEEEEE,
'default_font': 'montserrat_18', **package.pop('lvgl')}, **package}
filename = root / 'preview.yaml'
filename.write_text(yaml.dump(config, Dumper=Dumper, sort_keys=False, allow_unicode=True))
subprocess.run(['esphome', 'compile', str(filename)], check=True)
binary = root / '.esphome/build/bambu-display-preview/.pioenvs/bambu-display-preview/program'
subprocess.run([str(binary)], check=True, timeout=30, cwd=root)
for name in ['fleet'] + [printer['id'] for printer in printers] + ['color_test', 'health_notice']:
bmp = root / '.esphome/snapshots/bambu-display-preview' / (name + '.bmp')
with Image.open(bmp) as image:
image.save(args.output / (name + '.png'))
print(f'Saved {len(printers)} demo screenshots to {args.output}')
print('Passed LVGL pointer swipe tests, including wraparound and button gesture consumption.')
print('Passed status-tap health panel open and Close-button tests.')
print('Passed Elegoo telemetry, text-error and offline rendering checks.')
if __name__ == '__main__':
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+63
View File
@@ -0,0 +1,63 @@
# Order here is the swipe order. Add/remove entries and rerun generate.py.
# entity_prefix is the HA entity ID between the domain and the metric suffix.
# Optional entities: {metric: sensor.custom_entity} overrides individual mappings.
printers:
- id: prints_charming
name: PrintsCharming
subtitle: P1S / Office
entity_prefix: p1s_01p09c470102673
extra_fans: [aux_fan, exhaust_fan]
filament_banks:
- label: A
entity_prefix: p1s_01p09c470102673_ams_1
- label: B
entity_prefix: p1s_01p09c470102673_ams_2
- id: polly_pocket
name: Polly-Pocket
subtitle: A1 mini / Office
entity_prefix: a1mini_0309ca580403653
external_spools:
- label: EXT
entity: sensor.a1mini_0309ca580403653_externalspool_external_spool
- id: ds_turd_flinger
name: Ds-Turd-Flinger
subtitle: A1 / Upstairs
entity_prefix: a1_03919d540806387
filament_banks:
- label: A
entity_prefix: a1_03919d540806387_ams_1
- id: jordyns_layer_slayer
name: Jordyns-Layer-Slayer
subtitle: A1 / Upstairs
entity_prefix: jordyns_layer_slayer
filament_banks:
- label: A
entity_prefix: jordyns_layer_slayer_ams
- id: constipation_orion
name: Constipation-Orion
subtitle: Dual nozzle / Office
entity_prefix: constipation_orion
dual_nozzle: true
extra_fans: [exhaust_fan]
filament_banks:
- label: A
entity_prefix: constipation_orion_ams_1
- label: B
# This unit retains its original hardware-based HA entity names.
entity_prefix: h2c_31b8ap5a2200288_ams_2
- id: centauri_carbon
name: Centauri Carbon
subtitle: FDM / Office
entity_prefix: centauri_carbon
profile: elegoo_fdm
- id: saturn
name: Saturn
subtitle: Resin / Garage
entity_prefix: saturn
profile: elegoo_resin
+175
View File
@@ -0,0 +1,175 @@
#include "dashboard.h"
#include <cassert>
int main() {
using namespace bambu_display;
assert(remaining(1.75f, "h") == "1h 45m");
assert(remaining(105.0f, "min") == "1h 45m");
assert(remaining(6300.0f, "s") == "1h 45m");
assert(remaining(6300000.0f, "ms") == "1h 45m");
assert(remaining(0.15f, "h") == "0h 09m");
assert(remaining(NAN, "h") == "--");
assert(remaining(-1, "min") == "--");
assert(remaining(10, "") == "--");
assert(remaining(INFINITY, "h") == "--");
assert(number(NAN) == "--");
assert(temperature(219, 220, "°C") == "219 / 220 °C");
assert(filament_color("#00FF00FF") == GREEN);
assert(filament_color("60de9a") == 0x60DE9A);
assert(filament_color("#60DE9AZZ") == TRACK);
assert(filament_color("unknown") == TRACK);
assert(gauge_value(NAN, 100) == 0);
assert(gauge_value(110, 100) == 100);
assert(gauge_value(-10, 100) == 0);
assert(duration_seconds(1.75f, "h") == 6300);
assert(duration_seconds(105, "min") == 6300);
assert(duration_seconds(6300000, "ms") == 6300);
assert(duration_seconds(NAN, "h") == -1);
assert(duration_seconds(1, "") == -1);
assert(std::abs(celsius(392, "°F") - 200) < 0.01f);
Snapshot data;
data.online = "on";
data.status = "running";
assert(!data.live());
assert(data.status_text() == "HA OFFLINE");
data.connected = true;
assert(data.live());
assert(data.status_text() == "PRINTING");
data.online = "off";
assert(!data.live());
assert(data.status_text() == "PRINTER OFFLINE");
data.online = "unavailable";
assert(data.status_text() == "SYNCING");
data.online = "on";
data.status = "unknown";
assert(!data.live());
data.status = "pause";
assert(data.status_text() == "PAUSED");
assert(data.status_color() == AMBER);
data.print_error = "on";
assert(data.status_text() == "PRINT ERROR");
assert(data.status_color() == RED);
data.print_error = "off";
data.hms_error = "True";
assert(data.status_text() == "HEALTH NOTICE");
assert(data.status_color() == AMBER);
assert(data.health_details().find("have not arrived") != std::string::npos);
data.hms_count = "1";
data.health_notices = {{"DEMO_CODE", "A synthetic information message", "info"}};
assert(data.status_text() == "HEALTH INFO");
assert(data.health_details().find("DEMO_CODE") != std::string::npos);
assert(data.alert_summary() == "A synthetic information message");
data.health_notices.push_back({"DEMO_2", "A synthetic serious message", "serious"});
data.hms_count = "2";
assert(data.status_text() == "HEALTH ERROR");
assert(data.status_color() == RED);
assert(data.health_details().find("A synthetic serious message") != std::string::npos);
data.health_notices.pop_back();
data.hms_count = "5";
assert(data.status_text() == "HEALTH NOTICE");
assert(data.health_details().find("4 more") != std::string::npos);
data.hms_error = "off";
assert(data.health_details() == "No current printer health notifications.");
data.status = "finish";
assert(data.status_text() == "COMPLETE");
assert(data.status_color() == GREEN);
// Elegoo exposes error enums rather than Bambu binary errors/HMS notices.
data.elegoo = true;
data.print_error = "none";
data.print_error_reason = "ok";
data.status = "printing";
assert(data.live());
assert(!data.has_print_error());
assert(data.status_text() == "PRINTING");
data.print_error_reason = "filament_runout";
assert(data.status_text() == "PRINT ERROR");
assert(data.status_color() == RED);
assert(data.health_details().find("Filament runout") != std::string::npos);
data.print_error = "invalid_resolution";
assert(data.alert_summary() == "Invalid resolution / Filament runout");
data.print_error_reason = "unavailable";
assert(data.alert_summary() == "Invalid resolution");
data.print_error = "unknown";
assert(!data.has_print_error());
data.status = "error";
assert(data.has_print_error());
data.status = "pausing";
assert(data.status_color() == AMBER);
data.status = "complete";
assert(data.status_text() == "COMPLETE");
data.status = "preheating_completed";
data.stage = "printing";
data.progress = 0;
data.layer = 0;
data.total_layers = 400;
data.elapsed = 30;
data.elapsed_unit = "min";
data.total_time = 60;
data.total_time_unit = "min";
assert(std::fabs(data.completion() - 50.0f) < 0.01f);
data.layer = 100;
assert(std::fabs(data.completion() - 25.0f) < 0.01f);
data.progress = 67;
assert(std::fabs(data.completion() - 67.0f) < 0.01f);
data.progress = 0;
data.layer = 0;
data.status = "idle";
data.stage = "idle";
assert(data.completion() == 0.0f);
data.online = "off";
assert(!data.live());
assert(data.status_text() == "PRINTER OFFLINE");
assert(data.health_details().find("unavailable") != std::string::npos);
data.connected = true;
data.online = "on";
data.elegoo = false;
data.print_error = "off";
data.print_error_reason = "ok";
data.hms_error = "off";
data.status = "running";
data.task = "Workshop organizer";
data.progress = 67;
data.time_left = 1.75f;
data.remaining_unit = "h";
data.finish_time = "16:05";
assert(data.overview_task() == "Workshop organizer");
assert(data.overview_meta() == "67% 16:05 1h 45m");
data.task = "";
assert(data.overview_task() == "No active print");
data.connected = false;
assert(data.overview_task() == "Waiting for Home Assistant");
assert(data.overview_meta() == "--");
data.trays = {{"A1", "PLA", "#FF8800FF", "off"}, {"A2", "PETG", "00FF00", "on"}};
assert(data.active_filament_color() == GREEN);
data.trays[1].active = "off";
data.trays[0].active = "on";
assert(data.active_filament_color() == 0xFF8800);
data.trays.clear();
assert(data.active_filament_color() == GREEN);
assert(filament_is_black(0x000000));
assert(filament_is_black(filament_color("#000000FF")));
assert(filament_is_black(0x202020));
assert(!filament_is_black(0xFFFFFF));
assert(!filament_is_black(GREEN));
assert(!filament_is_black(0xFF8800));
FleetCounts counts;
data.connected = true;
data.online = "on";
data.status = "printing";
tally(counts, data);
data.status = "pause";
tally(counts, data);
data.print_error = "on";
tally(counts, data);
data.print_error = "off";
data.online = "off";
tally(counts, data);
data.online = "unavailable";
data.connected = true;
tally(counts, data);
assert(fleet_summary(counts) == "1 attention, 1 printing, 1 paused, 1 offline, 1 syncing");
}
+201
View File
@@ -0,0 +1,201 @@
"""Run with: python -m unittest discover -s esphome/bambu-display -v."""
import copy
import unittest
import yaml
from generate import HERE, generate, validate_printers
class FleetTests(unittest.TestCase):
def setUp(self):
self.printers = yaml.safe_load((HERE / 'printers.yaml').read_text())['printers']
def test_all_subscriptions_match_repository_entities(self):
known = {line.split(' | ')[0] for line in
(HERE.parent / 'ha-entities.txt').read_text().splitlines()}
package = generate(self.printers)
subscribed = {entry['entity_id'] for domain in ('sensor', 'text_sensor')
for entry in package[domain]}
self.assertFalse(subscribed - known)
def test_fleet_can_expand_beyond_configured_printers(self):
expected_count = len(self.printers) + 2
for index in (8, 9):
extra = copy.deepcopy(self.printers[0])
extra.update(id=f'printer_{index}', entity_prefix=f'new_printer_{index}')
self.printers.append(extra)
package = generate(self.printers)
self.assertEqual(len([page for page in package['lvgl']['pages'] if not page.get('skip')]), expected_count + 1)
self.assertEqual(len(package['script']), expected_count + 1)
subscriptions = {entry['id']: entry['entity_id'] for entry in package['sensor']}
self.assertEqual(subscriptions['printer_9_progress'], 'sensor.new_printer_9_print_progress')
for page in package['lvgl']['pages']:
self.assertIn('lvgl.page.next', page['on_swipe_left'][-1])
self.assertIn('lvgl.page.previous', page['on_swipe_right'][-1])
def test_elegoo_profiles_use_available_metrics_and_domains(self):
package = generate(self.printers)
subscriptions = {entry['id']: entry for domain in ('sensor', 'text_sensor')
for entry in package[domain]}
self.assertEqual(subscriptions['centauri_carbon_nozzle_target']['entity_id'],
'number.centauri_carbon_target_nozzle_temp')
self.assertEqual(subscriptions['centauri_carbon_fan']['entity_id'],
'sensor.centauri_carbon_model_fan_speed')
for pid in ('centauri_carbon', 'saturn'):
self.assertEqual(subscriptions[f'{pid}_online']['entity_id'], f'binary_sensor.{pid}_sdcp_status')
self.assertEqual(subscriptions[f'{pid}_remaining_unit']['entity_id'], f'sensor.{pid}_remaining_print_time')
self.assertEqual(subscriptions[f'{pid}_print_error']['entity_id'], f'sensor.{pid}_print_error')
self.assertEqual(subscriptions[f'{pid}_print_error_reason']['entity_id'], f'sensor.{pid}_error_status_reason')
self.assertNotIn(f'{pid}_hms_error', subscriptions)
self.assertNotIn('saturn_nozzle', subscriptions)
self.assertNotIn('saturn_fan', subscriptions)
self.assertEqual(subscriptions['saturn_uv_unit']['entity_id'], 'sensor.saturn_uv_led_temp')
self.assertEqual(subscriptions['saturn_elapsed_unit']['entity_id'], 'sensor.saturn_current_print_time')
def test_resin_page_contains_only_resin_gauges(self):
package = generate(self.printers)
page = next(page for page in package['lvgl']['pages'] if page['id'] == 'saturn_page')
arcs = {widget['arc']['id'] for widget in page['widgets'] if 'arc' in widget}
self.assertEqual(arcs, {'saturn_progress_arc', 'saturn_uv_arc', 'saturn_layer_arc'})
def test_number_overrides_are_supported(self):
self.printers[-2]['entities'] = {'nozzle_target': 'number.custom_target'}
package = generate(self.printers)
entry = next(row for row in package['sensor'] if row['id'] == 'centauri_carbon_nozzle_target')
self.assertEqual(entry['entity_id'], 'number.custom_target')
def test_reject_unknown_profiles(self):
self.printers[0]['profile'] = 'unsupported'
with self.assertRaisesRegex(ValueError, 'unsupported profile'):
generate(self.printers)
def test_overrides_apply_to_values_and_unit_subscriptions(self):
self.printers[0]['entities'] = {'time_left': 'sensor.custom_remaining'}
package = generate(self.printers)
for domain, key in [('sensor', 'time_left'), ('text_sensor', 'remaining_unit')]:
entry = next(row for row in package[domain] if row['id'] == f'prints_charming_{key}')
self.assertEqual(entry['entity_id'], 'sensor.custom_remaining')
def test_orion_has_both_ams_units_with_their_actual_entity_prefixes(self):
package = generate(self.printers)
types = [entry['entity_id'] for entry in package['text_sensor']
if entry['id'].startswith('constipation_orion_tray_') and entry.get('attribute') == 'type']
self.assertEqual(len(types), 8)
self.assertIn('sensor.constipation_orion_ams_1_tray_1', types)
self.assertIn('sensor.h2c_31b8ap5a2200288_ams_2_tray_4', types)
def test_health_messages_import_numbered_ha_attributes(self):
package = generate(self.printers)
subscriptions = {entry['id']: entry for entry in package['text_sensor']}
message = subscriptions['prints_charming_hms_1_message']
self.assertEqual(message['entity_id'], 'binary_sensor.p1s_01p09c470102673_hms_errors')
self.assertEqual(message['attribute'], '1-Error')
self.assertEqual(subscriptions['prints_charming_hms_3_severity']['attribute'], '3-Severity')
def test_reject_empty_duplicate_and_malformed_printer_ids(self):
for invalid in ([], self.printers + [self.printers[0]],
[{**self.printers[0], 'id': 'invalid-id'}]):
with self.subTest(invalid=invalid), self.assertRaises(ValueError):
validate_printers(invalid)
def test_reject_overflowing_filament_strip(self):
self.printers[0]['external_spools'] = [{'label': 'EXT', 'entity': 'sensor.external_spool'}]
with self.assertRaisesRegex(ValueError, 'eight slots'):
generate(self.printers)
def test_progress_and_dual_nozzle_widgets_fit_the_panel(self):
package = generate(self.printers)
for page in package['lvgl']['pages']:
for widget in page['widgets']:
props = next(iter(widget.values()))
self.assertGreaterEqual(props.get('x', 0), 0)
self.assertGreaterEqual(props.get('y', 0), 0)
self.assertLessEqual(props.get('x', 0) + props.get('width', 0), 800)
self.assertLessEqual(props.get('y', 0) + props.get('height', 0), 480)
def test_rgb565_primaries_reach_the_correct_panel_bits(self):
# mipi_rgb emits named RGB data pins with bytes exchanged, then copies
# LVGL RGB565 bytes straight into the ESP32 little-endian framebuffer.
# Exercise red, green, blue and combined primaries through that path.
class Loader(yaml.SafeLoader):
pass
for tag in ('!secret', '!include'):
Loader.add_constructor(tag, lambda loader, node: loader.construct_scalar(node))
config = yaml.load((HERE / 'board.yaml').read_text(), Loader=Loader)
display = config['display'][0]
pins = display['data_pins']
natural = pins['blue'] + pins['green'] + pins['red']
driver = natural[8:] + natural[:8]
order = {'big_endian': 'big', 'little_endian': 'little'}[config['lvgl']['byte_order']]
for color in (0xF800, 0x07E0, 0x001F, 0xFFFF, 0x07FF, 0xF81F):
memory_word = int.from_bytes(color.to_bytes(2, order), 'little')
panel_word = sum(((memory_word >> i) & 1) << natural.index(pin)
for i, pin in enumerate(driver))
self.assertEqual(panel_word, color, f'RGB565 color {color:#06x} is wired incorrectly')
def test_both_boards_share_one_hardware_package(self):
class Loader(yaml.SafeLoader):
pass
Loader.add_constructor('!include', lambda loader, node: loader.construct_scalar(node))
names = set()
for filename in ('bambu-printer-display.yaml', 'bambu-printer-display-2.yaml'):
config = yaml.load((HERE.parent / filename).read_text(), Loader=Loader)
names.add(config['substitutions']['device_name'])
self.assertEqual(config['packages']['board'], 'bambu-display/board.yaml')
self.assertEqual(config['packages']['fleet'], 'bambu-display/generated.yaml')
self.assertEqual(len(names), 2)
def test_fleet_overview_lists_every_printer(self):
package = generate(self.printers)
page = package['lvgl']['pages'][0]
self.assertEqual(page['id'], 'fleet_page')
self.assertFalse(page.get('skip'))
arcs = [widget['arc'] for widget in page['widgets']
if widget.get('arc', {}).get('id', '').endswith('_overview_arc')]
self.assertEqual([arc['id'][:-len('_overview_arc')] for arc in arcs],
[printer['id'] for printer in self.printers])
backings = [widget['arc'] for widget in page['widgets']
if widget.get('arc', {}).get('id', '').endswith('_overview_ring')]
self.assertEqual([arc['id'][:-len('_overview_ring')] for arc in backings],
[printer['id'] for printer in self.printers])
for backing, arc in zip(backings, arcs):
self.assertTrue(backing['hidden'])
self.assertEqual(backing['arc_color'], 0x6E767E)
self.assertGreater(backing['width'], arc['width'])
self.assertGreaterEqual(backing['x'], 0)
self.assertGreaterEqual(backing['y'], 0)
self.assertLessEqual(backing['x'] + backing['width'], 800)
self.assertLessEqual(backing['y'] + backing['height'], 480)
for arc, printer in zip(arcs, self.printers):
self.assertEqual(arc['on_click'], [{'lvgl.page.show': printer['id'] + '_page'}])
self.assertGreaterEqual(arc['width'], 120)
self.assertLessEqual(arc['width'], 154)
self.assertEqual(arc['value'], 0)
self.assertEqual(arc['arc_color'], 0x19232B)
self.assertEqual(arc['start_angle'], 135)
self.assertLessEqual(arc['x'] + arc['width'], 800)
self.assertLessEqual(arc['y'] + arc['height'], 480)
code = next(script['then'][0]['lambda'] for script in package['script'] if script['id'] == 'fleet_refresh')
for printer in self.printers:
self.assertIn(f'id({printer["id"]}_overview_arc)', code)
self.assertIn(f'id({printer["id"]}_overview_finish)', code)
self.assertIn(f'id({printer["id"]}_overview_ring)', code)
titles = [next(iter(widget.values())).get('text') for widget in page['widgets']]
self.assertIn('Print Status Overview', titles)
self.assertNotIn('fleet_summary', {next(iter(widget.values())).get('id') for widget in page['widgets']})
shown = [step['if']['condition']['lvgl.page.is_showing'] for step in package['interval'][0]['then']]
self.assertIn('fleet_page', shown)
charming = next(item for item in package['lvgl']['pages'] if item['id'] == 'prints_charming_page')
numbers = [next(iter(widget.values())).get('text') for widget in charming['widgets']]
self.assertIn(f'2 / {len(self.printers) + 1}', numbers)
def test_diagnostic_color_page_is_skipped_during_normal_navigation(self):
package = generate(self.printers)
page = next(page for page in package['lvgl']['pages'] if page['id'] == 'color_test_page')
self.assertTrue(page['skip'])
if __name__ == '__main__':
unittest.main()
+10
View File
@@ -0,0 +1,10 @@
# Second Sunton ESP32-8048S043C fleet display. Same pages and hardware as
# bambu-printer-display.yaml, with its own ESPHome name so both boards can
# join Home Assistant. Edit bambu-display/printers.yaml, then run generate.py.
substitutions:
device_name: bambu-printer-display-2
friendly_name: Bambu Printer Display 2
packages:
board: !include bambu-display/board.yaml
fleet: !include bambu-display/generated.yaml
+10
View File
@@ -0,0 +1,10 @@
# Sunton ESP32-8048S043C: 4.3" 800x480 RGB LCD + GT911 capacitive touch.
# Edit bambu-display/printers.yaml, then run its generate.py to change the fleet.
# The second board is bambu-printer-display-2.yaml; both include bambu-display/board.yaml.
substitutions:
device_name: bambu-printer-display
friendly_name: Bambu Printer Display
packages:
board: !include bambu-display/board.yaml
fleet: !include bambu-display/generated.yaml
-275
View File
@@ -1,275 +0,0 @@
substitutions:
device_name: esp32-h2c-camera
friendly_name: "H2C Camera"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
# Required for ESP32 camera buffering.
psram:
mode: octal
logger:
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.69.90
gateway: 192.168.69.1
subnet: 255.255.255.0
ap:
ssid: "${friendly_name} Fallback"
password: !secret fallback_password
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: true # Set to true if you use static IPs and don't need discovery
# captive_portal:
web_server:
port: 80
esp32_camera:
name: "${friendly_name}"
external_clock:
pin: GPIO5
# 10MHz (half the usual 20MHz) slows the sensor's line rate, so each of the
# 1200 exposure lines below collects roughly twice the light. Costs
# framerate, which does not matter for chamber monitoring.
frequency: 10MHz
i2c_pins:
sda: GPIO8
scl: GPIO9
data_pins: [GPIO16, GPIO18, GPIO21, GPIO17, GPIO14, GPIO7, GPIO6, GPIO4]
vsync_pin: GPIO1
href_pin: GPIO2
pixel_clock_pin: GPIO15
# Resolution & quality (ESP32-S3 with Octal PSRAM handles XGA easily)
resolution: 1024X768
jpeg_quality: 10
max_framerate: 20 fps
idle_framerate: 0.2 fps
frame_buffer_count: 2
# Orientation
vertical_flip: true
horizontal_mirror: false
# Image tuning for chamber monitoring through tinted door.
# Low contrast + high brightness lifts the shadows; the LED strips are
# allowed to blow out since the chamber is what matters.
brightness: 2
contrast: -2
saturation: 0
# Exposure & gain are MANUAL on purpose. The two LED strips in frame kept the
# auto-exposure meter satisfied, so AEC never lengthened the exposure and the
# tinted-door scene stayed black regardless of gain ceiling or AE level.
# Pinning both to max forces a bright chamber; the strips clip out entirely.
aec_mode: manual
aec_value: 1200 # max integration time (0-1200 lines)
aec2: true
ae_level: 2 # only consulted if AEC is flipped back to auto
agc_mode: manual
agc_value: 30 # max sensor gain (0-30)
agc_gain_ceiling: 128X # only consulted if AGC is flipped back to auto
wb_mode: auto
# Camera endpoints:
# Stream: http://<ip>:8080
# Snapshot: http://<ip>:8081
esp32_camera_web_server:
- port: 8080
mode: stream
- port: 8081
mode: snapshot
switch:
# DFRobot board IR illumination control pin.
- platform: gpio
name: "${friendly_name} IR LED"
pin: GPIO47
restore_mode: ALWAYS_OFF
# Flip exposure/gain back to automatic for A/B comparison without a reflash.
# Off (the default) = manual exposure per the esp32_camera block above.
- platform: template
name: "${friendly_name} Auto Exposure"
optimistic: true
restore_mode: ALWAYS_OFF
turn_on_action:
- lambda: |-
sensor_t *s = esp_camera_sensor_get();
if (!s) return;
s->set_exposure_ctrl(s, 1);
s->set_gain_ctrl(s, 1);
turn_off_action:
- lambda: |-
sensor_t *s = esp_camera_sensor_get();
if (!s) return;
s->set_exposure_ctrl(s, 0);
s->set_gain_ctrl(s, 0);
sensor:
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${friendly_name} Uptime"
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
button:
- platform: restart
name: "${friendly_name} Restart"
text_sensor:
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
# Dynamic controls exposed to Home Assistant for real-time camera tuning
number:
# The two that actually drive image brightness in manual mode.
- platform: template
name: "${friendly_name} Exposure"
min_value: 0
max_value: 1200
step: 25
initial_value: 1200
optimistic: true
set_action:
- lambda: |-
sensor_t *s = esp_camera_sensor_get();
if (s) s->set_aec_value(s, (int)x);
- platform: template
name: "${friendly_name} Gain"
min_value: 0
max_value: 30
step: 1
initial_value: 30
optimistic: true
set_action:
- lambda: |-
sensor_t *s = esp_camera_sensor_get();
if (s) s->set_agc_gain(s, (int)x);
- platform: template
name: "${friendly_name} Brightness"
min_value: -2
max_value: 2
step: 1
initial_value: 2
optimistic: true
set_action:
- lambda: |-
sensor_t *s = esp_camera_sensor_get();
if (s) s->set_brightness(s, (int)x);
- platform: template
name: "${friendly_name} Contrast"
min_value: -2
max_value: 2
step: 1
initial_value: -2
optimistic: true
set_action:
- lambda: |-
sensor_t *s = esp_camera_sensor_get();
if (s) s->set_contrast(s, (int)x);
- platform: template
name: "${friendly_name} AE Level"
min_value: -2
max_value: 2
step: 1
initial_value: 2
optimistic: true
set_action:
- lambda: |-
sensor_t *s = esp_camera_sensor_get();
if (s) s->set_ae_level(s, (int)x);
select:
# Caps how far AGC may amplify. Higher = brighter but noisier; back this off
# if the chamber image gets too grainy at 128X.
- platform: template
name: "${friendly_name} Gain Ceiling"
optimistic: true
initial_option: "128X"
options:
- "2X"
- "4X"
- "8X"
- "16X"
- "32X"
- "64X"
- "128X"
set_action:
- lambda: |-
sensor_t *s = esp_camera_sensor_get();
if (!s) return;
int idx = 6;
if (x == "2X") idx = 0;
else if (x == "4X") idx = 1;
else if (x == "8X") idx = 2;
else if (x == "16X") idx = 3;
else if (x == "32X") idx = 4;
else if (x == "64X") idx = 5;
s->set_gainceiling(s, (gainceiling_t)idx);
-140
View File
@@ -1,140 +0,0 @@
substitutions:
device_name: saturn4-printer-cam
hostname: "Saturn 4 Printer Cam"
friendly_name: "Saturn 4 Printer Cam"
device_description: ESP32-CAM module in Saturn 4 Ultra Resin printer.
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32dev
framework:
type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "3Zxo91fAfA7wpZ4DAYFxGarCRIGQr+0rUJH2taJo7ds="
ota:
- platform: esphome
password: "d2e50b2ad36ed7de4ecbfd90725765e3"
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Esp32-Saturn4-Cam"
password: "klRzTYuqPJXm"
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: false # Set to true if you use static IPs and don't need discovery
# Diagnostic sensors
sensor:
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
text_sensor:
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
captive_portal:
# Example configuration entry
i2c:
- id: camera_i2c
sda: GPIO26
scl: GPIO27
psram:
mode: quad
speed: 80MHz
# Flashlight # you can control this flashlight within Homeassistant or mqtt to shine on the birds. use it with an timer!
output:
- platform: ledc
pin: GPIO4
id: gpio_4
channel: 2
## GPIO_4 is the flash light pin
light:
- platform: monochromatic
output: gpio_4
name: $hostname light
restore_mode: RESTORE_AND_OFF
icon: mdi:flash
esp32_camera:
external_clock:
pin: GPIO0
frequency: 20MHz
i2c_id: camera_i2c
data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
vsync_pin: GPIO25
href_pin: GPIO23
pixel_clock_pin: GPIO22
power_down_pin: GPIO32
resolution: 640x480 # you can use: [1600x1200 , 1280x1024 , 1024x768 , 800x600, 640x480 ] for best performance (FPS) use 1024 of lower resolution.
jpeg_quality: 12 #The JPEG quality that the camera should encode images with. From 10 (best) to 63 (worst)
aec2: true
ae_level: 2
brightness: 2 #The brightness to apply to the picture, default 0
contrast: 2
saturation: -2
vertical_flip: False
# Image settings
name: ${device_name}
# ...
esp32_camera_web_server:
- port: 80
mode: STREAM
- port: 8080
mode: SNAPSHOT
+1
View File
@@ -0,0 +1 @@
{}
+32
View File
@@ -0,0 +1,32 @@
# Keep this file safe: it contains the unique keys for this screen.
# Wi-Fi comes from the secrets.yaml of ESPHome Device Builder.
substitutions:
DEVICE_NAME: "family-room-control"
DEVICE_FRIENDLY_NAME: "Family Room Control"
LANGUAGE: "en"
DISPLAY_MODEL: "ST7789V"
GITHUB_REF: "screens-v0.4.93"
esphome:
name: "family-room-control"
friendly_name: "Family Room Control"
packages:
display:
url: https://github.com/MaxGramser/homeassistant_espscreen
ref: screens-v0.4.93
files: [packages/cyd.yaml]
refresh: 0s
local_overrides: !include family-room-control.local.yaml
api:
encryption:
key: "T0Qgy1qdHCXD0IiO72V0AqUb1WL3F9pOQ/zg2RlG9sg="
ota:
- platform: esphome
password: "zPMI1ZoPpQIu8yulgUiUTi8pPT8UClg9"
allow_partition_access: true
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
-127
View File
@@ -1,127 +0,0 @@
# Lights-only first version for the Family Room remote.
# Entity IDs came from the project's HA snapshot; verify them in live HA.
substitutions:
device_name: family-room-remote
friendly_name: Family Room Remote
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
packages:
hardware: !include panel-remote-common.yaml
binary_sensor:
- platform: homeassistant
id: standing_lamp_state
entity_id: light.living_room_lamp_1
publish_initial_state: true
on_state:
- lvgl.widget.update:
id: standing_lamp_button
state:
checked: !lambda return x;
- platform: homeassistant
id: small_lamp_state
entity_id: light.small_family_room_lamp
publish_initial_state: true
on_state:
- lvgl.widget.update:
id: small_lamp_button
state:
checked: !lambda return x;
- platform: homeassistant
id: tv_stand_state
entity_id: light.family_room_tv_stand
publish_initial_state: true
on_state:
- lvgl.widget.update:
id: tv_stand_button
state:
checked: !lambda return x;
lvgl:
on_idle:
- timeout: 45s
then:
- light.turn_on:
id: panel_backlight
brightness: 15%
pages:
- id: room_page
widgets:
- label:
x: 12
y: 8
text: Family Room
- button:
id: standing_lamp_button
x: 10
y: 45
width: 145
height: 75
checkable: true
widgets:
- label:
align: CENTER
text: Standing Lamp
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.living_room_lamp_1
- button:
id: small_lamp_button
x: 165
y: 45
width: 145
height: 75
checkable: true
widgets:
- label:
align: CENTER
text: Small Lamp
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.small_family_room_lamp
- button:
id: tv_stand_button
x: 10
y: 135
width: 145
height: 75
checkable: true
widgets:
- label:
align: CENTER
text: TV Stand
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.family_room_tv_stand
- button:
x: 165
y: 135
width: 145
height: 75
widgets:
- label:
align: CENTER
text: All Off
on_click:
- homeassistant.action:
action: light.turn_off
data:
entity_id: light.living_room_lamp_1
- homeassistant.action:
action: light.turn_off
data:
entity_id: light.small_family_room_lamp
- homeassistant.action:
action: light.turn_off
data:
entity_id: light.family_room_tv_stand
+11
View File
@@ -76,6 +76,17 @@ light:
# Effects - you can enable these for more features!
effects:
- strobe:
name: "Red Flash"
colors:
- state: true
brightness: 100%
red: 100%
green: 0%
blue: 0%
duration: 500ms
- state: false
duration: 250ms
# Basic effects
- random:
name: "Random"
-198
View File
@@ -1,198 +0,0 @@
substitutions:
device_name: led-controller-polly-a1m
friendly_name: "Polly's LED Controller"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32-c3-devkitm-1
framework:
type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: !secret api_encryption_key
# Enable over-the-air updates
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.69.181
gateway: 192.168.69.1
subnet: 255.255.255.0
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "${friendly_name} Fallback"
password: !secret fallback_password
# Prevents aggressive reconnection attempts
power_save_mode: none # Use 'light' for battery devices
# Slower but more reliable connection
fast_connect: true
# Handle connection failures gracefully
on_connect:
- logger.log: "Wi-Fi connected!"
on_disconnect:
- logger.log: "Wi-Fi disconnected!"
# Reduce mDNS traffic
mdns:
disabled: false # Set to true if you use static IPs and don't need discovery
#captive_portal:
web_server:
port: 80
# LED Light configuration
light:
- platform: esp32_rmt_led_strip
rgb_order: GRB
chipset: WS2812
pin: GPIO7
num_leds: 10
name: "${friendly_name}"
id: led_strip
# Default color on boot
restore_mode: RESTORE_DEFAULT_OFF
# Effects - you can enable these for more features!
effects:
# Basic effects
- random:
name: "Random"
transition_length: 5s
update_interval: 7s
- strobe:
name: "Strobe"
colors:
- state: true
brightness: 100%
red: 100%
green: 100%
blue: 100%
duration: 500ms
- state: false
duration: 250ms
- flicker:
name: "Flicker"
alpha: 95%
intensity: 1.5%
# Rainbow effects
- addressable_rainbow:
name: "Rainbow"
speed: 10
width: 50
- addressable_color_wipe:
name: "Color Wipe"
colors:
- red: 100%
green: 0%
blue: 0%
num_leds: 1
- red: 0%
green: 100%
blue: 0%
num_leds: 1
- red: 0%
green: 0%
blue: 100%
num_leds: 1
add_led_interval: 100ms
reverse: false
- addressable_scan:
name: "Scan"
move_interval: 100ms
scan_width: 3
- addressable_twinkle:
name: "Twinkle"
twinkle_probability: 5%
progress_interval: 4ms
- addressable_fireworks:
name: "Fireworks"
update_interval: 32ms
spark_probability: 10%
use_random_color: true
fade_out_rate: 120
# Pulse effect
- pulse:
name: "Pulse"
transition_length: 1s
update_interval: 1s
# Sensors for monitoring
sensor:
# WiFi Signal Strength
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 60s
# Uptime
- platform: uptime
name: "${friendly_name} Uptime"
# Diagnostic sensors
- platform: wifi_signal
name: "${device_name} WiFi Signal"
update_interval: 60s
- platform: uptime
name: "${device_name} Uptime"
update_interval: 60s
# Text sensors
text_sensor:
# ESPHome version
- platform: version
name: "${friendly_name} ESPHome Version"
# WiFi info
- platform: wifi_info
ip_address:
name: "${device_name} IP Address"
ssid:
name: "${device_name} Connected SSID"
bssid:
name: "${device_name} BSSID"
# Button to restart ESP32
button:
- platform: restart
name: "${friendly_name} Restart"
# Watchdog to auto-reboot if things go wrong
interval:
- interval: 5min
then:
- if:
condition:
and:
- lambda: 'return millis() > 300000;' # >5 min uptime
- not:
wifi.connected:
then:
- logger.log: "Wi-Fi not connected for 5 min, rebooting..."
- delay: 10s
- lambda: 'App.safe_reboot();'
-98
View File
@@ -1,98 +0,0 @@
# Lights-only first version for the Living Room remote.
# Entity IDs came from the project's HA snapshot; verify them in live HA.
substitutions:
device_name: living-room-remote
friendly_name: Living Room Remote
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
packages:
hardware: !include panel-remote-common.yaml
binary_sensor:
- platform: homeassistant
id: ceiling_light_state
entity_id: light.living_room_light_2
publish_initial_state: true
on_state:
- lvgl.widget.update:
id: ceiling_light_button
state:
checked: !lambda return x;
- platform: homeassistant
id: small_lamp_state
entity_id: light.small_living_room_lamp
publish_initial_state: true
on_state:
- lvgl.widget.update:
id: small_lamp_button
state:
checked: !lambda return x;
lvgl:
on_idle:
- timeout: 45s
then:
- light.turn_on:
id: panel_backlight
brightness: 15%
pages:
- id: room_page
widgets:
- label:
x: 12
y: 8
text: Living Room
- button:
id: ceiling_light_button
x: 10
y: 45
width: 145
height: 75
checkable: true
widgets:
- label:
align: CENTER
text: Main Light
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.living_room_light_2
- button:
id: small_lamp_button
x: 165
y: 45
width: 145
height: 75
checkable: true
widgets:
- label:
align: CENTER
text: Small Lamp
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.small_living_room_lamp
- button:
x: 10
y: 135
width: 300
height: 75
widgets:
- label:
align: CENTER
text: All Lights Off
on_click:
- homeassistant.action:
action: light.turn_off
data:
entity_id: light.living_room_light_2
- homeassistant.action:
action: light.turn_off
data:
entity_id: light.small_living_room_lamp
+1
View File
@@ -0,0 +1 @@
{}
+32
View File
@@ -0,0 +1,32 @@
# Keep this file safe: it contains the unique keys for this screen.
# Wi-Fi comes from the secrets.yaml of ESPHome Device Builder.
substitutions:
DEVICE_NAME: "living-room"
DEVICE_FRIENDLY_NAME: "Living Room"
LANGUAGE: "en"
DISPLAY_MODEL: "ST7789V"
GITHUB_REF: "screens-v0.4.93"
esphome:
name: "living-room"
friendly_name: "Living Room"
packages:
display:
url: https://github.com/MaxGramser/homeassistant_espscreen
ref: screens-v0.4.93
files: [packages/cyd.yaml]
refresh: 0s
local_overrides: !include living-room.local.yaml
api:
encryption:
key: "UPxExwkTfV//nSJtp+kcRkf11KFVsW4BSV0b/Nyyus8="
ota:
- platform: esphome
password: "ODRQoZGsPXZ_A-js0Sn3S9MNYPqEM8on"
allow_partition_access: true
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
+2 -1
View File
@@ -24,7 +24,7 @@ api:
ota:
- platform: esphome
password: !secret ota_password
encryption:
wifi:
ssid: !secret wifi_iot_ssid
@@ -87,6 +87,7 @@ touchscreen:
- platform: ft5x06
id: panel_touch
i2c_id: touch_bus
address: 0x38
display: panel_display
interrupt_pin: GPIO48
on_touch:
+2 -2
View File
@@ -18,7 +18,7 @@ api:
ota:
- platform: esphome
password: !secret ota_password
encryption:
wifi:
ssid: !secret wifi_iot_ssid
@@ -44,7 +44,6 @@ display:
dc_pin: GPIO18
reset_pin: GPIO3
dimensions: 240x320
rotation: 90
data_rate: 8MHz
invert_colors: true
auto_clear_enabled: false
@@ -73,6 +72,7 @@ touchscreen:
- platform: ft5x06
id: panel_touch
i2c_id: touch_bus
address: 0x38
display: panel_display
interrupt_pin: GPIO48
on_touch:
+363
View File
@@ -0,0 +1,363 @@
substitutions:
device_name: resin-curing-timer
outlet_entity: switch.resin_uv_light # Home Assistant UV light outlet.
display_model: ESP32-2432S028-7789 # 2.8" CYD with ST7789V (USB-C + micro-USB).
esphome:
name: ${device_name}
friendly_name: Resin Curing Timer
min_version: 2026.9.0
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
api:
id: ha_api
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
encryption:
key: !secret api_encryption_key
wifi:
ssid: !secret wifi_iot_ssid
password: !secret wifi_password
spi:
- id: tft_spi
clk_pin: GPIO14
mosi_pin: GPIO13
- id: touch_spi
clk_pin: GPIO25
mosi_pin: GPIO32
miso_pin: GPIO39
output:
- platform: ledc
pin: GPIO21
id: backlight_output
light:
- platform: monochromatic
output: backlight_output
name: Backlight
id: backlight
restore_mode: ALWAYS_ON
default_transition_length: 0s
display:
- platform: mipi_spi
id: tft
model: ${display_model}
spi_id: tft_spi
dimensions: 240x320 # Native portrait size; LVGL rotates to 320x240 landscape.
auto_clear_enabled: false
update_interval: never
touchscreen:
- platform: xpt2046
id: touch
spi_id: touch_spi
display: tft
cs_pin: GPIO33
interrupt_pin: GPIO36
threshold: 400
# Starting calibration only: verify all four corners on your board.
calibration:
x_min: 200
x_max: 3900
y_min: 200
y_max: 3900
transform:
swap_xy: true
mirror_x: true
on_touch:
- logger.log:
format: "Touch x=%d y=%d raw_x=%d raw_y=%d"
args: [touch.x, touch.y, touch.x_raw, touch.y_raw]
time:
- platform: homeassistant
id: ha_time
text_sensor:
- platform: homeassistant
id: timer_state
entity_id: timer.resin_curing
internal: true
- platform: homeassistant
id: outlet_state
entity_id: ${outlet_entity}
internal: true
- platform: homeassistant
id: timer_duration
entity_id: timer.resin_curing
attribute: duration
internal: true
# Keep epoch seconds as text: ESPHome numeric sensors use float32, which
# loses minute-level precision for contemporary Unix timestamps.
- platform: homeassistant
id: deadline
entity_id: sensor.resin_curing_deadline
internal: true
script:
- id: request_start
parameters:
minutes: int
then:
- if:
condition:
api.connected:
state_subscription_only: true
then:
- homeassistant.event:
event: esphome.resin_curing_timer
data:
controller: ${device_name}
command: start
minutes: !lambda return to_string(minutes);
- id: request_stop
then:
- if:
condition:
api.connected:
state_subscription_only: true
then:
- homeassistant.event:
event: esphome.resin_curing_timer
data:
controller: ${device_name}
command: stop
# Only Home Assistant's reported state drives the status and countdown.
interval:
- interval: 1s
then:
- lvgl.label.update:
id: status_label
text: !lambda |-
if (!id(ha_api).is_connected_with_state_subscription())
return std::string("Home Assistant offline");
if (!id(outlet_state).has_state() ||
(id(outlet_state).state != "on" && id(outlet_state).state != "off"))
return std::string("Outlet unavailable");
if (id(outlet_state).state == "on") {
if (id(timer_state).state == "active")
return std::string("UV light ON | Time remaining");
return std::string("UV light ON | No active timer");
}
return std::string("UV light OFF | Choose a time");
- lvgl.label.update:
id: countdown_label
text: !lambda |-
if (!id(ha_api).is_connected_with_state_subscription())
return std::string("--:--");
if (!id(timer_state).has_state() ||
(id(timer_state).state != "active" && id(timer_state).state != "idle"))
return std::string("--:--");
if (id(timer_state).state == "idle") {
if (!id(ha_time).now().is_valid()) return std::string("--:--");
return id(ha_time).now().strftime("%H:%M");
}
const char *raw = id(deadline).state.c_str();
char *tail;
const double end = strtod(raw, &tail);
if (!id(ha_time).now().is_valid() || tail == raw || *tail != '\0' ||
!std::isfinite(end) || end <= 0)
return std::string("Syncing...");
const int remaining = std::max(0, static_cast<int>(
end - id(ha_time).now().timestamp));
char buffer[16];
snprintf(buffer, sizeof(buffer), "%02d:%02d", remaining / 60, remaining % 60);
return std::string(buffer);
# Highlight only the preset confirmed by Home Assistant, never a local tap.
- lvgl.widget.update:
id: preset_30
state:
checked: !lambda |-
return id(ha_api).is_connected_with_state_subscription() &&
id(timer_state).state == "active" && id(outlet_state).state == "on" &&
id(timer_duration).state == "0:30:00";
- lvgl.widget.update:
id: preset_45
state:
checked: !lambda |-
return id(ha_api).is_connected_with_state_subscription() &&
id(timer_state).state == "active" && id(outlet_state).state == "on" &&
id(timer_duration).state == "0:45:00";
- lvgl.widget.update:
id: preset_60
state:
checked: !lambda |-
return id(ha_api).is_connected_with_state_subscription() &&
id(timer_state).state == "active" && id(outlet_state).state == "on" &&
id(timer_duration).state == "1:00:00";
- lvgl.widget.update:
id: preset_90
state:
checked: !lambda |-
return id(ha_api).is_connected_with_state_subscription() &&
id(timer_state).state == "active" && id(outlet_state).state == "on" &&
id(timer_duration).state == "1:30:00";
lvgl:
displays: [tft]
rotation: 90 # Landscape; LVGL also rotates touch coordinates.
touchscreens: [touch]
buffer_size: 25%
bg_color: 0x1C2638
text_color: 0xD6E8F0
default_font: montserrat_16
theme:
button:
bg_color: 0x7ED4E6
bg_opa: COVER
radius: 30
width: 60
height: 60
border_width: 0
shadow_width: 0
pad_all: 0
text_color: 0x102A3A
pressed:
bg_color: 0xC4F4FA
checked:
bg_color: 0xF7D264
text_color: 0x102A3A
pages:
- id: timer_page
bg_color: 0x1C2638
bg_opa: COVER
pad_all: 0
scrollbar_mode: "OFF"
widgets:
- label:
text: "RESIN CURING"
x: 14
y: 20
text_font: montserrat_14
- button:
id: stop_button
x: 242
y: 10
width: 64
height: 34
radius: 17
bg_color: 0x343044
text_color: 0xF7C0BE
pressed:
bg_color: 0x793E48
widgets:
- label:
text: "STOP"
align: CENTER
text_font: montserrat_12
on_click:
- script.execute: request_stop
- label:
id: status_label
text: "Connecting..."
x: 0
y: 114
width: 320
text_align: CENTER
text_font: montserrat_12
text_color: 0xBED1DF
- label:
id: countdown_label
text: "--:--"
x: 0
y: 49
width: 320
text_align: CENTER
text_font: montserrat_48
text_color: 0xA7EFF5
# All four time presets share one horizontal row on the 320x240 page.
- button:
id: preset_30
x: 10
y: 150
widgets:
- label:
text: "30"
align: CENTER
text_font: montserrat_24
on_click:
- script.execute:
id: request_start
minutes: 30
- label:
text: "minutes"
x: 10
y: 217
width: 60
text_align: CENTER
text_font: montserrat_12
- button:
id: preset_45
x: 90
y: 150
widgets:
- label:
text: "45"
align: CENTER
text_font: montserrat_24
on_click:
- script.execute:
id: request_start
minutes: 45
- label:
text: "minutes"
x: 90
y: 217
width: 60
text_align: CENTER
text_font: montserrat_12
- button:
id: preset_60
x: 170
y: 150
widgets:
- label:
text: "60"
align: CENTER
text_font: montserrat_24
on_click:
- script.execute:
id: request_start
minutes: 60
- label:
text: "minutes"
x: 170
y: 217
width: 60
text_align: CENTER
text_font: montserrat_12
- button:
id: preset_90
x: 250
y: 150
widgets:
- label:
text: "90"
align: CENTER
text_font: montserrat_24
on_click:
- script.execute:
id: request_start
minutes: 90
- label:
text: "minutes"
x: 250
y: 217
width: 60
text_align: CENTER
text_font: montserrat_12
+6 -6
View File
@@ -1,6 +1,6 @@
# Timer helpers
# Example:
# bedtime_timer:
# name: Bedtime Timer
# duration: "00:30:00"
# icon: mdi:timer
# Used by esphome/resin-curing-timer.yaml and its companion HA package.
resin_curing:
name: Resin Curing
duration: "00:30:00"
icon: mdi:timer-outline
restore: true
+161
View File
@@ -0,0 +1,161 @@
# School dates transcribed from the supplied Blackhawk 2026-2027 calendar.
# Early-dismissal days are school days. May 4-6 are closed for kindergarten
# only and are not excluded from the older children's schedule.
template:
# Fetch overlapping events together; calendar.message exposes only one event.
- triggers:
- trigger: homeassistant
event: start
- trigger: event
event_type: event_template_reloaded
- trigger: time
at: '00:00:00'
- trigger: time_pattern
minutes: '/15'
- trigger: state
entity_id: calendar.family_events
actions:
- variables:
requested_day: "{{ now().date().isoformat() }}"
agenda: {}
- action: calendar.get_events
continue_on_error: true
target:
entity_id: calendar.family_events
data:
start_date_time: "{{ today_at('00:00') }}"
end_date_time: "{{ today_at('00:00') + timedelta(days=1) }}"
response_variable: agenda
- condition: template
value_template: >-
{{ agenda is defined and agenda is mapping
and 'calendar.family_events' in agenda
and 'events' in agenda['calendar.family_events'] }}
sensor:
- name: Family Day Events
unique_id: family_day_events
state: "{{ requested_day }}"
attributes:
refreshed_at: "{{ now().isoformat() }}"
events: "{{ agenda['calendar.family_events']['events'] }}"
- binary_sensor:
- name: Family Schedule Ready
unique_id: family_schedule_ready
state: >-
{{ has_value('calendar.family_events')
and is_state('sensor.family_day_events', now().date().isoformat())
and as_timestamp(now())
- as_timestamp(state_attr('sensor.family_day_events', 'refreshed_at'), 0) < 1200 }}
- name: Day Camp Today
unique_id: family_day_camp_today
availability: "{{ is_state('binary_sensor.family_schedule_ready', 'on') }}"
state: >-
{% set ns = namespace(camp=false) %}
{% for event in state_attr('sensor.family_day_events', 'events') or [] %}
{% set summary = event.summary | default('', true) | lower %}
{% if summary | regex_search('\\bcamp\\b')
and not summary | regex_search('\\b(no|cancelled|canceled)\\s+(day\\s+)?camp\\b|\\bcamp\\s+(is\\s+)?(cancelled|canceled)\\b') %}
{% set ns.camp = true %}
{% endif %}
{% endfor %}
{{ ns.camp }}
- name: Family No School Today
unique_id: family_no_school_today
availability: "{{ is_state('binary_sensor.family_schedule_ready', 'on') }}"
state: >-
{% set ns = namespace(no_school=false) %}
{% for event in state_attr('sensor.family_day_events', 'events') or [] %}
{% set summary = event.summary | default('', true) | lower %}
{% if summary | regex_search('\\b(no school|school closed|snow day)\\b') %}
{% set ns.no_school = true %}
{% endif %}
{% endfor %}
{{ ns.no_school }}
- name: Girls At Home
unique_id: family_girls_at_home
availability: "{{ is_state('binary_sensor.family_schedule_ready', 'on') }}"
state: >-
{% set ns = namespace(home=false) %}
{% for event in state_attr('sensor.family_day_events', 'events') or [] %}
{% if event.summary | default('', true) | trim | lower == 'girls at home' %}
{% set start = as_datetime(event.start | default(''), none) %}
{% set end = as_datetime(event.end | default(''), none) %}
{% if start is not none and end is not none %}
{# All-day dates have no timezone; interpret them as local dates. #}
{% if start.tzinfo is none %}
{% set start = start.replace(tzinfo=now().tzinfo) %}
{% endif %}
{% if end.tzinfo is none %}
{% set end = end.replace(tzinfo=now().tzinfo) %}
{% endif %}
{% if start <= now() < end %}
{% set ns.home = true %}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{{ ns.home }}
- name: Blackhawk Scheduled School Day
unique_id: blackhawk_scheduled_school_day
# Do not silently extend an old district calendar into a new school year.
availability: >-
{{ '2026-08-01' <= now().date().isoformat() <= '2027-07-31' }}
state: >-
{% set day = now().date().isoformat() %}
{% set closed = [
'2026-09-07', '2026-10-12', '2026-11-11',
'2026-11-24', '2026-11-25', '2026-11-26', '2026-11-27', '2026-11-30',
'2026-12-23', '2026-12-24', '2026-12-25',
'2026-12-28', '2026-12-29', '2026-12-30', '2026-12-31',
'2027-01-01', '2027-01-18', '2027-02-15',
'2027-03-25', '2027-03-26', '2027-03-29', '2027-05-07'
] %}
{{ '2026-08-19' <= day <= '2027-05-27'
and now().weekday() < 5 and day not in closed }}
attributes:
school_year: '2026-2027'
first_day: '2026-08-19'
last_day: '2027-05-27'
calendar_valid_until: '2027-07-31'
- name: Blackhawk School Day
unique_id: blackhawk_school_day
# Scheduled closures remain known even if the family calendar is down.
# On regular school days, require fresh data for snow-day overrides.
availability: >-
{{ has_value('binary_sensor.blackhawk_scheduled_school_day')
and (is_state('binary_sensor.blackhawk_scheduled_school_day', 'off')
or has_value('binary_sensor.family_no_school_today')) }}
state: >-
{{ is_state('binary_sensor.blackhawk_scheduled_school_day', 'on')
and is_state('binary_sensor.family_no_school_today', 'off') }}
- name: Kids Summer Schedule Day
unique_id: kids_summer_schedule_day
availability: "{{ has_value('binary_sensor.blackhawk_school_day') }}"
state: >-
{% set day = now().date().isoformat() %}
{{ now().weekday() < 5
and ('2026-08-01' <= day < '2026-08-19'
or '2027-05-27' < day <= '2027-07-31') }}
automation:
- id: family_schedule_sync_legacy_camp_helper
alias: Family Schedule - Sync Day Camp Helper
description: Keep the existing camp helper compatible with the shared calendar sensor.
triggers:
- trigger: state
entity_id: binary_sensor.day_camp_today
to:
- 'on'
- 'off'
actions:
- action: "{{ 'input_boolean.turn_on' if is_state('binary_sensor.day_camp_today', 'on') else 'input_boolean.turn_off' }}"
target:
entity_id: input_boolean.day_camp_today
mode: restart
+117
View File
@@ -0,0 +1,117 @@
# Companion to esphome/resin-curing-timer.yaml.
# Keep the outlet and controller IDs in sync with that file's substitutions.
# The timer helper is defined in helpers/timer.yaml.
template:
- sensor:
- name: Resin Curing Deadline
default_entity_id: sensor.resin_curing_deadline
unique_id: resin_curing_deadline
icon: mdi:timer-sand
# The ESPHome display expects Unix epoch seconds, rather than an ISO date.
state: >-
{{ as_timestamp(state_attr('timer.resin_curing', 'finishes_at'), 0) | int
if is_state('timer.resin_curing', 'active') else 0 }}
automation:
- id: resin_curing_timer_control
alias: Resin Curing Timer - Control UV Light
description: >-
Handle the CYD presets and STOP button, turn off the UV outlet when the
timer ends, and reconcile the timer and outlet after restart or recovery.
triggers:
- trigger: event
event_type: esphome.resin_curing_timer
event_data:
controller: resin-curing-timer
id: command
- trigger: state
entity_id: timer.resin_curing
to:
- idle
- paused
id: timer_stopped
- trigger: event
event_type: timer.finished
event_data:
entity_id: timer.resin_curing
id: timer_finished
- trigger: state
entity_id: switch.resin_uv_light
to:
- "on"
- "off"
id: outlet_changed
- trigger: homeassistant
event: start
id: startup
- trigger: event
event_type: automation_reloaded
id: reload
# Retry a missed shutoff once the outlet recovers, including after downtime.
- trigger: time_pattern
seconds: "/30"
id: reconcile
actions:
- choose:
- conditions:
- condition: template
value_template: >-
{{ trigger.id == 'command'
and trigger.event.data.get('command') == 'start'
and trigger.event.data.get('minutes') | string
in ['30', '45', '60', '90']
and states('switch.resin_uv_light') in ['on', 'off'] }}
sequence:
# Arm the shutoff before requesting power. Each preset replaces
# the remaining time, matching the duration shown on the CYD.
- action: timer.start
target:
entity_id: timer.resin_curing
data:
duration: "{{ (trigger.event.data.minutes | int) * 60 }}"
- action: switch.turn_on
target:
entity_id: switch.resin_uv_light
- conditions:
- condition: template
value_template: >-
{{ trigger.id == 'command'
and trigger.event.data.get('command') == 'stop' }}
sequence:
- action: switch.turn_off
continue_on_error: true
target:
entity_id: switch.resin_uv_light
- action: timer.cancel
target:
entity_id: timer.resin_curing
default:
# Ignore malformed commands. Recheck current state when a queued
# event runs so an old expiry cannot stop a newly started session.
- condition: template
value_template: "{{ trigger.id != 'command' }}"
- choose:
- conditions:
- condition: template
value_template: >-
{{ not is_state('timer.resin_curing', 'active')
and not is_state('switch.resin_uv_light', 'off') }}
sequence:
- action: switch.turn_off
continue_on_error: true
target:
entity_id: switch.resin_uv_light
- conditions:
- condition: state
entity_id: timer.resin_curing
state: active
- condition: state
entity_id: switch.resin_uv_light
state: "off"
sequence:
# A physical/HA outlet shutoff ends the displayed countdown.
- action: timer.cancel
target:
entity_id: timer.resin_curing
mode: queued
max: 20
+35 -12
View File
@@ -1,13 +1,13 @@
check_day_camp:
alias: Check Day Camp
sequence:
- condition: template
value_template: "{{ has_value('binary_sensor.day_camp_today') }}"
- choose:
- conditions:
- condition: template
value_template: >-
{{ states('calendar.family_events') == 'on' and
'camp' in (state_attr('calendar.family_events', 'message') | lower) and
'no' not in (state_attr('calendar.family_events', 'message') | lower) }}
- condition: state
entity_id: binary_sensor.day_camp_today
state: 'on'
sequence:
- service: input_boolean.turn_on
target:
@@ -17,16 +17,12 @@ check_day_camp:
target:
entity_id: input_boolean.day_camp_today
- service: system_log.write
data:
message: >
Calendar Check: State={{ states('calendar.family_events') }},
Message={{ state_attr('calendar.family_events', 'message')
| default('') }}
level: debug
mode: restart
notify_events_alerts:
alias: Notify.Events Alerts
mode: queued
max: 20
fields:
message:
required: true
@@ -42,3 +38,30 @@ notify_events_alerts:
title: "{{ title | default('Home Assistant') }}"
data:
token: !secret notify_event_alerts_token
family_announcement:
alias: Family Announcement
description: Deliver family announcements in order, with common Alexa settings.
mode: queued
max: 20
fields:
targets:
required: true
selector:
entity:
domain: media_player
multiple: true
message:
required: true
selector:
text: {}
delivery:
description: Optional Alexa delivery options, such as type and method.
selector:
object: {}
sequence:
- action: notify.alexa_media
data:
target: "{{ targets }}"
message: "{{ message }}"
data: "{{ delivery | default({}) }}"