parent
89d12ee1e7
commit
4feee5caf6
@ -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;
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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: []
|
||||
@ -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…
Reference in new issue