I am using Home Assistant Yellow with a CM4:
- Core: 2026.6.3
- Supervisor: 2026.06.1
- Operating System: 17.3
- Frontend: 20260527.6
- HACS: button-card: v.7.0.1
- Ecobee 3 Lite with HomeKit Integration
I used Google Gemini AI web interface to do this coding work to show the Air Handler Unit's (AHU) fan different colors for the different modes of the Heating, Ventilation, and Air Conditioning (HVAC) modes. For "cooling" mode the fan becomes light blue and rotates, for "heating" mode the fan becomes bright orange and rotates, and finally for all other fan on states the fan becomes green and rotates.
The picture elements card ends up looking like this with the Ecobee/HVAC in cooling mode:
You will need to update the sensor.xyz in the below code for your specific setups. Here's the picture elements yaml code to copy and paste:
# A dashboard card that overlays dynamic home automation data directly onto a background blueprint or graphic
type: picture-elements
# The path to the custom background image representing the HVAC system / air handler hardware layout
image: /local/air-handler-3.png
# The array of icons, badges, and labels placed sequentially on top of the background image
elements:
# =========================================================================
# AIR RETURN SENSORS (Left Column - Centered horizontally at X: 2%)
# Represents air entering the system from the house
# =========================================================================
# Badge displaying current humidity of the return air stream
- type: state-badge
entity: sensor.esp2_air_handler_return_humidity
style:
top: 48%
left: 2%
color: transparent # Prevents fallback/unformatted default text colors
transform: scale(0.7,0.7) translate(-43%,-43%) # Shrinks the badge to 70% size and fixes anchor offset
# Badge displaying the calculated dew point of the return air stream
- type: state-badge
entity: sensor.esp2_air_return_dew_point
style:
top: 77%
left: 2%
color: transparent
transform: scale(0.7,0.7) translate(-43%,-43%)
# Badge displaying the current temperature of the return air stream
- type: state-badge
entity: sensor.esp2_air_handler_return_temperature
style:
top: 19%
left: 2%
color: transparent
transform: scale(0.7,0.7) translate(-43%,-43%)
# =========================================================================
# AIR SUPPLY SENSORS (Right Column - Centered horizontally at X: 92%)
# Represents conditioned air being blown back out to the house
# =========================================================================
# Badge displaying the humidity of the treated air entering the supply ducts
- type: state-badge
entity: sensor.esp2_air_handler_supply_humidity
style:
top: 48%
left: 92%
color: transparent
transform: scale(0.7,0.7) translate(-43%,-43%)
# Badge displaying the calculated dew point of the supply air stream
- type: state-badge
entity: sensor.esp2_air_supply_dew_point
style:
top: 77%
left: 92%
color: transparent
transform: scale(0.7,0.7) translate(-43%,-43%)
# Badge displaying the temperature of the supply air stream
- type: state-badge
entity: sensor.esp2_air_handler_supply_temperature
style:
top: 19%
left: 92%
color: transparent
transform: scale(0.7,0.7) translate(-43%,-43%)
# =========================================================================
# AIR TEMPERATURE DELTA (Center Top - Positioned at X: 47%, Y: 19%)
# Tracks temperature drop across the cooling/heating coils
# =========================================================================
- type: state-badge
entity: sensor.air_handler_temperature_delta
name: " " # Blank space overrides and suppresses the default lower text label
icon: none # Hides the inside central icon to maximize space for raw state numbers
style:
left: 47%
top: 19%
transform: scale(0.7,0.7) translate(-43%,-43%)
card_mod:
style:
ha-state-label-badge:
$:
# Targets the root HTML tag responsible for rendering a standard Home Assistant badge structure
ha-label-badge:
# THE SHADOW DOM BREAKOUT CHARACTER ($:)
# This tells card-mod to drill through the badge's protected web component boundaries (Shadow DOM).
# Without this symbol, standard CSS cannot reach or style the inner elements like .label-badge.
$: |
/* CSS Pseudo-element: Injects a physical text string ("Δ ") right before the native numeric state value */
.label-badge .value::before {
content: "Δ " !important;
}
/* CSS Layout Fix: Overrides Home Assistant's default hardcoded width limits to prevent the new "Δ" symbol from truncating or breaking onto a new line */
.label-badge .value {
max-width: none !important;
overflow: visible !important;
}
/* CSS Target: Forces the unit text block inside the badge's lower label loop to explicitly print "°C" */
.label-badge .label .value {
content: "°C" !important;
}
/* CSS Layout Visibility: Overrides the built-in logic that occasionally hides the lower label block, forcing it to render safely as a distinct row block */
.label-badge .label {
display: block !important;
}
/* CSS Clean-up: Hides Home Assistant's default outer badge label string to prevent a messy layout duplicate from clashing with our custom elements above */
.badge-label {
display: none !important;
}
# THE MAIN CONTEXT RETURN KEYWORK (.:)
# This tells card-mod to exit the deep Shadow DOM loop and jump right back up to the base element level.
.: |
# Target rule mapping variables directly onto the primary parent container host wrapper (:host)
:host {
/* JINJA2 VARIABLE 1: Verifies if the HVAC system thermostat is actively making a cooling call */
{% set ecobee_cooling = is_state('sensor.ecobee_state', 'cooling') %}
/* JINJA2 VARIABLE 2: Converts the current numerical temperature drop into a floating-point integer. Then verifies if it drops below a safe operational split window of 8°C */
{% set low_delta = states('sensor.air_handler_temperature_delta') | float(0) < 8.0 %}
/* JINJA2 VARIABLE 3: Checks timestamps to see if the AC state has been running for at least 60 seconds. This avoids false alarms during initial system compressor startup lag */
{% set time_elapsed = (as_timestamp(now()) - as_timestamp(states.sensor.ecobee_state.last_changed | default(now()))) >= 60 %}
/* EVALUATION PHASE: If the AC cooling loop runs for 1+ minutes but fails to generate an 8°C air drop, trigger an active performance warning flag */
{% if ecobee_cooling and low_delta and time_elapsed %}
/* CSS State Override: Colors the badge text bright alert red and weights it thick to stand out as a fault condition */
--label-badge-text-color: #ff3333 !important;
font-weight: bold !important;
{% else %}
/* Fallback CSS State: Reverts back to your default system theme color style parameters when HVAC variables measure safe */
--label-badge-text-color: var(--secondary-text-color) !important;
{% endif %}
}
# =========================================================================
# REFRIGERANT LINES / LOOP SENSORS (Lower Row)
# Tracks temperatures within the AC condenser/evaporator physical piping
# =========================================================================
# Badge displaying temperature of refrigerant expanding into the system coils
- type: state-badge
entity: sensor.esp2_refrigerant_supply_temperature
style:
top: 84%
left: 35%
color: transparent
transform: scale(0.7,0.7) translate(-43%,-43%)
# Badge displaying temperature of refrigerant flowing out back to the outdoor compressor
- type: state-badge
entity: sensor.esp2_refrigerant_return_temperature
style:
top: 84%
left: 60%
color: transparent
transform: scale(0.7,0.7) translate(-43%,-43%)
# Badge calculating temperature difference between the liquid and gas line pipes
- type: state-badge
entity: sensor.refrigerant_temperature_delta
name: " "
icon: none
style:
left: 47%
top: 84%
transform: scale(0.7,0.7) translate(-43%,-43%)
card_mod:
style:
ha-state-label-badge:
$:
ha-label-badge:
$: |
/* Applies identical formatting to inject 'Δ' and '°C' onto the refrigerant delta badge */
.label-badge .value::before {
content: "Δ " !important;
}
.label-badge .value {
max-width: none !important;
overflow: visible !important;
}
.label-badge .label .value {
content: "°C" !important;
}
.label-badge .label {
display: block !important;
}
.badge-label {
display: none !important;
}
.: |
:host {
/* Keeps the text standard color without custom fault/alert rules */
--label-badge-text-color: var(--secondary-text-color) !important;
}
# =========================================================================
# ANIMATED BLOWER FAN STATUS ICON (Center Right)
# Dynamic rotation and color swapping representing air handler fan activity
# =========================================================================
- type: state-icon
entity: binary_sensor.ecobee_fan
state_color: false # Deactivates standard HA theme coloring to use our advanced card_mod styling rules below
style:
left: 78.8%
top: 46%
card_mod:
style:
state-badge:
$:
ha-state-icon:
$:
# Targets the innermost iron-icon or web component rendering the vector graphic image
ha-icon:
# THE SHADOW DOM DRILL-DOWN SYMBOL ($:)
# Pierces into the inner shadow root layer of the icon element.
# This is mandatory to directly target and rotate the raw graphic container instead of the whole card block.
$: |
/* JINJA2 CONDITIONAL: Checks if the physical HVAC air circulation blower fan is actively powered on */
{% if is_state('binary_sensor.ecobee_fan', 'on') %}
/* Targets the inner icon root element wrapper */
:host {
/* Layout Hack: Switches container from generic inline to inline-block. Without this, standard HTML blocks refuse to process 2D/3D CSS transformations and spin animations */
display: inline-block !important;
/* Animation Trigger: Points to our 'rotation' sequence below, forcing a 2-second linear continuous loop spin */
animation: rotation 2s linear infinite !important;
}
{% else %}
/* Actions executed if the fan sensor evaluates to off, idle, or unavailable states */
:host {
/* Halts and resets all running movement timelines when the system powers down */
animation: none !important;
}
{% endif %}
/* KEYFRAME SEQUENCER: Defines the custom rotational animation engine */
@keyframes rotation {
0% { transform: rotate(0deg); } /* Start position frame */
100% { transform: rotate(360deg); } /* Full loop completion frame */
}
# THE ROOT ESCAPE SEQUENCE (.:)
# Jumps completely backward out of the inner shadow roots to style the base structural layer of the parent element
.: |
/* Targets the high-level host layout node wrapper containing our icon element */
:host {
/* PRIMARY JINJA2 BLOCK: Evaluates if the HVAC system is moving air through the home */
{% if is_state('binary_sensor.ecobee_fan', 'on') %}
/* SUB-JINJA2 CONDITION A: Checks if the outdoor compressor is active in cooling mode */
{% if is_state('sensor.ecobee_state', 'cooling') %}
/* Theme Variables: Overrides icon color to bright sky blue to represent cold air blowing */
--state-icon-color: #00bfff !important;
--paper-item-icon-color: #00bfff !important;
/* SUB-JINJA2 CONDITION B: Checks if the gas furnace or heat pump is active in heating mode */
{% elif is_state('sensor.ecobee_state', 'heating') %}
/* Theme Variables: Overrides icon color to bright orange to represent warm air blowing */
--state-icon-color: #ffa500 !important;
--paper-item-icon-color: #ffa500 !important;
/* SUB-JINJA2 FALLBACK: Executed if the fan is on but the system is neither cooling nor heating */
{% else %}
/* Theme Variables: Overrides icon color to green, indicating pure ventilation or air filtration mode */
--state-icon-color: #008000 !important;
--paper-item-icon-color: #008000 !important;
{% endif %}
{% else %}
/* GLOBAL FALLBACK BLOCK: Executed when the fan is completely turned off and inactive */
{% demonstrators %}
/* Theme Variables: Sets icon color to an idle steel blue, matching standard passive dashboard components */
--state-icon-color: #44739e !important;
--paper-item-icon-color: #44739e !important;
{% endif %}
}
