feat: add purpose-built bedroom and kitchen kiosks

pull/1898/head
Carlo Costanzo 2 months ago
parent 89d12ee1e7
commit 4feee5caf6

@ -8,6 +8,10 @@
# -------------------------------------------------------------------
# Notes:
# Avoid trigger entity string-splitting; map trigger IDs safely.
# Lightning may turn on the lights here, but its safety speech is owned by
# the Lightning Notification automation to prevent back-to-back briefings.
# Kitchen Show illuminance confirms the occupied first floor is actually dim;
# weather behavior falls back unchanged if that MQTT sensor is unavailable.
######################################################################
- alias: 'Dark House Little extra light'
@ -24,6 +28,10 @@
- platform: numeric_state
entity_id: sensor.blitzortung_lightning_counter
above: 1
- platform: numeric_state
entity_id: sensor.kitchen_show_ambient_light
below: 30
for: "00:05:00"
- platform: state
entity_id: group.family
to: 'home'
@ -44,6 +52,11 @@
- condition: numeric_state
entity_id: sensor.blitzortung_lightning_counter
above: 1
- condition: template
value_template: >-
{% set lux = states('sensor.kitchen_show_ambient_light') %}
{{ lux in ['unknown', 'unavailable', 'none', '']
or lux | float(0) < 35 }}
- condition: state
entity_id: binary_sensor.sleepnumber_carlo_carlo_is_in_bed
state: 'off'
@ -66,21 +79,27 @@
brightness: 255
color_temp_kelvin: 5500
- service: script.speech_engine
data:
value1: >-
{% set reason_map = {
'sensor.pirateweather_precip': 'rain',
'sensor.pirateweather_precip_intensity': 'heavy rain',
'sensor.blitzortung_lightning_counter': 'lightning',
'group.family': 'rain and clouds',
'binary_sensor.sleepnumber_carlo_carlo_is_in_bed': 'rain and clouds'
} %}
{% set reason = reason_map.get(trigger.entity_id | default(''), 'weather changes') %}
Because of the {{ reason }} outside. I will turn on some extra lights in the living room.
call_outside_weather: 1
call_window_check: 1
call_garage_check: 1
- choose:
- conditions:
- condition: template
value_template: >-
{{ trigger.entity_id | default('') != 'sensor.blitzortung_lightning_counter' }}
sequence:
- service: script.speech_engine
data:
value1: >-
{% set reason_map = {
'sensor.pirateweather_precip': 'rain',
'sensor.pirateweather_precip_intensity': 'heavy rain',
'sensor.kitchen_show_ambient_light': 'rain and clouds',
'group.family': 'rain and clouds',
'binary_sensor.sleepnumber_carlo_carlo_is_in_bed': 'rain and clouds'
} %}
{% set reason = reason_map.get(trigger.entity_id | default(''), 'weather changes') %}
Because of the {{ reason }} outside. I will turn on some extra lights in the living room.
call_outside_weather: 1
call_window_check: 1
call_garage_check: 1
- delay:
hours: 3

@ -10,6 +10,8 @@
# Notes: Uses color temperature only to avoid inherited color scenes.
# TV playback keeps the sink off so media time does not re-light it.
# Notes: Office lamp switch off-gates the sink while active PC work is in progress.
# Notes: Kitchen Show illuminance gates only new automatic turn-ons. Once the
# sink is on, its normal circadian tuning continues without lux-based shutoff.
######################################################################
- alias: "Kitchen Sink Circadian Daylight"
@ -59,6 +61,11 @@
- platform: time_pattern
minutes: "/30"
id: tune
- platform: numeric_state
entity_id: sensor.kitchen_show_ambient_light
below: 30
for: "00:05:00"
id: tune
- platform: homeassistant
event: start
id: tune
@ -82,6 +89,10 @@
{{ carlo_awake or stacey_awake }}
working_active: >-
{{ is_state('switch.office_lamp_switch', 'on') }}
ambient_dark: >-
{% set lux = states('sensor.kitchen_show_ambient_light') %}
{{ lux in ['unknown', 'unavailable', 'none', '']
or lux | float(0) < 35 }}
target_kelvin: >-
{% set elevation = state_attr('sun.sun', 'elevation') | float(-90) %}
{% if elevation <= -2 %}
@ -111,7 +122,8 @@
and not (tv_active | bool)
and not (working_active | bool)
and ((awake_home | bool) or is_state('light.sink', 'on'))
and (is_state('light.sink', 'on') or now().hour >= 4) }}
and (is_state('light.sink', 'on')
or ((ambient_dark | bool) and now().hour >= 4)) }}
- choose:
- conditions:
- condition: template

@ -28,14 +28,10 @@ homeassistant:
default_config:
# Temporary Rheem EcoNet/ClearBlade TLS workaround.
# Remove when https://github.com/CCOSTAN/Home-AssistantConfig/issues/1820 is resolved.
additional_ca:
rheem_econet_digicert_g1: DigiCertGlobalRootCA.crt.pem
frontend:
themes: !include_dir_merge_named themes
extra_module_url:
- /hacsfiles/custom-sidebar/custom-sidebar-plugin.js?v=16.0.0
- /local/custom-sidebar-loader.js?v=1.0.0
# -------------------------------------------------------------------
# Lovelace (YAML dashboards/resources)
@ -59,13 +55,20 @@ lovelace:
show_in_sidebar: true
require_admin: true
filename: dashboards/infrastructure/dashboard.yaml
dashboard-kiosk:
dashboard-bedroom:
mode: yaml
title: "Bedroom"
icon: mdi:bed-outline
show_in_sidebar: false
require_admin: false
filename: dashboards/bedroom/dashboard.yaml
dashboard-kiosk-satellite-staging:
mode: yaml
title: "KIOSK"
title: "Kiosk Satellite Preview"
icon: mdi:tablet-dashboard
show_in_sidebar: true
show_in_sidebar: false
require_admin: false
filename: dashboards/kiosk/dashboard.yaml
filename: dashboards/kiosk_satellite_staging/dashboard.yaml
config:
@ -96,8 +99,13 @@ cloud:
- light.upstairs_lamp
- light.sink
- camera.frontdoorbell
- camera.poolcam
- camera.frontlawn
- camera.driveway
- camera.garagecam
- camera.kidsgate
- camera.bedroomgate
- camera.filtergate
- camera.birdseye
- switch.kitchen_accent_1
- switch.kitchen_accent_2
@ -130,10 +138,20 @@ cloud:
name: Living Room Slider
camera.frontdoorbell:
name: Front Doorbell
camera.poolcam:
name: Pool Camera
camera.frontlawn:
name: Front Lawn Camera
camera.driveway:
name: Driveway
camera.garagecam:
name: Garage Camera
camera.kidsgate:
name: Kids Gate Camera
camera.bedroomgate:
name: Bedroom Gate Camera
camera.filtergate:
name: Filter Gate Camera
camera.birdseye:
name: Birdseye

