type: custom:button-card
variables:
thermostatversion: 2.0.0
var_url_params: |-
[[[
return new URLSearchParams(window.location.search);
]]]
var_climate_visuals: |-
[[[
/**
* Get visual styling based on HVAC mode and action
* @param {Object} state - Climate entity state object
* @returns {Object} Object with gradient and icon properties
*/
const getClimateVisuals = (state) => {
if (!state) {
return {
gradient: 'linear-gradient(135deg, #1c1c1c 0%, #2d2d2d 100%)',
icon: 'mdi:thermostat'
};
}
const mode = state.state;
const action = state.attributes?.hvac_action;
// Priority 1: Use action if available for most accurate representation
if (action) {
switch(action) {
case 'heating':
return {
gradient: 'linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%)',
icon: 'mdi:fire'
};
case 'cooling':
return {
gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
icon: 'mdi:snowflake'
};
case 'drying':
return {
gradient: 'linear-gradient(135deg, #f39c12 0%, #f1c40f 100%)',
icon: 'mdi:water-percent'
};
case 'fan':
return {
gradient: 'linear-gradient(135deg, #3498db 0%, #2980b9 100%)',
icon: 'mdi:fan'
};
case 'idle':
// Idle state - show mode intention with muted colors
switch(mode) {
case 'heat':
return {
gradient: 'linear-gradient(135deg, #c46b6b 0%, #c47e53 100%)',
icon: 'mdi:fire-circle'
};
case 'cool':
return {
gradient: 'linear-gradient(135deg, #5a9cc4 0%, #4090c4 100%)',
icon: 'mdi:snowflake-variant'
};
case 'heat_cool':
return {
gradient: 'linear-gradient(135deg, #c46b6b 0%, #5a9cc4 100%)',
icon: 'mdi:sync-circle'
};
default:
return {
gradient: 'linear-gradient(135deg, #434343 0%, #5a5a5a 100%)',
icon: 'mdi:power-standby'
};
}
case 'off':
return {
gradient: 'linear-gradient(135deg, #1c1c1c 0%, #2d2d2d 100%)',
icon: 'mdi:power'
};
}
}
// Priority 2: Fall back to mode if no action available
switch(mode) {
case 'heat':
return {
gradient: 'linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%)',
icon: 'mdi:fire'
};
case 'cool':
return {
gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
icon: 'mdi:snowflake'
};
case 'heat_cool':
return {
gradient: 'linear-gradient(135deg, #ff6b6b 0%, #4facfe 100%)',
icon: 'mdi:autorenew'
};
case 'dry':
return {
gradient: 'linear-gradient(135deg, #f39c12 0%, #f1c40f 100%)',
icon: 'mdi:water-percent'
};
case 'fan_only':
return {
gradient: 'linear-gradient(135deg, #3498db 0%, #2980b9 100%)',
icon: 'mdi:fan'
};
case 'off':
return {
gradient: 'linear-gradient(135deg, #1c1c1c 0%, #2d2d2d 100%)',
icon: 'mdi:power'
};
}
// Priority 3: Infer from temperature difference
const currentTemp = state.attributes?.current_temperature;
const targetTemp = state.attributes?.temperature;
if (currentTemp != null && targetTemp != null) {
const tempDiff = targetTemp - currentTemp;
if (tempDiff > 0.5) {
return {
gradient: 'linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%)',
icon: 'mdi:fire'
};
}
if (tempDiff < -0.5) {
return {
gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
icon: 'mdi:snowflake'
};
}
}
// Final fallback: Neutral gradient
return {
gradient: 'linear-gradient(135deg, #434343 0%, #5a5a5a 100%)',
icon: 'mdi:thermostat'
};
};
// Return visuals for the current climate device
if (!variables.var_climate_device) {
return getClimateVisuals(null);
}
const state = hass.states[variables.var_climate_device];
return getClimateVisuals(state);
]]]
var_climate_device: |-
[[[
/**
* Get the area/floor ID for a given device
* @param {string} deviceId - Device ID to look up
* @returns {Object} Object with areaId and floorId properties
*/
const getDeviceLocation = (deviceId) => {
if (!deviceId) return { areaId: null, floorId: null };
const device = Object.values(hass.devices || {}).find(d => d.id === deviceId);
if (!device) return { areaId: null, floorId: null };
const areaId = device.area_id || null;
let floorId = null;
if (areaId && hass.areas) {
const area = Object.values(hass.areas).find(a => a.area_id === areaId);
if (area) {
floorId = area.floor_id || null;
}
}
return { areaId, floorId };
};
/**
* Get the device ID for a climate entity
* @param {string} entityId - Climate entity ID
* @returns {string|null} Device ID or null
*/
const getClimateDeviceId = (entityId) => {
if (!hass.entities || !hass.entities[entityId]) return null;
return hass.entities[entityId].device_id || null;
};
/**
* Get the mic device ID from View Assist sensor entity
* @returns {string|null} Mic device ID or null
*/
const getViewAssistDeviceId = () => {
const vaEntity = variables.var_assistsat_entity;
if (!vaEntity || !hass.states[vaEntity]) return null;
const micDeviceId = hass.states[vaEntity].attributes.mic_device_id;
return micDeviceId || null;
};
const urlParams = variables.var_url_params;
const urlClimate = urlParams.get('climate');
// If URL specifies a climate device, use it
if (urlClimate) {
const entity = hass.states[urlClimate];
if (entity && entity.state !== 'unavailable' && entity.state !== 'unknown') {
return urlClimate;
}
}
// Get View Assist device location for intelligent matching
const vaDeviceId = getViewAssistDeviceId();
const vaLocation = getDeviceLocation(vaDeviceId);
// Get all available climate devices
const climateKeys = Object.keys(hass.states).filter(id => id.startsWith('climate.'));
const availableClimate = [];
const maxIterations = Math.min(climateKeys.length, 15); // Realistic upper bound
for (let i = 0; i < maxIterations; i++) {
const entityId = climateKeys[i];
const state = hass.states[entityId].state;
if (state !== 'unavailable' && state !== 'unknown') {
const deviceId = getClimateDeviceId(entityId);
const location = getDeviceLocation(deviceId);
availableClimate.push({
entityId,
deviceId,
areaId: location.areaId,
floorId: location.floorId
});
}
}
if (availableClimate.length === 0) {
return "";
}
// Priority 1: Same area as View Assist device
if (vaLocation.areaId) {
const sameAreaClimate = availableClimate.find(c => c.areaId === vaLocation.areaId);
if (sameAreaClimate) {
return sameAreaClimate.entityId;
}
}
// Priority 2: Same floor as View Assist device
if (vaLocation.floorId) {
const sameFloorClimate = availableClimate.find(c => c.floorId === vaLocation.floorId);
if (sameFloorClimate) {
return sameFloorClimate.entityId;
}
}
// Priority 3: First available climate device
return availableClimate[0].entityId;
]]]
var_all_climate_devices: |-
[[[
/**
* Format climate entity name for display
* @param {string} entityId - Climate entity ID
* @returns {string} Formatted name
*/
const formatClimateName = (entityId) => {
return entityId.replace('climate.', '').replace(/_/g, ' ');
};
/**
* Check if climate entity is available
* @param {string} state - Entity state
* @returns {boolean} True if available
*/
const isClimateAvailable = (state) => {
return state !== 'unavailable' && state !== 'unknown';
};
/**
* Get visual styling based on HVAC mode and action
* @param {Object} entity - Entity object
* @returns {Object} Object with gradient and icon properties
*/
const getClimateVisuals = (entity) => {
const mode = entity.state;
const action = entity.attributes?.hvac_action;
// Priority 1: Use action if available
if (action) {
switch(action) {
case 'heating':
return { icon: 'mdi:fire' };
case 'cooling':
return { icon: 'mdi:snowflake' };
case 'drying':
return { icon: 'mdi:water-percent' };
case 'fan':
return { icon: 'mdi:fan' };
case 'idle':
// Idle state - show mode intention with variant icons
switch(mode) {
case 'heat':
return { icon: 'mdi:fire-circle' };
case 'cool':
return { icon: 'mdi:snowflake-variant' };
case 'heat_cool':
return { icon: 'mdi:sync-circle' };
default:
return { icon: 'mdi:power-standby' };
}
case 'off':
return { icon: 'mdi:power' };
}
}
// Priority 2: Fall back to mode
switch(mode) {
case 'heat':
return { icon: 'mdi:fire' };
case 'cool':
return { icon: 'mdi:snowflake' };
case 'heat_cool':
return { icon: 'mdi:autorenew' };
case 'dry':
return { icon: 'mdi:water-percent' };
case 'fan_only':
return { icon: 'mdi:fan' };
case 'off':
return { icon: 'mdi:power' };
default:
return { icon: entity.attributes.icon || 'mdi:thermostat' };
}
};
const urlParams = variables.var_url_params;
const showType = urlParams.get('show');
const configuredClimate = urlParams.get('climate_devices');
// If specific devices are configured via URL
if (showType === 'configured' && configuredClimate) {
const devices = [];
const climateEntityList = decodeURIComponent(configuredClimate).split(',');
const maxIterations = Math.min(climateEntityList.length, 10); // Realistic upper bound
for (let i = 0; i < maxIterations; i++) {
const entityId = climateEntityList[i].trim();
const entity = hass.states[entityId];
if (entity && isClimateAvailable(entity.state)) {
const visuals = getClimateVisuals(entity);
devices.push({
entity_id: entityId,
name: entity.attributes.friendly_name || formatClimateName(entityId),
hvac_mode: entity.state,
hvac_action: entity.attributes?.hvac_action || null,
icon: visuals.icon
});
}
}
return devices;
}
// Get all available climate devices
const devices = [];
const climateKeys = Object.keys(hass.states).filter(id => id.startsWith('climate.'));
const maxIterations = Math.min(climateKeys.length, 15); // Realistic upper bound
for (let i = 0; i < maxIterations; i++) {
const entityId = climateKeys[i];
const entity = hass.states[entityId];
if (isClimateAvailable(entity.state)) {
const visuals = getClimateVisuals(entity);
devices.push({
entity_id: entityId,
name: entity.attributes.friendly_name || formatClimateName(entityId),
hvac_mode: entity.state,
hvac_action: entity.attributes?.hvac_action || null,
icon: visuals.icon
});
}
}
// Sort alphabetically for consistent display
devices.sort((a, b) => a.name.localeCompare(b.name));
return devices;
]]]
var_has_multiple_climate: "[[[ return variables.var_all_climate_devices.length > 1; ]]]"
template:
- body_template
styles:
grid:
- grid-template-areas: |-
[[[
const isPortrait = window.viewAssistResponsive?.orientation === "portrait";
if (!variables.var_climate_device) {
return isPortrait ?
`"title_card status"
"no_climate no_climate"
"no_climate no_climate"` :
`"title_card status status"
"no_climate no_climate no_climate"
"no_climate no_climate no_climate"`;
}
if (variables.var_has_multiple_climate) {
return isPortrait ?
`"title_card status"
"thermostat thermostat"
"thermostat thermostat"
"thermostat thermostat"
"thermostat thermostat"
"controls controls"` :
`"title_card status status status"
"thermostat thermostat thermostat controls"
"thermostat thermostat thermostat controls"
"thermostat thermostat thermostat controls"`;
}
return isPortrait ?
`"title_card status"
"thermostat thermostat"
"thermostat thermostat"
"thermostat thermostat"
"thermostat thermostat"` :
`"title_card status status"
"thermostat thermostat thermostat"
"thermostat thermostat thermostat"
"thermostat thermostat thermostat"`;
]]]
- grid-template-columns: |-
[[[
const isPortrait = window.viewAssistResponsive?.orientation === "portrait";
if (!variables.var_climate_device) {
return isPortrait ? "min-content 1fr" : "min-content 1fr 1fr";
}
if (variables.var_has_multiple_climate && !isPortrait) {
return "min-content 1fr 1fr min-content";
}
return isPortrait ? "min-content 1fr" : "min-content 1fr 1fr";
]]]
- grid-template-rows: |-
[[[
if (!variables.var_climate_device) {
return "min-content 1fr 1fr";
}
if (variables.var_has_multiple_climate && window.viewAssistResponsive?.orientation === "portrait") {
return "min-content 1fr 1fr 1fr 1fr min-content";
}
if (variables.var_has_multiple_climate) {
return "min-content 1fr 1fr 1fr";
}
return window.viewAssistResponsive?.orientation === "portrait" ?
"min-content 1fr 1fr 1fr 1fr" :
"min-content 1fr 1fr 1fr";
]]]
card:
- background: "[[[ return variables.var_climate_visuals.gradient; ]]]"
- border-radius: 0px
- font-family: >-
[[[ return `"${variables.var_assistsat_entity_font_style}", sans-serif`;
]]]
custom_fields:
title_card:
- position: absolute
- justify-self: start
- align-self: start
- z-index: 1
- width: max-content
- margin-left: 2%
- margin-top: "-4%"
thermostat:
- display: "[[[ return variables.var_climate_device ? 'flex' : 'none' ]]]"
- justify-content: center
- align-items: center
- height: 100%
- width: 100%
- padding-top: |-
[[[
return window.viewAssistResponsive?.orientation === "portrait" ? "8vh" : "1vh";
]]]
- padding-left: |-
[[[
return window.viewAssistResponsive?.orientation === "portrait" ? "0" : "1vw";
]]]
- padding-right: |-
[[[
return window.viewAssistResponsive?.orientation === "portrait" ? "0" : "1vw";
]]]
controls:
- display: "[[[ return variables.var_has_multiple_climate ? 'flex' : 'none' ]]]"
- justify-self: |-
[[[
return window.viewAssistResponsive?.orientation === "portrait" ? "center" : "end";
]]]
- align-self: |-
[[[
return window.viewAssistResponsive?.orientation === "portrait" ? "end" : "center";
]]]
- justify-content: flex-start
- align-items: |-
[[[
// In landscape, align to start so cards begin at top
// In portrait, keep centered
return window.viewAssistResponsive?.orientation === "portrait" ? "center" : "flex-start";
]]]
- padding: |-
[[[
return window.viewAssistResponsive?.orientation === "portrait" ? "1vh 0" : "0 2vw";
]]]
- overflow-y: |-
[[[
return window.viewAssistResponsive?.orientation === "portrait" ? "hidden" : "auto";
]]]
- overflow-x: |-
[[[
return window.viewAssistResponsive?.orientation === "portrait" ? "auto" : "hidden";
]]]
- max-height: |-
[[[
return window.viewAssistResponsive?.orientation === "portrait" ? "25vh" : "84vh";
]]]
- max-width: |-
[[[
return window.viewAssistResponsive?.orientation === "portrait" ? "93vw" : "18vw";
]]]
- margin-top: |-
[[[
return window.viewAssistResponsive?.orientation === "portrait" ? "0" : "9vh";
]]]
- "-webkit-overflow-scrolling": touch
- scroll-behavior: smooth
no_climate:
- display: "[[[ return variables.var_climate_device ? 'none' : 'flex' ]]]"
- justify-content: center
- align-items: center
- color: white
- text-align: center
custom_fields:
title_card:
card:
type: markdown
content: |-
[[[
if (!variables.var_climate_device) {
return "## Climate Control";
}
const state = hass.states[variables.var_climate_device];
const name = state?.attributes?.friendly_name || variables.var_climate_device.replace('climate.', '');
return `## ${name}`;
]]]
card_mod:
style: |-
[[[
const isPortrait = window.viewAssistResponsive?.orientation === "portrait";
const fontSize = isPortrait ? "320%" : "280%";
return `
ha-card {
background: transparent !important;
box-shadow: none !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
ha-markdown {
color: white !important;
text-align: left !important;
text-shadow: 0 2px 4px rgba(0,0,0,0.7) !important;
}
ha-markdown h2 {
margin: 0 !important;
font-size: ${fontSize} !important;
font-weight: 400 !important;
letter-spacing: 0.05em !important;
}
`;
]]]
thermostat:
card:
type: thermostat
entity: "[[[ return variables.var_climate_device ]]]"
card_mod:
style:
ha-state-control-climate-temperature$: |
.container {
--primary-text-color: white !important;
--secondary-text-color: white !important;
}
p.label,
p.label.secondary {
color: white !important;
}
.: |-
[[[
const isPortrait = window.viewAssistResponsive?.orientation === "portrait";
const zoomLevel = isPortrait ? "1.7" : "1.6";
return `
ha-card {
background: transparent !important;
height: 100% !important;
zoom: ${zoomLevel};
--mdc-ripple-hover-opacity: 0 !important;
--mdc-ripple-focus-opacity: 0 !important;
--mdc-ripple-press-opacity: 0 !important;
}
.title {
display: none !important;
}
.more-info {
color: white !important;
${isPortrait ? 'margin-top: 7vh !important;' : ''}
}
ha-state-control-climate-temperature {
--secondary-text-color: rgba(255, 255, 255, 0.9) !important;
--control-circular-slider-color: white !important;
--control-circular-slider-background: rgba(255, 255, 255, 0.3) !important;
}
`;
]]]
controls:
card:
type: |-
[[[
return window.viewAssistResponsive?.orientation === "portrait" ?
"horizontal-stack" : "vertical-stack";
]]]
cards: |-
[[[
if (!variables.var_has_multiple_climate) return [];
const devices = variables.var_all_climate_devices;
const maxDevices = Math.min(devices.length, 10);
const currentDevice = variables.var_climate_device;
const isPortrait = window.viewAssistResponsive?.orientation === "portrait";
const cards = [];
for (let i = 0; i < maxDevices; i++) {
const device = devices[i];
const isActive = device.entity_id === currentDevice;
cards.push({
type: "custom:button-card",
icon: device.icon,
name: device.name,
show_name: true,
show_icon: true,
tap_action: {
action: "call-service",
service: "view_assist.navigate",
service_data: {
device: variables.var_assistsat_entity,
path: (variables.var_dashboard || '/view-assist') + "/thermostat?climate=" + device.entity_id
}
},
styles: {
card: [
{ "background": isActive ? "rgba(255,255,255,0.4)" : "rgba(255,255,255,0.2)" },
{ "border": isActive ? "3px solid white" : "2px solid rgba(255,255,255,0.5)" },
{ "border-radius": "1rem" },
{ "padding": "1.5rem" },
{ "width": isPortrait ? "29vw" : "16vw" },
{ "height": isPortrait ? "29vw" : "16vw" },
{ "backdrop-filter": "blur(15px)" },
{ "box-shadow": "0 4px 20px rgba(0,0,0,0.3)" },
{ "margin": isPortrait ? "0 0.5vw" : "0.75vh 0" },
{ "flex-shrink": "0" }
],
icon: [
{ "width": isPortrait ? "15vw" : "8vw" },
{ "height": isPortrait ? "15vw" : "8vw" },
{ "color": "white" },
{ "filter": "drop-shadow(0 2px 4px rgba(0,0,0,0.5))" }
],
name: [
{ "font-size": isPortrait ? "3.7vw" : "1.9vw" },
{ "color": "white" },
{ "margin-top": "0.75rem" },
{ "font-weight": "700" },
{ "text-shadow": "0 2px 9px rgba(0,0,0,0.8)" },
{ "line-height": "1.2" },
{ "letter-spacing": "0.02em" },
{ "overflow": "hidden" },
{ "text-overflow": "ellipsis" },
{ "white-space": "nowrap" },
{ "max-width": "100%" }
],
grid: [
{ "grid-template-areas": '"i" "n"' },
{ "grid-template-rows": "1fr auto" },
{ "gap": "0.5rem" }
]
}
});
}
return cards;
]]]
no_climate:
card:
type: markdown
content: |
## No Climate Devices Available
No thermostats found in your Home Assistant setup.
card_mod:
style: |
ha-card {
background: rgba(255,255,255,0.1);
color: white;
padding: 2rem;
border-radius: 1rem;
text-align: center;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.2);
}