title: Klubbane Garasjebryggeri
kiosk_mode:
hide_header: true
hide_sidebar: true
mobile_settings:
hide_header: true
hide_sidebar: true
button_card_templates:
klubbane_keg:
show_name: false
show_icon: false
show_state: false
tap_action:
action: none
hold_action:
action: none
double_tap_action:
action: none
triggers_update:
- sensor.keg_list
- '[[[ return variables.selector; ]]]'
- '[[[ return variables.liters; ]]]'
custom_fields:
content: |
[[[
const helper = states[variables.selector];
const selected = helper?.state || 'Ingen øl valgt';
const beers = states['sensor.keg_list']?.attributes?.beers || [];
// input_select options are trimmed before they are stored. Brewfather
// names can contain invisible leading/trailing or repeated spaces.
// Compare normalized names so the selected beer still finds its data.
const normalizeBeerName = value => String(value ?? '')
.trim()
.replace(/\s+/g, ' ')
.toLocaleLowerCase('nb-NO');
const selectedKey = normalizeBeerName(selected);
const beer = beers.find(
b => normalizeBeerName(b?.name) === selectedKey
) || {};
const litersState = states[variables.liters];
const liters = Number.parseFloat(litersState?.state);
const validLiters = Number.isFinite(liters);
const capacity = Number(variables.capacity || 19);
const percent = validLiters
? Math.max(0, Math.min(100, liters / capacity * 100))
: 0;
const options = [...new Set(
(helper?.attributes?.options || [])
.filter(n => typeof n === 'string' && n.trim())
)];
const esc = value => String(value ?? '')
.replaceAll('&','&').replaceAll('<','<')
.replaceAll('>','>').replaceAll('"','"')
.replaceAll("'",''');
const fmtDate = value => {
if (!value) return '–';
const d = new Date(value);
if (Number.isNaN(d.getTime())) return String(value);
return new Intl.DateTimeFormat('nb-NO', {
day:'2-digit', month:'2-digit', year:'numeric'
}).format(d);
};
// Approximate standard beer-colour palette, indexed by SRM.
const srmPalette = [
'#f8f3a5','#f6e96b','#f4dc42','#efc72f','#e7ae22',
'#dc9418','#cc7913','#b96010','#a54a0d','#91380b',
'#7d2b09','#692207','#561b06','#451505','#361104',
'#2a0d04','#210a04','#190803','#120602','#0c0402'
];
let srm = Number.parseFloat(beer.color_srm);
if (!Number.isFinite(srm)) {
const ebc = Number.parseFloat(beer.color_ebc);
srm = Number.isFinite(ebc) ? ebc / 1.97 : 7;
}
const colorIndex = Math.max(0, Math.min(srmPalette.length - 1, Math.round(srm) - 1));
const beerColor = srmPalette[colorIndex];
// Progress colour: red at empty, yellow halfway, green at full.
const progressHue = Math.round(percent * 1.2);
const progressColor = `hsl(${progressHue} 82% 45%)`;
const optionHtml = options.map(name =>
`<option value="${esc(name)}"${name === selected ? ' selected' : ''}>${esc(name)}</option>`
).join('');
return `
<div class="keg-card">
<div class="fat-title"><i></i><span>FAT ${variables.keg_number}</span><i></i></div>
<div class="selector-wrap">
<ha-icon icon="mdi:beer"></ha-icon>
<select class="beer-select"
onclick="event.stopPropagation()"
onmousedown="event.stopPropagation()"
ontouchstart="event.stopPropagation()"
onchange="event.stopPropagation(); document.querySelector('home-assistant').hass.callService(
'input_select','select_option',
{entity_id:'${variables.selector}', option:this.value}
);">
${optionHtml}
</select>
</div>
<div class="style">${esc(beer.style || '–')}</div>
<div class="facts">
<div>
<ha-icon icon="mdi:water-outline"></ha-icon>
<strong>${beer.abv !== undefined && beer.abv !== null ? esc(String(beer.abv).replace('.', ',')) + '%' : '–'}</strong>
<span>ABV</span>
</div>
<div>
<ha-icon icon="mdi:barrel"></ha-icon>
<strong>${beer.batch_number ? '#' + esc(beer.batch_number) : '–'}</strong>
<span>BRYGG NR.</span>
</div>
</div>
<div class="tap-date">
<ha-icon icon="mdi:calendar"></ha-icon>
<span>TAPPEDATO</span>
<strong>${fmtDate(beer.packaging_date)}</strong>
</div>
<div class="tank-area">
<div class="tank">
<div class="tank-cap"></div>
<div class="tank-window">
<div class="liquid" style="height:${percent}%; --beer:${beerColor}">
<div class="wave"></div>
<i class="bubble b1"></i><i class="bubble b2"></i>
<i class="bubble b3"></i><i class="bubble b4"></i>
</div>
<div class="tank-copy">
<span>LITER IGJEN</span>
<strong>${validLiters ? liters.toFixed(1).replace('.', ',') : '–'} <small>L</small></strong>
<em>${validLiters ? Math.round(percent) + '%' : '–'}</em>
</div>
</div>
<div class="tank-base"></div>
</div>
</div>
<div class="progress-shell">
<div class="progress-fill"
style="width:${percent}%; background:${progressColor}"></div>
<b>${validLiters ? Math.round(percent) + '%' : '–'}</b>
</div>
<div class="progress-labels"><span>0%</span><span>50%</span><span>100%</span></div>
</div>`;
]]]
styles:
card:
- height: 100%
- padding: 0
- border-radius: 14px
- overflow: hidden
- background: linear-gradient(180deg,#171b1c,#0d1112)
- border: 1px solid rgba(185,190,188,.36)
- box-shadow: 0 10px 25px rgba(0,0,0,.45), inset 0 1px rgba(255,255,255,.04)
grid:
- height: 100%
- grid-template-areas: '"content"'
- grid-template-columns: 1fr
- grid-template-rows: 1fr
custom_fields:
content:
- height: 100%
- pointer-events: auto
- position: relative
- z-index: 10
extra_styles: >
* { box-sizing:border-box; }
.keg-card { height:100%; display:grid; grid-template-rows:34px 45px 35px
70px 54px minmax(0,1fr) 18px 16px; gap:4px; padding:6px 10px 7px;
color:#f3f0e9; font-family:Arial,sans-serif; }
.fat-title { display:grid; grid-template-columns:1fr auto 1fr;
align-items:center; gap:10px; color:#dc9a30; font-size:21px;
font-weight:800; letter-spacing:.07em; }
.fat-title i { height:1px;
background:linear-gradient(90deg,transparent,#c48929); }
.fat-title i:last-child {
background:linear-gradient(90deg,#c48929,transparent); }
.selector-wrap { display:grid; grid-template-columns:33px minmax(0,1fr);
align-items:center; border:1px solid #c88620; border-radius:11px;
background:#080b0c; overflow:hidden; position:relative; z-index:20;
pointer-events:auto !important; touch-action:manipulation; }
.selector-wrap ha-icon { color:#dc981e; justify-self:center; width:23px;
height:23px; }
.beer-select { width:100%; height:43px; padding:0 34px 0 5px; border:0;
outline:0; color:#f5f3ed; background:#080b0c;
font-size:clamp(16px,2.0vw,25px); font-weight:650; text-align:center;
position:relative; z-index:30; pointer-events:auto !important;
touch-action:manipulation; cursor:pointer; -webkit-appearance:menulist;
appearance:auto; }
.beer-select option { color:#fff; background:#101314; }
.style { display:flex; align-items:center; justify-content:center;
color:#d99527; font-size:clamp(13px,1.45vw,19px); border-bottom:1px solid
rgba(255,255,255,.17); white-space:nowrap; overflow:hidden;
text-overflow:ellipsis; }
.facts { display:grid; grid-template-columns:1fr 1fr; border-bottom:1px
solid rgba(255,255,255,.17); }
.facts > div { display:grid; grid-template-columns:34px 1fr;
grid-template-rows:1fr 20px; grid-template-areas:'icon value' 'icon
label'; align-items:center; padding:4px 10px; }
.facts > div + div { border-left:1px solid rgba(255,255,255,.17); }
.facts ha-icon { grid-area:icon; color:#d79525; width:28px; height:28px; }
.facts strong { grid-area:value; font-size:clamp(18px,2.2vw,29px);
font-weight:700; }
.facts span { grid-area:label; color:#d79525; font-size:11px;
letter-spacing:.12em; }
.tap-date { display:grid; grid-template-columns:38px 1fr;
grid-template-rows:19px 1fr; grid-template-areas:'icon label' 'icon
value'; align-items:center; padding:3px 12%; border-bottom:1px solid
rgba(255,255,255,.17); }
.tap-date ha-icon { grid-area:icon; color:#d79525; width:27px;
height:27px; }
.tap-date span { grid-area:label; color:rgba(245,245,245,.6);
font-size:10px; letter-spacing:.13em; }
.tap-date strong { grid-area:value; font-size:clamp(15px,1.65vw,21px);
font-weight:550; }
.tank-area { min-height:0; display:flex; align-items:stretch;
justify-content:center; padding-top:2px; }
.tank { position:relative; width:96%; height:100%; min-height:210px;
display:grid; grid-template-rows:18px minmax(0,1fr) 12px;
filter:drop-shadow(0 10px 10px rgba(0,0,0,.48)); }
.tank-cap,.tank-base { z-index:3; margin:0 6%; border:2px solid #858989;
background:linear-gradient(90deg,#323536,#d1d2cf 20%,#55595a 45%,#e1e0da
70%,#373a3b); }
.tank-cap { border-radius:15px 15px 4px 4px; }
.tank-base { border-radius:4px 4px 13px 13px; }
.tank-window { position:relative; overflow:hidden; border:5px solid
transparent; border-left-color:#7c8080; border-right-color:#7c8080;
background:radial-gradient(circle at 50% 45%,#151a1b,#050708 72%);
box-shadow:inset 0 0 0 2px #1d2021,inset 0 0 22px #000;
border-radius:10px; }
.liquid { position:absolute; left:0; right:0; bottom:0;
background:linear-gradient(90deg,color-mix(in srgb,var(--beer),#fff
14%),var(--beer) 45%,color-mix(in srgb,var(--beer),#000 30%));
transition:height 1.2s ease; box-shadow:inset 14px 0 20px
rgba(255,255,255,.10),inset -16px 0 24px rgba(0,0,0,.3); }
.wave { position:absolute; top:-8px; left:-8%; width:116%; height:18px;
border-radius:48%; background:color-mix(in srgb,var(--beer),#fff 38%);
box-shadow:0 2px 7px rgba(255,255,255,.45); animation:slosh 4s ease-in-out
infinite alternate; }
@keyframes slosh { from{transform:translateX(-2%) rotate(-1deg)}
to{transform:translateX(2%) rotate(1deg)} }
.bubble { position:absolute; bottom:4%; border:1px solid
rgba(255,255,255,.58); border-radius:50%; opacity:0; animation:rise 5s
infinite linear; }
.b1{width:7px;height:7px;left:17%;animation-delay:-1s}.b2{width:10px;height:10px;left:42%;animation-delay:-3s}.b3{width:5px;height:5px;left:68%;animation-delay:-2s}.b4{width:8px;height:8px;left:82%;animation-delay:-4s}
@keyframes rise { 0%{transform:translateY(0);opacity:0} 15%{opacity:.55}
100%{transform:translateY(-170px);opacity:0} }
.tank-copy { position:absolute; z-index:4; inset:0; display:flex;
flex-direction:column; align-items:center; justify-content:flex-start;
padding-top:15px; text-shadow:0 2px 5px #000; pointer-events:none; }
.tank-copy span { color:#d79525; font-size:11px; letter-spacing:.12em; }
.tank-copy strong { margin-top:4px; font-size:clamp(28px,3.4vw,42px);
line-height:1; }
.tank-copy small { font-size:.48em; }
.tank-copy em { color:#d79525; margin-top:4px; font-size:18px;
font-style:normal; }
.progress-shell { position:relative; height:18px; overflow:hidden;
border:1px solid rgba(220,220,210,.7); border-radius:5px;
background:#090c0d; }
.progress-fill { height:100%; transition:width 1.2s ease,background .8s
ease; }
.progress-shell b { position:absolute; right:5px; top:1px; color:#fff;
font-size:12px; text-shadow:0 1px 3px #000; }
.progress-labels { display:flex; justify-content:space-between;
color:#f4f3ee; font-size:10px; }
views:
- title: Tappekraner
path: tappekraner
type: panel
cards:
- type: vertical-stack
card_mod:
style: |
ha-card {
height: 100vh;
overflow: hidden;
background: #050607;
border-radius: 0;
box-shadow: none;
}
cards:
- type: picture
image: /local/klubbane_banner_2020.png
tap_action:
action: none
hold_action:
action: none
card_mod:
style: >
ha-card { margin:0; border-radius:0; box-shadow:none;
background:#050607; }
img { display:block; width:100%; height:116px; object-fit:cover;
}
- type: grid
columns: 3
square: false
card_mod:
style: |
ha-card {
height: calc(100vh - 116px);
padding: 5px;
margin: 0;
background: #050607;
box-sizing: border-box;
}
#root {
height: 100%;
gap: 7px !important;
align-items: stretch;
}
cards:
- type: custom:button-card
template: klubbane_keg
entity: input_select.keg_1_beer
variables:
keg_number: 1
selector: input_select.keg_1_beer
liters: sensor.keg_1_fg_kompensert_ol_igjen
capacity: 19
- type: custom:button-card
template: klubbane_keg
entity: input_select.keg_2_beer
variables:
keg_number: 2
selector: input_select.keg_2_beer
liters: sensor.keg_2_fg_kompensert_ol_igjen
capacity: 19
- type: custom:button-card
template: klubbane_keg
entity: input_select.keg_3_beer
variables:
keg_number: 3
selector: input_select.keg_3_beer
liters: sensor.keg_3_fg_kompensert_ol_igjen
capacity: 19