@ -55,19 +55,29 @@ This folder holds YAML-managed Home Assistant Lovelace dashboards and UI resourc
- Primary actions cover garage doors, the front-door lock, thermostats, household alerts, and entry-point status.
- Packages opens the doorbell camera; Systems opens the desktop control center; destination views place a large Back to Home control at the top.
### Kiosk Camera Dashboard
### Event-Driven Kiosk Dashboards
- Logic and recovery package: [![YAML source: kiosk_tablet](https://img.shields.io/static/v1?label=YAML&message=kiosk_tablet&color=lightgrey&logo=github&logoColor=181717)](../packages/kiosk_tablet.yaml)
- Dashboard entrypoint: [![YAML source: dashboard](https://img.shields.io/static/v1?label=YAML&message=dashboard&color=lightgrey&logo=github&logoColor=181717)](kiosk/dashboard.yaml)
- Camera view: [![YAML source: 01_kiosk_oveview](https://img.shields.io/static/v1?label=YAML&message=01_kiosk_oveview&color=lightgrey&logo=github&logoColor=181717)](kiosk/views/01_kiosk_oveview.yaml)
- Camera sections: [![YAML source: kiosk_oveview_sections](https://img.shields.io/static/v1?label=YAML&message=kiosk_oveview_sections&color=lightgrey&logo=github&logoColor=181717)](kiosk/partials/kiosk_oveview_sections.yaml)
- Bedroom dashboard: [![YAML source: dashboard](https://img.shields.io/static/v1?label=YAML&message=bedroom&color=lightgrey&logo=github&logoColor=181717)](bedroom/dashboard.yaml)
- Bedroom landscape view: [![YAML source: 01_home](https://img.shields.io/static/v1?label=YAML&message=01_home&color=lightgrey&logo=github&logoColor=181717)](bedroom/views/01_home.yaml)
- Android 5 renderer: [![HTML source: bedroom-kiosk](https://img.shields.io/static/v1?label=HTML&message=bedroom-kiosk&color=lightgrey&logo=html5&logoColor=E34F26)](../www/bedroom-kiosk.html)
- Kiosk Satellite preview: [![YAML source: dashboard](https://img.shields.io/static/v1?label=YAML&message=kiosk_satellite_staging&color=lightgrey&logo=github&logoColor=181717)](kiosk_satellite_staging/dashboard.yaml)
- Kitchen tablet view: [![YAML source: 01_home](https://img.shields.io/static/v1?label=YAML&message=01_home&color=lightgrey&logo=github&logoColor=181717)](kiosk_satellite_staging/views/01_home.yaml)
- Automatic day/night theme: [![YAML source: kiosk_satellite](https://img.shields.io/static/v1?label=YAML&message=kiosk_satellite_theme&color=lightgrey&logo=github&logoColor=181717)](../themes/kiosk_satellite.yaml)
The displays share only the reusable event and screen-policy state they need. Kitchen keeps its clock/current-weather/forecast banner and rotating household context, including the next confirmed cruise countdown, while the fixed-landscape Bedroom dashboard omits the redundant clock and uses that space for the next wake alarm, security/weather context, larger Front Lawn and Driveway feeds, and a readable household alert. Both use the same Garage-then-Front-Door event priority. The canonical Bedroom Lovelace view and its Android 5 lightweight renderer consume the same camera, header, and alert sensors; the renderer bypasses only the unsupported Home Assistant frontend on the frozen Fire WebView. Kitchen uses native Kiosk Satellite camera overlays and idle dimming; Home Assistant handles only household sleep/away enforcement, volume, and event-camera wake instead of periodically forcing the screen on. True idle panel-off remains disabled until the Crown LineageOS device has a reliable motion-wake input. Its device schedule changes the Home Assistant theme between light at 07:00 and dark at 19:00. Other clients follow their own system preference.
### How it is wired into Home Assistant
This folder is referenced from `config/configuration.yaml` via:
- `lovelace.resource_mode: yaml`
- `lovelace.resources: !include dashboards/resources.yaml`
- `lovelace.dashboards: ...`
- `frontend.themes: !include_dir_merge_named themes` loads the shared tablet's light and dark palette.
- Default Overview YAML dashboard: `lovelace.dashboards.lovelace.filename: ui-lovelace.yaml`
- Additional YAML dashboards: `filename: dashboards/<dashboard>/dashboard.yaml`
- `dashboard-bedroom` is the canonical Bedroom desktop-review and future-hardware route; it is hidden from the sidebar and linked from the Bedroom Tablet snapshot on Systems' Kiosks tab. The current Android 5 Fire uses the equivalent lightweight renderer under `www/`.
- `dashboard-kiosk-satellite-staging` is the hidden validation surface for the shared Kiosk Satellite camera view.
- Systems' visible Kiosks tab sits immediately before Joanna and combines attention telemetry with the latest Bedroom Tablet and Kitchen Show screenshots; tapping either snapshot opens that display's live dashboard.
- The Systems Joanna page links to BearClaw Admin from Dispatch Trend and to a hidden AI Diagnostics subview that shows the latest OpenAI instructions and response together; LLM Vision production diagnostics and retired QMD health telemetry are not shown.
- Overview's second visible view is **Mobile**, a phone-first quick-control surface for garage doors, the front-door lock, thermostats, and compact household alerts.
- Overview Home uses ordered production sections under `overview/sections/`; approved pilot edits now apply directly there.
- Overview Home's **Windows** ribbon opens a dedicated GPIO entry-point subview with perimeter, MQTT, and individual contact status.

@ -3,11 +3,12 @@
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# KIOSK Dashboard - YAML entrypoint
# YAML-exported Lovelace dashboard (split into view files).
# Bedroom Dashboard - YAML entrypoint
# Dedicated fixed-landscape event-driven view for the Bedroom Fire tablet.
# -------------------------------------------------------------------
# Notes: Entry point for dashboard key `dashboard-kiosk`. Views are loaded from /config/dashboards/kiosk/views.
# Related Issue: 1875
# Notes: Presentation is Bedroom-specific; camera event priority is shared.
# Video: https://youtu.be/ChgEu0IDWzc
######################################################################
views: !include_dir_list /config/dashboards/kiosk/views
views: !include_dir_list /config/dashboards/bedroom/views

@ -0,0 +1,130 @@
######################################################################
# @CCOSTAN - Follow Me on X
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Bedroom Home Cards - Fixed 1024x600 landscape event display
# Shared camera priority with a Bedroom-specific read-only default view.
# -------------------------------------------------------------------
# Related Issue: 1875
# Notes: No current-time clock; both nightstands already provide visible clocks.
# Notes: Full-screen event cameras replace the default status/camera/alert bands.
# Video: https://youtu.be/ChgEu0IDWzc
######################################################################
- type: vertical-stack
cards:
- type: conditional
conditions:
- entity: sensor.kiosk_active_camera_view
state: garage
card:
type: picture-glance
title: Garage
camera_image: camera.garagecam
camera_view: auto
aspect_ratio: "2:1"
entities: []
- type: conditional
conditions:
- entity: sensor.kiosk_active_camera_view
state: front_door
card:
type: picture-glance
title: Front Door
camera_image: camera.frontdoorbell
camera_view: auto
aspect_ratio: "2:1"
entities: []
# Bedroom context is intentionally distinct from Kitchen's clock/forecast banner.
- type: conditional
conditions:
- entity: sensor.kiosk_active_camera_view
state: none
card:
type: markdown
content: >-
## {{ states('sensor.bedroom_kiosk_header') }}
{{ state_attr('sensor.bedroom_kiosk_header', 'detail') }}
card_mod:
style: |
ha-card {
height: 100px !important;
overflow: hidden !important;
}
ha-markdown {
padding: 12px 24px !important;
}
h2 {
font-size: 28px !important;
line-height: 1.15 !important;
margin: 0 0 8px !important;
}
p {
font-size: 18px !important;
line-height: 1.2 !important;
margin: 0 !important;
}
# Built-in camera cards are the legacy-WebView-safe rendering path.
- type: grid
columns: 2
square: false
cards:
- type: conditional
conditions:
- entity: sensor.kiosk_active_camera_view
state: none
card:
type: picture-glance
title: Front Lawn
camera_image: camera.frontlawn
camera_view: auto
aspect_ratio: "2:1"
entities: []
- type: conditional
conditions:
- entity: sensor.kiosk_active_camera_view
state: none
card:
type: picture-glance
title: Driveway
camera_image: camera.driveway
camera_view: auto
aspect_ratio: "2:1"
entities: []
# One large priority message remains readable from across the Bedroom.
- type: conditional
conditions:
- entity: sensor.kiosk_active_camera_view
state: none
card:
type: markdown
content: >-
## {{ states('sensor.kiosk_household_alert') }}
{{ state_attr('sensor.kiosk_household_alert', 'detail') }}
card_mod:
style: |
ha-card {
height: 120px !important;
overflow: hidden !important;
}
ha-markdown {
padding: 12px 24px !important;
}
h2 {
font-size: 26px !important;
line-height: 1.15 !important;
margin: 0 0 8px !important;
}
p {
font-size: 17px !important;
line-height: 1.25 !important;
margin: 0 !important;
}

@ -3,10 +3,10 @@
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Kiosk Partial - locator sections
# Bedroom Partial - locator sections
# Reusable list extracted from a view for smaller diffs and safer edits.
# -------------------------------------------------------------------
# Notes: Extracted from config/dashboards/kiosk/views/02_locator.yaml key `sections`.
# Notes: Preserved from the legacy kiosk dashboard during the Bedroom route split.
######################################################################
- cards:

@ -0,0 +1,23 @@
######################################################################
# @CCOSTAN - Follow Me on X
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Bedroom Home - Landscape night-watch display
# Wake alarm, exterior cameras, and household exceptions without a clock.
# -------------------------------------------------------------------
# Related Issue: 1875
# Notes: Garage takes priority, followed by Front Door, then Bedroom Home.
# Notes: Core cards remain compatible with the Fire tablet's legacy WebView.
# Video: https://youtu.be/ChgEu0IDWzc
######################################################################
title: Bedroom
path: home
icon: mdi:bed-outline
theme: kiosk_satellite_auto
type: panel
cards: !include /config/dashboards/bedroom/partials/home_cards.yaml
visible:
- user: be280a93c9d7416e98d25d0470f414be
- user: 9543feb5402742a0ab90f301f09f1c0e

@ -3,7 +3,7 @@
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# KIOSK View - Locator
# Bedroom View - Locator
# YAML-exported Lovelace dashboard (split into view files).
# -------------------------------------------------------------------
# Notes: Exported from config/.storage/lovelace.dashboard_kiosk view index 2.
@ -25,5 +25,5 @@ background:
visible:
- user: be280a93c9d7416e98d25d0470f414be
- user: 9543feb5402742a0ab90f301f09f1c0e
sections: !include /config/dashboards/kiosk/partials/locator_sections.yaml
sections: !include /config/dashboards/bedroom/partials/locator_sections.yaml
cards: []

@ -28,7 +28,7 @@
columns: full
layout_type: custom:grid-layout
layout:
grid-template-columns: repeat(9, minmax(0, 1fr))
grid-template-columns: repeat(10, minmax(0, 1fr))
grid-auto-flow: row
grid-auto-rows: min-content
grid-gap: 12px
@ -160,6 +160,19 @@
action: navigate
navigation_path: /dashboard-infrastructure/mariadb
- type: custom:button-card
template: bearstone_infra_status_kpi
entity: binary_sensor.kiosk_devices_need_attention
name: Kiosks
icon: mdi:tablet-dashboard
variables:
ok_state: 'off'
healthy_text: ONLINE
alert_text: ATTENTION
tap_action:
action: navigate
navigation_path: /dashboard-infrastructure/kiosks
- type: custom:button-card
template: bearstone_infra_status_kpi
entity: sensor.bearclaw_status_telemetry
@ -300,6 +313,27 @@
action: navigate
navigation_path: /dashboard-infrastructure/vacuum
- type: custom:button-card
template: bearstone_infra_alert_row
entity: binary_sensor.kiosk_devices_need_attention
name: Kiosk device needs attention
icon: mdi:tablet-alert
variables:
alert_kind: binary_on
exception_detail: >
[[[
const bedroom = states['binary_sensor.bedroom_kiosk_needs_attention']?.attributes?.status;
const kitchen = states['binary_sensor.kitchen_kiosk_needs_attention']?.attributes?.status;
const issues = [];
if (states['binary_sensor.bedroom_kiosk_needs_attention']?.state === 'on') issues.push(`BEDROOM: ${String(bedroom ?? 'ATTENTION').toUpperCase()}`);
if (states['binary_sensor.kitchen_kiosk_needs_attention']?.state === 'on') issues.push(`KITCHEN: ${String(kitchen ?? 'ATTENTION').toUpperCase()}`);
return issues.join(' · ') || 'KIOSK TELEMETRY REQUIRES REVIEW';
]]]
state_display: '[[[ return "ATTENTION"; ]]]'
tap_action:
action: navigate
navigation_path: /dashboard-infrastructure/kiosks
- type: custom:button-card
template: bearstone_infra_alert_row
entity: binary_sensor.proxmox1_runtime_healthy

@ -0,0 +1,192 @@
######################################################################
# @CCOSTAN - Follow Me on X
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Infrastructure Partial - kiosks
# Exception-first health, device telemetry, and live dashboard previews.
# -------------------------------------------------------------------
# Notes: Included by config/dashboards/infrastructure/views/05z_kiosks.yaml.
######################################################################
# -------------------------------------------------------------------
# Device health strip
# -------------------------------------------------------------------
- type: grid
column_span: 4
columns: 1
square: false
cards:
- type: custom:layout-card
grid_options:
columns: full
layout_type: custom:grid-layout
layout:
grid-template-columns: repeat(2, minmax(0, 1fr))
grid-auto-flow: row
grid-auto-rows: min-content
grid-gap: 12px
margin: 0
mediaquery:
"(max-width: 900px)":
grid-template-columns: repeat(1, minmax(0, 1fr))
cards:
- type: custom:button-card
template: bearstone_infra_status_kpi
entity: binary_sensor.bedroom_kiosk_needs_attention
name: Bedroom Tablet
icon: mdi:tablet
variables:
ok_state: 'off'
healthy_text: ONLINE
alert_text: NEEDS ATTENTION
state_display: >
[[[ return String(entity?.attributes?.status ?? entity?.state ?? 'unknown').toUpperCase(); ]]]
tap_action:
action: navigate
navigation_path: /dashboard-bedroom/home
- type: custom:button-card
template: bearstone_infra_status_kpi
entity: binary_sensor.kitchen_kiosk_needs_attention
name: Kitchen Show
icon: mdi:monitor-dashboard
variables:
ok_state: 'off'
healthy_text: ONLINE
alert_text: NEEDS ATTENTION
state_display: >
[[[ return String(entity?.attributes?.status ?? entity?.state ?? 'unknown').toUpperCase(); ]]]
tap_action:
action: navigate
navigation_path: /dashboard-kiosk-satellite-staging/home
# -------------------------------------------------------------------
# Latest device screenshots
# -------------------------------------------------------------------
- type: grid
column_span: 4
columns: 2
square: false
cards:
- camera_view: auto
type: picture-glance
title: Bedroom Tablet
image: https://demo.home-assistant.io/stub_config/kitchen.png
entities:
- entity: binary_sensor.bedroom_kiosk_needs_attention
image_entity: image.alarm_panel_1_screenshot
tap_action:
action: navigate
navigation_path: /dashboard-bedroom/home
- camera_view: auto
type: picture-glance
title: Kitchen Show
image: https://demo.home-assistant.io/stub_config/kitchen.png
entities:
- entity: binary_sensor.kitchen_kiosk_needs_attention
- entity: sensor.kitchen_show_last_screenshot
camera_image: camera.kitchen_show_screenshot
tap_action:
action: navigate
navigation_path: /dashboard-kiosk-satellite-staging/home
# -------------------------------------------------------------------
# Device telemetry
# -------------------------------------------------------------------
- type: grid
column_span: 4
columns: 2
square: false
cards:
- type: custom:vertical-stack-in-card
card_mod:
style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml
cards:
- type: custom:button-card
template: bearstone_infra_panel_header
name: Bedroom Tablet
- type: custom:button-card
template: bearstone_infra_list_row
entity: sensor.alarm_panel_1_battery
name: Battery
icon: mdi:battery
- type: custom:button-card
template: bearstone_infra_list_row
entity: binary_sensor.alarm_panel_1_plugged_in
name: Power
icon: mdi:power-plug
state_display: >
[[[ return entity?.state === 'on' ? 'PLUGGED IN' : 'ON BATTERY'; ]]]
- type: custom:button-card
template: bearstone_infra_list_row
entity: sensor.alarm_panel_1_current_page
name: Dashboard
icon: mdi:web
state_display: >
[[[
const page = String(entity?.state ?? '');
return page.includes('/local/bedroom-kiosk.html') ? 'PAGE OK' : 'CHECK PAGE';
]]]
- type: custom:button-card
template: bearstone_infra_list_row
entity: sensor.alarm_panel_1_foreground_app
name: Foreground App
icon: mdi:application-outline
state_display: >
[[[ return entity?.state === 'de.ozerov.fully' ? 'FULLY KIOSK' : String(entity?.state ?? 'UNKNOWN').toUpperCase(); ]]]
- type: custom:vertical-stack-in-card
card_mod:
style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml
cards:
- type: custom:button-card
template: bearstone_infra_panel_header
name: Kitchen Show
- type: custom:button-card
template: bearstone_infra_list_row
entity: sensor.kitchen_show_battery
name: Battery
icon: mdi:battery
- type: custom:button-card
template: bearstone_infra_list_row
entity: sensor.kitchen_show_last_seen
name: Last Seen
icon: mdi:clock-check-outline
- type: custom:button-card
template: bearstone_infra_list_row
entity: sensor.kitchen_show_current_page
name: Dashboard
icon: mdi:web
state_display: >
[[[
const page = String(entity?.state ?? '');
return page.includes('/dashboard-kiosk-satellite-staging/home') ? 'PAGE OK' : 'CHECK PAGE';
]]]
- type: custom:button-card
template: bearstone_infra_list_row
entity: update.kitchen_show_update
name: Kiosk Satellite
icon: mdi:update
state_display: >
[[[
const installed = entity?.attributes?.installed_version ?? 'unknown';
const latest = entity?.attributes?.latest_version ?? installed;
return entity?.state === 'on' ? `UPDATE ${latest}` : `CURRENT ${installed}`;
]]]
- type: custom:button-card
template: bearstone_infra_list_row
entity: sensor.kitchen_show_remote_admin
name: Remote Admin
icon: mdi:remote-desktop
state_display: AVAILABLE

@ -3,18 +3,19 @@
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Kiosk Partial - kiosk_oveview badges
# Reusable list extracted from a view for smaller diffs and safer edits.
# Infrastructure View - Kiosks
# Kiosk health, device telemetry, screenshots, and live-dashboard links.
# -------------------------------------------------------------------
# Notes: Extracted from config/dashboards/kiosk/views/01_kiosk_oveview.yaml key `badges`.
# Notes: Sorted immediately before Joanna in the Systems view navigation.
######################################################################
- type: entity
show_name: true
show_state: false
show_icon: true
entity: switch.alarm_panel_1_screensaver
tap_action:
action: toggle
show_entity_picture: false
name: Turn On ScreenSaver
title: Kiosks
path: kiosks
icon: mdi:tablet-dashboard
theme: default
type: sections
max_columns: 4
dense_section_placement: true
badges: []
sections: !include /config/dashboards/infrastructure/partials/kiosk_sections.yaml
cards: []

@ -1,67 +0,0 @@
######################################################################
# @CCOSTAN - Follow Me on X
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Kiosk Partial - kiosk_oveview sections
# Reusable list extracted from a view for smaller diffs and safer edits.
# -------------------------------------------------------------------
# Notes: Extracted from config/dashboards/kiosk/views/01_kiosk_oveview.yaml key `sections`.
# Keep this Fire 7 kiosk view on built-in cards; avoid live camera/custom-card resources on the old WebView.
# Video: https://youtu.be/ChgEu0IDWzc
######################################################################
- type: grid
cards:
- type: picture-glance
camera_image: camera.frontdoorbell
entities: []
grid_options:
columns: 12
rows: auto
column_span: 1
- type: grid
column_span: 1
cards:
- type: picture-glance
camera_image: camera.garagecam
entities: []
grid_options:
columns: 6
rows: auto
- type: picture-glance
camera_image: camera.driveway
entities: []
grid_options:
columns: 6
rows: auto
- type: picture-glance
camera_image: camera.kidsgate
entities: []
grid_options:
columns: 6
rows: auto
- type: picture-glance
camera_image: camera.frontlawn
entities: []
grid_options:
columns: 6
rows: auto
- type: picture-glance
camera_image: camera.bedroomgate
entities: []
grid_options:
columns: 6
rows: auto
- type: picture-glance
camera_image: camera.filtergate
entities: []
grid_options:
columns: 6
rows: auto
- type: picture-glance
camera_image: camera.poolcam
entities: []
grid_options:
columns: 6
rows: auto

@ -1,34 +0,0 @@
######################################################################
# @CCOSTAN - Follow Me on X
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# KIOSK View - Overview
# YAML-exported Lovelace dashboard (split into view files).
# -------------------------------------------------------------------
# Notes: Exported from config/.storage/lovelace.dashboard_kiosk view index 1.
# Video: https://youtu.be/ChgEu0IDWzc
######################################################################
title: Overview
path: kiosk_oveview
icon: mdi:cctv
type: sections
max_columns: 2
sections: !include /config/dashboards/kiosk/partials/kiosk_oveview_sections.yaml
badges: !include /config/dashboards/kiosk/partials/kiosk_oveview_badges.yaml
cards: []
visible:
- user: be280a93c9d7416e98d25d0470f414be
- user: 9543feb5402742a0ab90f301f09f1c0e
dense_section_placement: true
background:
opacity: 100
alignment: center
size: cover
repeat: no-repeat
attachment: fixed
image: /api/image/serve/caf7862354abd804ef658f899fb3218b/original
header:
layout: center
badges_position: top

@ -0,0 +1,14 @@
######################################################################
# @CCOSTAN - Follow Me on X
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Kiosk Satellite Staging Dashboard - Shared tablet camera surface
# Landscape-first Home Assistant dashboard for Kiosk Satellite devices.
# -------------------------------------------------------------------
# Notes: Hidden preview dashboard; the production Bedroom kiosk remains unchanged.
# Notes: Contains no tablet controls; Kiosk Satellite handles native camera overlays.
######################################################################
title: Kiosk Satellite Preview
views: !include_dir_list /config/dashboards/kiosk_satellite_staging/views

@ -0,0 +1,565 @@
######################################################################
# @CCOSTAN - Follow Me on X
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Kiosk Satellite Partial - Ambient kitchen display
# Enlarged camera feeds plus rotating household reminders, cruise countdown, and locations.
# -------------------------------------------------------------------
# Notes: Three-row landscape layout uses full-width clock and alert banners around the camera row.
# Notes: Camera cards use automatic streaming to limit idle tablet load.
# Notes: Native Garage and Front Door views take over for their matching live states.
# Notes: The lower panel is display-only and mirrors read-only household fact entities.
######################################################################
# Clock and camera wall
- type: grid
column_span: 4
columns: 1
square: false
cards:
- type: custom:layout-card
grid_options:
columns: full
layout_type: custom:grid-layout
layout:
grid-template-columns: repeat(2, minmax(0, 1fr))
grid-template-areas: |
"clock clock"
"frontlawn driveway"
"ambient ambient"
align-items: stretch
grid-gap: 10px
margin: 0
mediaquery:
"(max-width: 700px)":
grid-template-columns: 1fr
grid-template-areas: |
"clock"
"frontlawn"
"driveway"
"ambient"
cards:
# The established clock-weather-card supplies a live clock; button-card has no minute ticker.
- type: custom:clock-weather-card
entity: weather.pirateweather
sun_entity: sun.sun
weather_icon_type: fill
animated_icon: true
forecast_rows: 3
locale: en-US
time_pattern: h:mm
time_format: 12
date_pattern: DDDD
hide_today_section: false
hide_forecast_section: false
hide_clock: false
hide_date: false
use_browser_time: true
card_mod:
style: |
ha-card {
display: block !important;
height: 150px !important;
padding: 12px 24px !important;
box-sizing: border-box !important;
overflow: hidden !important;
}
ha-card > div {
display: grid !important;
grid-template-columns: minmax(0, 2fr) minmax(300px, 1fr);
align-items: center;
gap: 24px;
height: 126px;
}
clock-weather-card-today {
height: 126px !important;
}
clock-weather-card-forecast {
max-height: 126px;
overflow: hidden;
}
@media (max-width: 700px) {
ha-card > div {
grid-template-columns: 1fr;
}
clock-weather-card-forecast {
display: none;
}
}
view_layout:
grid-area: clock
# Built-in picture-glance is required because button-card cannot render HA camera streams.
- type: picture-glance
title: Driveway
camera_image: camera.driveway
camera_view: auto
aspect_ratio: "21:9"
entities: []
view_layout:
grid-area: driveway
- type: picture-glance
title: Front Lawn
camera_image: camera.frontlawn
camera_view: auto
aspect_ratio: "21:9"
entities: []
view_layout:
grid-area: frontlawn
# CSS animation rotates only the currently relevant, read-only household facts.
- type: custom:button-card
show_icon: false
show_name: false
show_state: false
tap_action:
action: none
hold_action:
action: none
double_tap_action:
action: none
triggers_update:
- binary_sensor.garage_phyn_battery_alert
- binary_sensor.garage_phyn_freeze_warning_alert
- binary_sensor.garage_phyn_high_pressure_alert
- binary_sensor.garage_phyn_leak_alert
- binary_sensor.garage_phyn_offline_leak_shutoff_alert
- binary_sensor.garage_phyn_pinhole_leak_alert
- binary_sensor.garage_phyn_recurring_flow_alert
- binary_sensor.garage_phyn_temperature_alert
- sensor.nws_alerts_alerts
- group.garage_doors
- group.entry_points
- group.locks
- binary_sensor.front_door_packages_present
- sensor.rheem_wh_available_hot_water
- input_boolean.garbage_cans_out
- input_datetime.water_delivery_date
- sensor.water_softener_salt_days_since_last_add
- sensor.water_softener_salt_average_days_between_refills
- sensor.spaceship_battery_level
- switch.spaceship_charge
- input_datetime.tesla_model_y_last_garage_close
- input_datetime.tesla_model_y_plug_in_snooze_until
- sensor.next_booked_cruise_countdown
- person.carlo
- person.stacey
- person.justin
- person.paige
- sensor.carlo_location_display
- sensor.stacey_location_display
- sensor.justin_location_display
- sensor.paige_location_display
custom_fields:
ambient: >
[[[
const state = (entityId) => states[entityId]?.state;
const valid = (value) => !['unknown', 'unavailable', 'none', '', undefined].includes(value);
const pretty = (value) => String(value || 'Unknown').replaceAll('_', ' ');
const dateKey = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const items = [];
const now = new Date();
const phynAlerts = [
'binary_sensor.garage_phyn_battery_alert',
'binary_sensor.garage_phyn_freeze_warning_alert',
'binary_sensor.garage_phyn_high_pressure_alert',
'binary_sensor.garage_phyn_leak_alert',
'binary_sensor.garage_phyn_offline_leak_shutoff_alert',
'binary_sensor.garage_phyn_pinhole_leak_alert',
'binary_sensor.garage_phyn_recurring_flow_alert',
'binary_sensor.garage_phyn_temperature_alert'
].filter((entityId) => state(entityId) === 'on');
if (phynAlerts.length > 0) {
const alertNames = phynAlerts.map((entityId) =>
states[entityId]?.attributes?.friendly_name || pretty(entityId.split('.')[1])
);
items.push({
kicker: 'WATER SYSTEM',
title: 'Phyn needs attention',
detail: alertNames.join(' · '),
icon: 'mdi:water-alert-outline',
accent: '#c62828'
});
}
const weatherAlertEntity = states['sensor.nws_alerts_alerts'];
const weatherAlertCount = Number.parseInt(weatherAlertEntity?.state, 10);
const weatherAlerts = Array.isArray(weatherAlertEntity?.attributes?.Alerts)
? weatherAlertEntity.attributes.Alerts
: [];
const cleanAlertText = (value) => String(value || '').replace(/\s+/g, ' ').trim();
const alertSection = (description, label) => {
const pattern = new RegExp(
`\\*\\s*${label}\\.{3}\\s*([\\s\\S]*?)(?=\\n\\s*\\n\\s*\\*\\s*[A-Z ]+\\.{3}|$)`,
'i'
);
return cleanAlertText(String(description || '').match(pattern)?.[1]);
};
const limitAlertText = (value, maxLength = 240) => {
const text = cleanAlertText(value);
if (text.length <= maxLength) return text;
return `${text.slice(0, maxLength - 3).replace(/\s+\S*$/, '')}...`;
};
const severityOrder = { Extreme: 0, Severe: 1, Moderate: 2, Minor: 3, Unknown: 4 };
const severityColors = {
Extreme: '#b71c1c',
Severe: '#c62828',
Moderate: '#d84315',
Minor: '#f9a825',
Unknown: '#546e7a'
};
const uniqueWeatherAlerts = weatherAlerts
.filter((alert, index, alerts) => {
const alertKey = alert.ID || alert.URL || `${alert.Event}|${alert.Expires}`;
return alerts.findIndex((candidate) =>
(candidate.ID || candidate.URL || `${candidate.Event}|${candidate.Expires}`) === alertKey
) === index;
})
.sort((left, right) =>
(severityOrder[left.Severity] ?? severityOrder.Unknown)
- (severityOrder[right.Severity] ?? severityOrder.Unknown)
);
if (uniqueWeatherAlerts.length > 0) {
uniqueWeatherAlerts.forEach((alert, index) => {
const severity = severityColors[alert.Severity] ? alert.Severity : 'Unknown';
const detailParts = [
alertSection(alert.Description, 'WHAT'),
alertSection(alert.Description, 'WHERE'),
alertSection(alert.Description, 'WHEN')
].filter(Boolean);
const detail = detailParts.length > 0
? detailParts.join(' · ')
: alert.Headline || alert.Description || 'Active National Weather Service alert';
const position = uniqueWeatherAlerts.length > 1
? ` ${index + 1}/${uniqueWeatherAlerts.length}`
: '';
items.push({
kicker: `WEATHER ALERT${position} · ${severity.toUpperCase()}`,
title: alert.Event || `${weatherAlertCount} active alerts`,
detail: limitAlertText(detail),
icon: 'mdi:weather-lightning-rainy',
accent: severityColors[severity]
});
});
} else if (Number.isFinite(weatherAlertCount) && weatherAlertCount > 0) {
items.push({
kicker: 'WEATHER ALERT',
title: `${weatherAlertCount} active ${weatherAlertCount === 1 ? 'alert' : 'alerts'}`,
detail: 'Alert details are temporarily unavailable',
icon: 'mdi:weather-lightning-rainy',
accent: severityColors.Unknown
});
}
if (valid(state('group.garage_doors')) && !['closed', 'off'].includes(state('group.garage_doors'))) {
const openGarages = ['cover.large_garage_door', 'cover.small_garage_door']
.filter((entityId) => valid(state(entityId)) && state(entityId) !== 'closed')
.map((entityId) => states[entityId]?.attributes?.friendly_name || pretty(entityId.split('.')[1]));
items.push({
kicker: 'GARAGE',
title: 'Garage door open',
detail: openGarages.join(' · ') || 'Check both garage doors',
icon: 'mdi:garage-open-variant',
accent: '#ef6c00'
});
}
if (state('group.entry_points') === 'on') {
const openEntries = (states['group.entry_points']?.attributes?.entity_id || [])
.filter((entityId) => state(entityId) === 'on')
.map((entityId) => states[entityId]?.attributes?.friendly_name || pretty(entityId.split('.')[1]));
const extraCount = Math.max(0, openEntries.length - 3);
const entryDetail = openEntries.slice(0, 3).join(' · ') + (extraCount ? ` · +${extraCount} more` : '');
items.push({
kicker: 'ENTRY POINT',
title: openEntries.length === 1 ? 'Door or window open' : `${openEntries.length} openings detected`,
detail: entryDetail || 'Check the exterior doors and windows',
icon: 'mdi:door-open',
accent: '#ef6c00'
});
}
if (valid(state('group.locks')) && state('group.locks') !== 'locked') {
items.push({
kicker: 'SECURITY',
title: 'Check the front door lock',
detail: `Lock group is ${pretty(state('group.locks'))}`,
icon: 'mdi:lock-alert-outline',
accent: '#c62828'
});
}
if (state('binary_sensor.front_door_packages_present') === 'on') {
items.push({
kicker: 'FRONT DOOR',
title: 'Package waiting',
detail: 'Remember to bring it inside',
icon: 'mdi:package-variant-closed',
accent: '#8e5a2b'
});
}
const hotWater = Number.parseFloat(state('sensor.rheem_wh_available_hot_water'));
if (Number.isFinite(hotWater) && hotWater < 30) {
items.push({
kicker: 'HOT WATER',
title: 'Hot water is running low',
detail: `${Math.round(hotWater)}% available — keep showers short`,
icon: 'mdi:water-thermometer-outline',
accent: '#0277bd'
});
}
const weekday = now.toLocaleDateString('en-US', {weekday: 'short'});
if (['Wed', 'Sun'].includes(weekday) && state('input_boolean.garbage_cans_out') !== 'on') {
items.push({
kicker: 'TODAY',
title: weekday === 'Wed' ? 'Garbage + recycling day' : 'Garbage day',
detail: 'Put the cans at the curb',
icon: 'mdi:trash-can-outline',
accent: '#2e7d32'
});
}
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
if (state('input_datetime.water_delivery_date') === dateKey(tomorrow)) {
items.push({
kicker: 'WATER DELIVERY TOMORROW',
title: 'Put the bottles out',
detail: 'Stage them before leaving the house',
icon: 'mdi:bottle-soda-classic-outline',
accent: '#0277bd'
});
}
const battery = Number.parseFloat(state('sensor.spaceship_battery_level'));
const lastClose = new Date(String(state('input_datetime.tesla_model_y_last_garage_close')).replace(' ', 'T'));
const snoozeUntil = new Date(String(state('input_datetime.tesla_model_y_plug_in_snooze_until')).replace(' ', 'T'));
const recentArrival = Number.isFinite(lastClose.getTime()) && (now.getTime() - lastClose.getTime()) < 43200000;
const snoozeExpired = !Number.isFinite(snoozeUntil.getTime()) || snoozeUntil <= now;
if (
Number.isFinite(battery) && battery < 50 && recentArrival && snoozeExpired &&
state('switch.spaceship_charge') === 'off' &&
state('person.carlo') === 'home' && state('person.stacey') === 'home'
) {
items.push({
kicker: 'MODEL Y',
title: 'Plug in the Tesla',
detail: `${Math.round(battery)}% battery after the latest garage arrival`,
icon: 'mdi:ev-plug-tesla',
accent: '#c62828'
});
}
const saltDays = Number.parseFloat(state('sensor.water_softener_salt_days_since_last_add'));
const saltAverage = Number.parseFloat(state('sensor.water_softener_salt_average_days_between_refills'));
if (
Number.isFinite(saltDays) && Number.isFinite(saltAverage) && saltAverage > 0 &&
saltDays >= saltAverage * 0.9
) {
items.push({
kicker: 'WATER SOFTENER',
title: 'Salt refill coming due',
detail: `${Math.round(saltDays)} days since the last refill`,
icon: 'mdi:shaker-outline',
accent: '#00838f'
});
}
const cruiseDays = Number.parseInt(state('sensor.next_booked_cruise_countdown'), 10);
const cruiseEntity = states['sensor.next_booked_cruise_countdown'];
const cruiseShip = String(cruiseEntity?.attributes?.ship || '').trim();
const cruiseDate = String(cruiseEntity?.attributes?.sail_date || '').trim();
if (Number.isInteger(cruiseDays) && cruiseDays >= 0 && cruiseShip && cruiseDate) {
const sailDate = new Date(`${cruiseDate}T12:00:00`);
const sailLabel = Number.isFinite(sailDate.getTime())
? sailDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
: cruiseDate;
const cruiseNights = Number.parseInt(cruiseEntity?.attributes?.nights, 10);
const cruisePort = String(cruiseEntity?.attributes?.port || '').trim();
const detail = [
`Departs ${sailLabel}`,
Number.isInteger(cruiseNights) && cruiseNights > 0
? `${cruiseNights} ${cruiseNights === 1 ? 'night' : 'nights'}`
: '',
cruisePort
].filter(Boolean).join(' · ');
items.push({
kicker: 'NEXT BOOKED CRUISE',
title: cruiseDays === 0
? `${cruiseShip} sails today`
: `${cruiseDays} ${cruiseDays === 1 ? 'day' : 'days'} until ${cruiseShip}`,
detail,
icon: 'mdi:ferry',
accent: '#00838f'
});
}
[
['Carlo', 'person.carlo', 'sensor.carlo_location_display'],
['Stacey', 'person.stacey', 'sensor.stacey_location_display'],
['Justin', 'person.justin', 'sensor.justin_location_display'],
['Paige', 'person.paige', 'sensor.paige_location_display']
].forEach(([name, personId, locationId]) => {
const personState = state(personId);
if (valid(personState) && personState !== 'home') {
const locationEntity = states[locationId];
const rawLocation = pretty(valid(state(locationId)) ? state(locationId) : personState);
const withoutName = rawLocation.replace(new RegExp(`^${name}\\s+`, 'i'), '');
const sinceMatch = withoutName.match(/\s*\(since\s+([^)]+)\)\s*$/i);
const location = withoutName.replace(/\s*\(since\s+[^)]+\)\s*$/i, '').trim();
const changedAt = new Date(locationEntity?.last_changed || states[personId]?.last_changed);
const since = sinceMatch?.[1] || (
Number.isFinite(changedAt.getTime())
? changedAt.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
: 'recently'
);
items.push({
kicker: 'FAMILY LOCATION',
title: `${name} - ${location || pretty(personState)}`,
detail: `Since ${since}`,
icon: 'mdi:map-marker-account-outline',
accent: '#6a1b9a'
});
}
});
if (items.length === 0) {
items.push({
kicker: 'HOUSEHOLD',
title: 'Everything is handled',
detail: 'Everyone is home and there are no active reminders',
icon: 'mdi:check-circle-outline',
accent: '#2e7d32'
});
}
const secondsPerSlide = 10;
const duration = items.length * secondsPerSlide;
const phase = Math.floor(now.getTime() / 1000) % duration;
const slot = 100 / items.length;
const fade = Math.min(4, slot * 0.18);
return html`
<style>
@keyframes kitchen-ambient-cycle {
0% { opacity: 0; transform: translateY(12px); }
${fade}% { opacity: 1; transform: translateY(0); }
${slot - fade}% { opacity: 1; transform: translateY(0); }
${slot}% { opacity: 0; transform: translateY(-12px); }
100% { opacity: 0; transform: translateY(-12px); }
}
.kitchen-ambient-rotator {
position: relative;
width: 100%;
height: 100%;
}
.kitchen-ambient-slide {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: 100px minmax(0, 1fr);
grid-template-areas: 'icon copy';
align-items: center;
gap: 24px;
padding: 8px 42px;
box-sizing: border-box;
opacity: 0;
animation-name: kitchen-ambient-cycle;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.kitchen-ambient-slide.single {
opacity: 1;
animation: none;
}
.kitchen-ambient-icon {
grid-area: icon;
display: grid;
place-items: center;
width: 88px;
height: 88px;
border-radius: 24px;
color: var(--accent);
background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.kitchen-ambient-icon ha-icon { --mdc-icon-size: 56px; }
.kitchen-ambient-copy {
grid-area: copy;
min-width: 0;
text-align: left;
}
.kitchen-ambient-kicker {
color: var(--accent);
font-size: 16px;
font-weight: 700;
letter-spacing: 0.12em;
}
.kitchen-ambient-title {
margin-top: 4px;
color: var(--primary-text-color);
font-size: 28px;
font-weight: 600;
line-height: 1;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
white-space: normal;
overflow-wrap: anywhere;
overflow: hidden;
}
.kitchen-ambient-detail {
margin-top: 4px;
color: var(--secondary-text-color);
font-size: 26px;
font-weight: 500;
line-height: 1.15;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
white-space: normal;
overflow-wrap: anywhere;
overflow: hidden;
}
</style>
<div class="kitchen-ambient-rotator">
${items.map((item, index) => html`
<div
class="kitchen-ambient-slide ${items.length === 1 ? 'single' : ''}"
style="--accent:${item.accent};animation-duration:${duration}s;animation-delay:${index * secondsPerSlide - phase}s"
>
<div class="kitchen-ambient-icon"><ha-icon .icon=${item.icon}></ha-icon></div>
<div class="kitchen-ambient-copy">
<div class="kitchen-ambient-kicker">${item.kicker}</div>
<div class="kitchen-ambient-title">${item.title}</div>
<div class="kitchen-ambient-detail">${item.detail}</div>
</div>
</div>
`)}
</div>
`;
]]]
styles:
card:
- height: 185px
- overflow: hidden
- border-radius: 18px
- background: linear-gradient(120deg, rgba(33, 150, 243, 0.08), rgba(103, 58, 183, 0.08))
grid:
- grid-template-areas: '"ambient"'
- grid-template-columns: 1fr
- grid-template-rows: 1fr
custom_fields:
ambient:
- width: 100%
- height: 100%
view_layout:
grid-area: ambient

@ -0,0 +1,22 @@
######################################################################
# @CCOSTAN - Follow Me on X
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Event-Driven Kiosk View - Home clock and cameras
# Shared landscape view for Kiosk Satellite and Fully Kiosk tablets.
# -------------------------------------------------------------------
# Related Issue: 1875
# Notes: The Kitchen Show is the reference implementation for this shared view.
# Notes: Device-specific kiosk controls stay out of the shared dashboard.
######################################################################
title: Home
path: home
icon: mdi:cctv
theme: kiosk_satellite_auto
type: sections
max_columns: 4
dense_section_placement: true
sections: !include /config/dashboards/kiosk_satellite_staging/partials/home_sections.yaml
cards: []

@ -5,8 +5,8 @@
# -------------------------------------------------------------------
# Infrastructure - Observability, disk pressure, and Joanna review workflows
# WAN/DNS/website/Docker host state normalized for dashboards, plus scheduled infrastructure reviews.
# Video: https://youtu.be/aceV7ObKJVA
# Blog: https://www.vcloudinfo.com/2026/07/using-codex-clean-up-home-assistant-entities.html
# Video: https://youtu.be/F-PpsKkzcrM
# Blog: https://www.vcloudinfo.com/2026/08/home-assistant-internet-speed-monitoring-ookla.html
# -------------------------------------------------------------------
# Related Issue: 1584
# Notes: Home dashboard consumes `infra_*` entities for exceptions-only alerts.
@ -235,7 +235,8 @@ template:
'binary_sensor.infra_dns_pihole_degraded',
'binary_sensor.infra_compute_degraded',
'binary_sensor.infra_storage_degraded',
'binary_sensor.infra_external_monitoring_degraded'
'binary_sensor.infra_external_monitoring_degraded',
'binary_sensor.kiosk_devices_need_attention'
] %}
{{ expand(categories) | selectattr('state', 'eq', 'on') | list | count }}
attributes:
@ -245,7 +246,8 @@ template:
'binary_sensor.infra_dns_pihole_degraded',
'binary_sensor.infra_compute_degraded',
'binary_sensor.infra_storage_degraded',
'binary_sensor.infra_external_monitoring_degraded'
'binary_sensor.infra_external_monitoring_degraded',
'binary_sensor.kiosk_devices_need_attention'
] %}
{{ expand(categories) | selectattr('state', 'eq', 'on') | map(attribute='entity_id') | list }}

@ -3,18 +3,31 @@
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Bedroom Kiosk Tablet - Fully Kiosk page recovery
# Keeps the Master Bedroom Fire tablet pinned to the kiosk dashboard.
# Kiosk Tablets - Fully recovery and Kiosk Satellite camera policy
# Keeps the Bedroom tablet stable and drives state-based Kitchen Show camera views.
# -------------------------------------------------------------------
# Notes: Baseline view path is the real typo-like `kiosk_oveview` slug.
# Notes: Uses Fully controls only; does not enable Fully Kiosk lockout mode.
# Related Issue: 1875
# Notes: `/dashboard-bedroom/home` remains the canonical desktop/future-hardware route.
# Notes: The Android 5 Fully tablet uses `/local/bedroom-kiosk.html`, a lightweight renderer.
# Notes: Shared camera, header, and alert sensors keep both Bedroom renderers DRY.
# Notes: Fully controls apply only to the legacy Bedroom tablet.
# Notes: Kitchen Show uses Kiosk Satellite discovery entities; no screensaver helper is required.
# Notes: Kitchen Show stays dark while the house is empty or asleep and wakes only for occupied-house events.
# Notes: Garage takes over while either door is moving/open; Front Door takes over while unlocked.
# Notes: New Kiosk Satellite releases dispatch Joanna to install and verify the Kitchen Show update.
# Notes: Wake events only restore brightness/focus; URL reloads require page drift.
# Notes: Fully auto-reload on screen-on/screensaver-stop is disabled to avoid motion wake reload loops.
# Notes: Startup recovery waits for HA's initial websocket state burst to settle.
# Notes: Brightness recovery only runs when both SleepIQ in-bed sensors are off.
# Notes: Fully availability/page/app/power plus Kitchen last-seen/update/screenshot freshness feed Systems telemetry.
# Video: https://youtu.be/ChgEu0IDWzc
######################################################################
input_text:
kitchen_show_kiosk_satellite_last_dispatched_version:
name: "Kitchen Show Kiosk Satellite Last Dispatched Version"
max: 32
automation:
- alias: "Bedroom Kiosk Tablet - Return to Kiosk View"
id: 11e50d2e-f66f-4fd0-86f4-bdca075c3a1c
@ -60,7 +73,7 @@ automation:
{% set bed_clear = is_state('binary_sensor.sleepnumber_carlo_carlo_is_in_bed', 'off')
and is_state('binary_sensor.sleepnumber_carlo_stacey_is_in_bed', 'off') %}
{% set invalid = ['unknown', 'unavailable', 'none', ''] %}
{{ (current_page not in invalid and '/dashboard-kiosk/kiosk_oveview' not in current_page)
{{ (current_page not in invalid and '/local/bedroom-kiosk.html' not in current_page)
or (foreground_app not in invalid and foreground_app != 'de.ozerov.fully')
or (bed_clear and brightness not in invalid and brightness | float(100) < 40) }}
@ -90,7 +103,7 @@ automation:
{{ previous_app not in invalid and previous_app != 'de.ozerov.fully' }}
page_drift: >-
{{ previous_page not in invalid
and '/dashboard-kiosk/kiosk_oveview' not in previous_page }}
and '/local/bedroom-kiosk.html' not in previous_page }}
- choose:
- conditions:
- condition: template
@ -130,3 +143,509 @@ automation:
Bedroom tablet reloaded after page drift from
{{ previous_page }} (foreground app: {{ previous_app }};
brightness: {{ previous_brightness }}).
- alias: "Kitchen Kiosk Satellite - State Camera Policy"
id: 4c3ddc5b-1fe2-4c11-a77c-e2c15d374e77
mode: restart
trigger:
- platform: state
entity_id: sensor.kiosk_active_camera_view
condition:
- condition: state
entity_id: binary_sensor.kitchen_kiosk_should_be_on
state: "on"
action:
- service: script.kitchen_kiosk_apply_camera_policy
- alias: "Kitchen Kiosk Satellite - Screen Policy"
id: 99d8d43b-9448-4916-b313-8117a038baaf
mode: restart
trigger:
- platform: state
entity_id: binary_sensor.kitchen_kiosk_should_be_on
to: "on"
- platform: state
entity_id: light.kitchen_show_screen
to: "on"
- platform: state
entity_id: binary_sensor.kitchen_kiosk_should_be_on
to: "off"
action:
- condition: template
value_template: >-
{{ states('binary_sensor.kitchen_kiosk_should_be_on') in ['on', 'off'] }}
- variables:
desired_volume: >-
{{ 10 if is_state('binary_sensor.kitchen_kiosk_should_be_on', 'on') else 0 }}
# Android reports the nearest hardware volume step (13% for a 10% request).
- if:
- condition: template
value_template: >-
{{ (states('number.kitchen_show_volume') | int(-1)
- desired_volume | int(-1)) | abs > 3 }}
then:
- service: number.set_value
target:
entity_id: number.kitchen_show_volume
data:
value: "{{ desired_volume | int(0) }}"
- choose:
- conditions:
- condition: state
entity_id: binary_sensor.kitchen_kiosk_should_be_on
state: "on"
sequence:
# Kiosk Satellite owns idle dimming. This event-driven dismiss
# wakes to the dashboard without a poller repeatedly forcing the
# panel on during an occupied day.
- service: button.press
target:
entity_id: button.kitchen_show_postpone_screensaver
- service: script.kitchen_kiosk_apply_camera_policy
default:
- service: button.press
target:
entity_id: button.kitchen_show_close_camera_view
- service: light.turn_off
target:
entity_id: light.kitchen_show_screen
- alias: "Kitchen Kiosk Satellite - Refresh Systems Snapshot"
id: e920cab1-59d3-4d45-8aad-67170127504f
mode: single
max_exceeded: silent
trigger:
- platform: event
event_type: homeassistant_started
id: startup
- platform: time_pattern
minutes: "/15"
id: periodic
action:
- delay: "{{ '00:02:00' if trigger.id == 'startup' else '00:00:00' }}"
- condition: template
value_template: >-
{% set last_seen = as_timestamp(states('sensor.kitchen_show_last_seen'), 0) %}
{{ last_seen > 0 and (as_timestamp(now()) - last_seen) < 300 }}
- service: button.press
target:
entity_id: button.kitchen_show_take_screenshot
- alias: "Kitchen Kiosk Satellite - Update Joanna Dispatch"
id: 199e30f0-e28c-4842-953d-a65bc4b4e1d3
description: "Dispatch Joanna when a new Kiosk Satellite release is available for the Kitchen Show."
mode: single
max_exceeded: silent
trigger:
- platform: state
entity_id: update.kitchen_show_update
from: "off"
to: "on"
for: "00:02:00"
condition:
- condition: template
value_template: >-
{% set installed = state_attr('update.kitchen_show_update', 'installed_version') | default('', true) | string %}
{% set latest = state_attr('update.kitchen_show_update', 'latest_version') | default('', true) | string %}
{{ installed not in ['', 'unknown', 'unavailable']
and latest not in ['', 'unknown', 'unavailable']
and installed != latest
and latest != states('input_text.kitchen_show_kiosk_satellite_last_dispatched_version') }}
action:
- variables:
installed_version: >-
{{ state_attr('update.kitchen_show_update', 'installed_version') | default('unknown', true) }}
latest_version: >-
{{ state_attr('update.kitchen_show_update', 'latest_version') | default('unknown', true) }}
release_url: >-
{{ state_attr('update.kitchen_show_update', 'release_url') | default('not provided', true) }}
structured_request: |-
KITCHEN_KIOSK_SATELLITE_UPDATE_REQUEST
target_device=Kitchen Show
update_entity=update.kitchen_show_update
installed_version={{ installed_version }}
target_version={{ latest_version }}
release_url={{ release_url }}
authorization=This dispatch authorizes installing the pending Kiosk Satellite application update on the Kitchen Show only. Do not update LineageOS, Home Assistant, or unrelated applications and do not factory-reset or re-enroll the tablet.
required_action=Inspect the live update entity and device availability, invoke the supported Home Assistant update.install action, and observe whether installation actually begins. If Android does not finish the installation, use the pre-authorized Joanna ops helper /opt/codex/bin/kitchen-kiosk-update --expected-version {{ latest_version }} to complete this same signed Kiosk Satellite update over the existing LAN-only management path.
required_verification=Do not stop at a successful service call. Verify update.kitchen_show_update reports installed_version equal to latest_version, in_progress is false, the Kitchen Show returns online, sensor.kitchen_show_current_page is the expected kiosk dashboard, sensor.kitchen_show_remote_admin is available, and a fresh screenshot can be requested.
failure_policy=If the signed release cannot be installed safely or the device does not recover, stop without changing unrelated software and report the blocking evidence.
- service: script.send_to_logbook
data:
topic: "KIOSK"
message: >-
Joanna dispatched to update Kitchen Show Kiosk Satellite from
{{ installed_version }} to {{ latest_version }}.
- service: script.joanna_dispatch
data:
trigger_context: "HA automation kitchen_kiosk_satellite_update_joanna_dispatch (Kitchen Kiosk Satellite - Update Joanna Dispatch)"
source: "home_assistant_automation.kitchen_kiosk_satellite_update_dispatch"
summary: >-
Kiosk Satellite {{ latest_version }} is available for Kitchen Show;
installed version is {{ installed_version }}.
request: "{{ structured_request }}"
entity_ids:
- update.kitchen_show_update
- sensor.kitchen_show_last_seen
- sensor.kitchen_show_current_page
- sensor.kitchen_show_remote_admin
- button.kitchen_show_take_screenshot
diagnostics: >-
update_state={{ states('update.kitchen_show_update') }};
in_progress={{ state_attr('update.kitchen_show_update', 'in_progress') }};
last_seen={{ states('sensor.kitchen_show_last_seen') }};
current_page={{ states('sensor.kitchen_show_current_page') }};
remote_admin={{ states('sensor.kitchen_show_remote_admin') }}
- service: input_text.set_value
target:
entity_id: input_text.kitchen_show_kiosk_satellite_last_dispatched_version
data:
value: "{{ latest_version }}"
template:
- sensor:
- name: "Kiosk Active Camera View"
unique_id: kiosk_active_camera_view
icon: mdi:camera-switch
state: >-
{% set garage_states = [
states('cover.large_garage_door'),
states('cover.small_garage_door')
] %}
{% if garage_states | select('in', ['opening', 'open', 'closing']) | list | count > 0 %}
garage
{% elif is_state('lock.front_door', 'unlocked') %}
front_door
{% else %}
none
{% endif %}
- name: "Bedroom Kiosk Header"
unique_id: bedroom_kiosk_header
icon: mdi:bed-clock
state: >-
{% set alarm = states('sensor.bedroom_next_wake_alarm') %}
{% set alarm_ts = as_timestamp(alarm, none) %}
{% set alarm_source = states('sensor.bedroom_next_wake_alarm_source')
| replace(' Bedroom', '') %}
{% if alarm_ts is not none %}
NEXT ALARM - {{ alarm_source }} · {{ alarm_ts | timestamp_custom('%a %-I:%M %p') }}
{% else %}
NO BEDROOM WAKE ALARM SET
{% endif %}
attributes:
detail: >-
{% set weather = states('weather.pirateweather') | replace('_', ' ') | title %}
{% set temperature = state_attr('weather.pirateweather', 'temperature') %}
{% set secure = is_state('group.garage_doors', 'closed')
and is_state('group.locks', 'locked')
and is_state('group.entry_points', 'off') %}
{{ 'House secured' if secure else 'Check house security' }} ·
{{ temperature | round(0) if temperature is number else '--' }}° · {{ weather }}
- name: "Kiosk Household Alert"
unique_id: kiosk_household_alert
icon: mdi:message-alert-outline
state: >-
{% set alerts = state_attr('sensor.nws_alerts_alerts', 'Alerts') | default([], true) %}
{% set hot_water = states('sensor.rheem_wh_available_hot_water') | float(100) %}
{% set tomorrow = (now() + timedelta(days=1)).strftime('%Y-%m-%d') %}
{% set battery = states('sensor.spaceship_battery_level') | float(100) %}
{% set ns = namespace(name='', raw='') %}
{% for name, person, location in [
('Carlo', 'person.carlo', 'sensor.carlo_location_display'),
('Stacey', 'person.stacey', 'sensor.stacey_location_display'),
('Justin', 'person.justin', 'sensor.justin_location_display'),
('Paige', 'person.paige', 'sensor.paige_location_display')
] %}
{% if not ns.name and states(person) not in ['home', 'unknown', 'unavailable'] %}
{% set ns.name = name %}
{% set ns.raw = states(location) if states(location) not in ['unknown', 'unavailable'] else states(person) %}
{% endif %}
{% endfor %}
{% if alerts | count > 0 %}
WEATHER ALERT - {{ alerts[0].Event | default('National Weather Service') }}
{% elif is_state('binary_sensor.front_door_packages_present', 'on') %}
PACKAGE AT THE FRONT DOOR
{% elif is_state('group.entry_points', 'on') %}
ENTRY POINT OPEN
{% elif states('group.locks') not in ['locked', 'unknown', 'unavailable'] %}
CHECK THE FRONT DOOR LOCK
{% elif hot_water < 30 %}
HOT WATER IS RUNNING LOW
{% elif now().strftime('%a') in ['Wed', 'Sun']
and not is_state('input_boolean.garbage_cans_out', 'on') %}
{{ 'GARBAGE + RECYCLING' if now().strftime('%a') == 'Wed' else 'GARBAGE' }} DAY
{% elif states('input_datetime.water_delivery_date') == tomorrow %}
WATER DELIVERY TOMORROW
{% elif battery < 50 and is_state('switch.spaceship_charge', 'off')
and is_state('person.carlo', 'home') and is_state('person.stacey', 'home') %}
PLUG IN THE TESLA
{% elif ns.name %}
{% set location = ns.raw | replace(ns.name ~ ' ', '') %}
{{ ns.name }} - {{ location.split(' (since ')[0] }}
{% else %}
EVERYTHING IS HANDLED
{% endif %}
attributes:
detail: >-
{% set alerts = state_attr('sensor.nws_alerts_alerts', 'Alerts') | default([], true) %}
{% set hot_water = states('sensor.rheem_wh_available_hot_water') | float(100) %}
{% set tomorrow = (now() + timedelta(days=1)).strftime('%Y-%m-%d') %}
{% set battery = states('sensor.spaceship_battery_level') | float(100) %}
{% set ns = namespace(name='', raw='') %}
{% for name, person, location in [
('Carlo', 'person.carlo', 'sensor.carlo_location_display'),
('Stacey', 'person.stacey', 'sensor.stacey_location_display'),
('Justin', 'person.justin', 'sensor.justin_location_display'),
('Paige', 'person.paige', 'sensor.paige_location_display')
] %}
{% if not ns.name and states(person) not in ['home', 'unknown', 'unavailable'] %}
{% set ns.name = name %}
{% set ns.raw = states(location) if states(location) not in ['unknown', 'unavailable'] else states(person) %}
{% endif %}
{% endfor %}
{% if alerts | count > 0 %}
{{ (alerts[0].Headline | default(alerts[0].Description, true))
| replace('\n', ' ') | truncate(300, true) }}
{% elif is_state('binary_sensor.front_door_packages_present', 'on') %}
Remember to bring it inside.
{% elif is_state('group.entry_points', 'on') %}
Check the exterior doors and windows.
{% elif states('group.locks') not in ['locked', 'unknown', 'unavailable'] %}
Lock status is {{ states('group.locks') | replace('_', ' ') }}.
{% elif hot_water < 30 %}
{{ hot_water | round(0) }}% is available - keep showers short.
{% elif now().strftime('%a') in ['Wed', 'Sun']
and not is_state('input_boolean.garbage_cans_out', 'on') %}
Put the cans at the curb.
{% elif states('input_datetime.water_delivery_date') == tomorrow %}
Put the empty bottles out.
{% elif battery < 50 and is_state('switch.spaceship_charge', 'off')
and is_state('person.carlo', 'home') and is_state('person.stacey', 'home') %}
Model Y battery is {{ battery | round(0) }}%.
{% elif ns.name %}
{% set parts = ns.raw.split(' (since ') %}
{{ 'Since ' ~ (parts[1] | replace(')', '')) if parts | count > 1 else 'Away from home' }}
{% else %}
No active household alerts or reminders.
{% endif %}
- binary_sensor:
- name: "Kitchen Kiosk Should Be On"
unique_id: kitchen_kiosk_should_be_on
icon: mdi:monitor-eye
state: >-
{% set guest = is_state('input_boolean.guest_mode', 'on') %}
{% set sitter = is_state('input_boolean.house_sitter_present', 'on') %}
{% set family_home = is_state('group.family', 'home') %}
{% set carlo_bed = is_state('binary_sensor.sleepnumber_carlo_carlo_is_in_bed', 'on') %}
{% set stacey_bed = is_state('binary_sensor.sleepnumber_carlo_stacey_is_in_bed', 'on') %}
{% set bedtime = (carlo_bed or stacey_bed)
and (carlo_bed or is_state('person.carlo', 'not_home'))
and (stacey_bed or is_state('person.stacey', 'not_home')) %}
{{ guest or sitter or (family_home and not bedtime) }}
attributes:
reason: >-
{% if is_state('input_boolean.guest_mode', 'on') %}
guest mode
{% elif is_state('input_boolean.house_sitter_present', 'on') %}
house sitter present
{% elif not is_state('group.family', 'home') %}
nobody home
{% elif is_state('binary_sensor.sleepnumber_carlo_carlo_is_in_bed', 'on')
and is_state('binary_sensor.sleepnumber_carlo_stacey_is_in_bed', 'on') %}
household asleep
{% elif is_state('binary_sensor.sleepnumber_carlo_carlo_is_in_bed', 'on')
and is_state('person.stacey', 'not_home') %}
Carlo asleep and Stacey away
{% elif is_state('binary_sensor.sleepnumber_carlo_stacey_is_in_bed', 'on')
and is_state('person.carlo', 'not_home') %}
Stacey asleep and Carlo away
{% else %}
household active
{% endif %}
- name: "Bedroom Kiosk Needs Attention"
unique_id: bedroom_kiosk_needs_attention
device_class: problem
icon: mdi:tablet-alert
delay_on: "00:02:00"
delay_off: "00:00:30"
state: >-
{% set invalid = ['unknown', 'unavailable', 'none', ''] %}
{% set page = states('sensor.alarm_panel_1_current_page') %}
{% set app = states('sensor.alarm_panel_1_foreground_app') %}
{% set battery = states('sensor.alarm_panel_1_battery') | float(0) %}
{{ page in invalid
or app in invalid
or '/local/bedroom-kiosk.html' not in page
or app != 'de.ozerov.fully'
or (battery < 20 and not is_state('binary_sensor.alarm_panel_1_plugged_in', 'on')) }}
attributes:
status: >-
{% set invalid = ['unknown', 'unavailable', 'none', ''] %}
{% set page = states('sensor.alarm_panel_1_current_page') %}
{% set app = states('sensor.alarm_panel_1_foreground_app') %}
{% set battery = states('sensor.alarm_panel_1_battery') | float(0) %}
{% if page in invalid or app in invalid %}
offline
{% elif '/local/bedroom-kiosk.html' not in page %}
page drift
{% elif app != 'de.ozerov.fully' %}
app drift
{% elif battery < 20 and not is_state('binary_sensor.alarm_panel_1_plugged_in', 'on') %}
low battery
{% else %}
online
{% endif %}
current_page: "{{ states('sensor.alarm_panel_1_current_page') }}"
foreground_app: "{{ states('sensor.alarm_panel_1_foreground_app') }}"
battery: "{{ states('sensor.alarm_panel_1_battery') }}"
- name: "Kitchen Kiosk Needs Attention"
unique_id: kitchen_kiosk_needs_attention
device_class: problem
icon: mdi:monitor-alert
delay_on: "00:02:00"
delay_off: "00:00:30"
state: >-
{% set invalid = ['unknown', 'unavailable', 'none', ''] %}
{% set last_seen = as_timestamp(states('sensor.kitchen_show_last_seen'), 0) %}
{% set screenshot = as_timestamp(states('sensor.kitchen_show_last_screenshot'), 0) %}
{% set page = states('sensor.kitchen_show_current_page') %}
{% set remote_admin = states('sensor.kitchen_show_remote_admin') %}
{% set battery = states('sensor.kitchen_show_battery') | float(0) %}
{% set offline = last_seen == 0 or (as_timestamp(now()) - last_seen) > 300 %}
{% set screenshot_stale = screenshot == 0 or (as_timestamp(now()) - screenshot) > 2700 %}
{{ offline
or page in invalid
or remote_admin in invalid
or '/dashboard-kiosk-satellite-staging/home' not in page
or (battery < 20 and not is_state('binary_sensor.kitchen_show_charging', 'on'))
or is_state('update.kitchen_show_update', 'on')
or screenshot_stale }}
attributes:
status: >-
{% set invalid = ['unknown', 'unavailable', 'none', ''] %}
{% set last_seen = as_timestamp(states('sensor.kitchen_show_last_seen'), 0) %}
{% set screenshot = as_timestamp(states('sensor.kitchen_show_last_screenshot'), 0) %}
{% set page = states('sensor.kitchen_show_current_page') %}
{% set remote_admin = states('sensor.kitchen_show_remote_admin') %}
{% set battery = states('sensor.kitchen_show_battery') | float(0) %}
{% if last_seen == 0 or (as_timestamp(now()) - last_seen) > 300 %}
offline
{% elif page in invalid or remote_admin in invalid %}
telemetry unavailable
{% elif '/dashboard-kiosk-satellite-staging/home' not in page %}
page drift
{% elif battery < 20 and not is_state('binary_sensor.kitchen_show_charging', 'on') %}
low battery
{% elif is_state('update.kitchen_show_update', 'on') %}
update available
{% elif screenshot == 0 or (as_timestamp(now()) - screenshot) > 2700 %}
screenshot stale
{% else %}
online
{% endif %}
last_seen: "{{ states('sensor.kitchen_show_last_seen') }}"
last_screenshot: "{{ states('sensor.kitchen_show_last_screenshot') }}"
current_page: "{{ states('sensor.kitchen_show_current_page') }}"
battery: "{{ states('sensor.kitchen_show_battery') }}"
- name: "Kiosk Devices Need Attention"
unique_id: kiosk_devices_need_attention
device_class: problem
icon: mdi:tablet-cellphone
state: >-
{{ is_state('binary_sensor.bedroom_kiosk_needs_attention', 'on')
or is_state('binary_sensor.kitchen_kiosk_needs_attention', 'on') }}
attributes:
status: >-
{% set bedroom = is_state('binary_sensor.bedroom_kiosk_needs_attention', 'on') %}
{% set kitchen = is_state('binary_sensor.kitchen_kiosk_needs_attention', 'on') %}
{% if bedroom and kitchen %}
Bedroom and Kitchen need attention
{% elif bedroom %}
Bedroom needs attention
{% elif kitchen %}
Kitchen needs attention
{% else %}
All kiosks online
{% endif %}
devices: >-
{% set ns = namespace(items=[]) %}
{% if is_state('binary_sensor.bedroom_kiosk_needs_attention', 'on') %}
{% set ns.items = ns.items + ['Bedroom'] %}
{% endif %}
{% if is_state('binary_sensor.kitchen_kiosk_needs_attention', 'on') %}
{% set ns.items = ns.items + ['Kitchen'] %}
{% endif %}
{{ ns.items }}
script:
kitchen_kiosk_apply_camera_policy:
alias: "Kitchen Kiosk - Apply Camera Policy"
mode: restart
variables:
desired_view: >-
{{ {'garage': 'Garage', 'front_door': 'Front Door'}.get(
states('sensor.kiosk_active_camera_view'), 'none') }}
desired_button: >-
{{ 'button.kitchen_show_show_garage'
if desired_view == 'Garage'
else 'button.kitchen_show_show_front_door' }}
sequence:
- choose:
- conditions:
- condition: template
value_template: "{{ desired_view in ['Garage', 'Front Door'] }}"
sequence:
- condition: template
value_template: >-
{{ states('sensor.kitchen_show_active_camera_view') != desired_view }}
- service: script.kitchen_kiosk_show_camera
data:
show_button: "{{ desired_button }}"
default:
- choose:
- conditions:
- condition: template
value_template: >-
{{ states('sensor.kitchen_show_active_camera_view')
not in ['none', 'unknown', 'unavailable'] }}
sequence:
- service: button.press
target:
entity_id: button.kitchen_show_close_camera_view
- service: button.press
target:
entity_id: button.kitchen_show_bring_to_front
kitchen_kiosk_show_camera:
alias: "Kitchen Kiosk - Show Camera"
mode: restart
fields:
show_button:
name: Camera view button
description: Kiosk Satellite MQTT button for the native camera view.
required: true
selector:
entity:
domain: button
sequence:
- service: light.turn_on
target:
entity_id: light.kitchen_show_screen
- service: button.press
target:
entity_id: button.kitchen_show_bring_to_front
- delay: "00:00:01"
- service: button.press
target:
entity_id: "{{ show_button }}"

@ -3,12 +3,33 @@
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Royal Caribbean Price Alerts - Webhook-driven drop notifications
# Uses Cruise Tracker webhook to notify via notify_engine + HA UI.
# Royal Caribbean - Price alerts and a sanitized booked-cruise countdown
# Uses Cruise Tracker for webhook alerts and a read-only Kitchen Show fact.
# -------------------------------------------------------------------
# Notes: Use script.notify_engine for app notifications.
# Notes: The REST endpoint omits confirmation, cabin, price, and watch-list data.
######################################################################
sensor:
- platform: rest
name: Next Booked Cruise Countdown
unique_id: next_booked_cruise_countdown
resource: !secret cruise_tracker_next_booked_url
method: GET
scan_interval: 3600
timeout: 20
value_template: >-
{% set cruise = value_json.next_booked_cruise | default(none) %}
{{ cruise.days_until if cruise is mapping and cruise.days_until is number else 'unknown' }}
json_attributes_path: "$.next_booked_cruise"
json_attributes:
- ship
- sail_date
- nights
- port
icon: mdi:ferry
unit_of_measurement: "days"
automation:
- alias: "Royal Caribbean Price Drop Alert"
id: royal_caribbean_price_drop_alert

@ -0,0 +1,29 @@
######################################################################
# @CCOSTAN - Follow Me on X
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Kiosk Satellite Theme - Automatic day and night palette
# Follows the Kiosk Satellite client theme schedule for shared kiosk views.
# -------------------------------------------------------------------
# Notes: Kitchen Show switches light/dark variants at 07:00/19:00 device time.
######################################################################
kiosk_satellite_auto:
modes:
light:
primary-background-color: "#f4f6f8"
secondary-background-color: "#ffffff"
card-background-color: "#ffffff"
primary-text-color: "#15181d"
secondary-text-color: "#5d6673"
divider-color: "rgba(0, 0, 0, 0.12)"
lovelace-background: "#f4f6f8"
dark:
primary-background-color: "#0b0f14"
secondary-background-color: "#111821"
card-background-color: "#151d27"
primary-text-color: "#f3f6fa"
secondary-text-color: "#aab6c5"
divider-color: "rgba(255, 255, 255, 0.12)"
lovelace-background: "#0b0f14"

@ -0,0 +1,336 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Bedroom Kiosk</title>
<style>
:root {
color-scheme: light dark;
--background: #eef1f5;
--surface: #ffffff;
--text: #111827;
--muted: #4b5563;
--border: rgba(17, 24, 39, .16);
--overlay: rgba(0, 0, 0, .62);
font-family: Roboto, Arial, sans-serif;
}
body.dark {
--background: #080b10;
--surface: #141922;
--text: #f7f8fa;
--muted: #c4cad4;
--border: rgba(255, 255, 255, .17);
--overlay: rgba(0, 0, 0, .72);
}
* { box-sizing: border-box; }
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
body { background: var(--background); color: var(--text); }
#default-view {
display: grid;
grid-template-rows: 100px minmax(0, 1fr) 120px;
gap: 8px;
width: 100%;
height: 100%;
padding: 8px;
}
.band {
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
padding: 12px 24px;
overflow: hidden;
border: 1px solid var(--border);
border-radius: 16px;
background: var(--surface);
}
.headline {
overflow: hidden;
margin: 0 0 8px;
font-size: 28px;
font-weight: 700;
line-height: 1.12;
text-overflow: ellipsis;
white-space: nowrap;
}
.detail {
display: -webkit-box;
overflow: hidden;
margin: 0;
color: var(--muted);
font-size: 18px;
line-height: 1.25;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
#cameras {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
min-height: 0;
}
.camera {
position: relative;
min-width: 0;
min-height: 0;
overflow: hidden;
border: 1px solid var(--border);
border-radius: 16px;
background: #111820;
}
.camera img, #event-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.camera-label, #event-label {
position: absolute;
right: 0;
bottom: 0;
left: 0;
padding: 10px 16px;
color: #fff;
background: var(--overlay);
font-size: 21px;
font-weight: 700;
}
#event-view {
position: fixed;
inset: 0;
display: none;
background: #000;
}
#event-view.front-door #event-image { object-fit: contain; }
#event-label { padding: 14px 22px; font-size: 28px; }
#connection {
position: fixed;
top: 0;
right: 0;
left: 0;
display: none;
padding: 9px 16px;
color: #fff;
background: #b91c1c;
font-size: 16px;
font-weight: 700;
text-align: center;
z-index: 10;
}
</style>
</head>
<body>
<div id="connection">Connecting to Home Assistant…</div>
<main id="default-view">
<section class="band">
<h1 class="headline" id="header-headline">Bedroom status</h1>
<p class="detail" id="header-detail">Connecting to Home Assistant…</p>
</section>
<section id="cameras">
<div class="camera">
<img id="front-lawn" alt="Front Lawn camera">
<div class="camera-label">Front Lawn</div>
</div>
<div class="camera">
<img id="driveway" alt="Driveway camera">
<div class="camera-label">Driveway</div>
</div>
</section>
<section class="band">
<h2 class="headline" id="alert-headline">Household status</h2>
<p class="detail" id="alert-detail">Loading alerts and reminders…</p>
</section>
</main>
<section id="event-view">
<img id="event-image" alt="Active event camera">
<div id="event-label"></div>
</section>
<script>
(function () {
'use strict';
var stateEntities = [
'sensor.kiosk_active_camera_view',
'sensor.bedroom_kiosk_header',
'sensor.kiosk_household_alert'
];
var states = {};
var imageUrls = {};
var stateRefreshRunning = false;
var imageRefreshRunning = false;
function setTheme() {
var hour = new Date().getHours();
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
document.body.classList.toggle('dark', prefersDark || hour >= 20 || hour < 7);
}
function getTokens() {
try {
return JSON.parse(localStorage.getItem('hassTokens') || 'null');
} catch (error) {
return null;
}
}
function tokenExpiresSoon(tokens) {
if (!tokens || !tokens.expires) return false;
var expires = typeof tokens.expires === 'number'
? tokens.expires
: new Date(tokens.expires).getTime();
return isFinite(expires) && expires - Date.now() < 60000;
}
function refreshToken(tokens) {
if (!tokens || !tokens.refresh_token) return Promise.reject(new Error('Home Assistant session missing'));
var body = new URLSearchParams();
body.set('grant_type', 'refresh_token');
body.set('refresh_token', tokens.refresh_token);
var clientId = tokens.clientId || tokens.client_id;
if (clientId) body.set('client_id', clientId);
return fetch('/auth/token', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'},
body: body.toString()
}).then(function (response) {
if (!response.ok) throw new Error('Home Assistant session refresh failed');
return response.json();
}).then(function (result) {
tokens.access_token = result.access_token;
tokens.token_type = result.token_type || tokens.token_type;
tokens.expires_in = result.expires_in;
tokens.expires = new Date(Date.now() + (result.expires_in * 1000)).toISOString();
localStorage.setItem('hassTokens', JSON.stringify(tokens));
return tokens;
});
}
function ensureToken(forceRefresh) {
var tokens = getTokens();
if (!tokens || !tokens.access_token) return Promise.reject(new Error('Home Assistant session missing'));
return forceRefresh || tokenExpiresSoon(tokens) ? refreshToken(tokens) : Promise.resolve(tokens);
}
function authorizedFetch(path, options, retry) {
options = options || {};
return ensureToken(false).then(function (tokens) {
options.headers = options.headers || {};
options.headers.Authorization = 'Bearer ' + tokens.access_token;
return fetch(path, options);
}).then(function (response) {
if (response.status === 401 && retry !== false) {
return ensureToken(true).then(function () {
return authorizedFetch(path, options, false);
});
}
if (!response.ok) throw new Error('Home Assistant request failed: ' + response.status);
return response;
});
}
function setConnectionError(message) {
var banner = document.getElementById('connection');
banner.textContent = message;
banner.style.display = 'block';
}
function clearConnectionError() {
document.getElementById('connection').style.display = 'none';
}
function render() {
var active = states['sensor.kiosk_active_camera_view'];
var header = states['sensor.bedroom_kiosk_header'];
var alert = states['sensor.kiosk_household_alert'];
var mode = active ? active.state : 'none';
var eventView = document.getElementById('event-view');
var defaultView = document.getElementById('default-view');
if (mode === 'garage' || mode === 'front_door') {
defaultView.style.display = 'none';
eventView.style.display = 'block';
eventView.className = mode === 'front_door' ? 'front-door' : '';
document.getElementById('event-label').textContent = mode === 'garage' ? 'Garage' : 'Front Door';
} else {
eventView.style.display = 'none';
defaultView.style.display = 'grid';
}
if (header) {
document.getElementById('header-headline').textContent = header.state;
document.getElementById('header-detail').textContent = header.attributes.detail || '';
}
if (alert) {
document.getElementById('alert-headline').textContent = alert.state;
document.getElementById('alert-detail').textContent = alert.attributes.detail || '';
}
}
function refreshStates() {
if (stateRefreshRunning) return;
stateRefreshRunning = true;
Promise.all(stateEntities.map(function (entityId) {
return authorizedFetch('/api/states/' + entityId).then(function (response) {
return response.json();
}).then(function (value) {
states[entityId] = value;
});
})).then(function () {
clearConnectionError();
render();
}).catch(function (error) {
setConnectionError(error.message + ' — load Home Assistant once to renew the trusted session.');
}).then(function () {
stateRefreshRunning = false;
});
}
function updateImage(elementId, cameraEntity) {
return authorizedFetch('/api/camera_proxy/' + cameraEntity).then(function (response) {
return response.blob();
}).then(function (blob) {
var nextUrl = URL.createObjectURL(blob);
var image = document.getElementById(elementId);
var previousUrl = imageUrls[elementId];
image.onload = function () {
if (previousUrl) URL.revokeObjectURL(previousUrl);
};
image.src = nextUrl;
imageUrls[elementId] = nextUrl;
});
}
function refreshImages() {
if (imageRefreshRunning) return;
imageRefreshRunning = true;
var active = states['sensor.kiosk_active_camera_view'];
var mode = active ? active.state : 'none';
var requests;
if (mode === 'garage') {
requests = [updateImage('event-image', 'camera.garagecam')];
} else if (mode === 'front_door') {
requests = [updateImage('event-image', 'camera.frontdoorbell')];
} else {
requests = [
updateImage('front-lawn', 'camera.frontlawn'),
updateImage('driveway', 'camera.driveway')
];
}
Promise.all(requests).catch(function (error) {
setConnectionError(error.message);
}).then(function () {
imageRefreshRunning = false;
});
}
setTheme();
refreshStates();
window.setTimeout(refreshImages, 1000);
window.setInterval(refreshStates, 5000);
window.setInterval(refreshImages, 4000);
window.setInterval(setTheme, 60000);
}());
</script>
</body>
</html>

@ -0,0 +1,9 @@
// Custom Sidebar is useful in interactive browsers, but wall kiosks run inside
// Android WebViews where the global sidebar extension is unnecessary and can
// delay Home Assistant startup on older hardware.
const isAndroidWebView = /\bAndroid\b/i.test(navigator.userAgent)
&& /(?:;|\s)wv\)/i.test(navigator.userAgent);
if (!isAndroidWebView) {
import("/hacsfiles/custom-sidebar/custom-sidebar-plugin.js?v=17.0.0");
}
Loading…
Cancel
Save

Powered by TurnKey Linux.