Simple Air Comfort card

V5.1 SIMPLE AIR COMFORT CARD

  • Increased floating circle to 20px size
  • Improved centering of flashing red, moved into card-mod css, from custom button card.
  • Flashing starts when floating circle breaches top of outer ring limits 26.5
  • High Temp Alert when floating circle breaches top of outer ring limits 26.5
  • Default center temperature 22

Horizontal Stack

Two Vertical Stacks within Horizontal Stack

  • Fully refactored code
    • One card code for all screen sizes
    • No NodeRED Dependency
    • Minimal card-mod used (improves loading times)
    • Previous card-mod code moved into button card native
    • All border and background transparent
    • Default white icons
    • Error handling for unknown and unavailable sensors (text and colour)
    • Improved scaling for (Floating Circle)
  • Macro Driven
    • Most of the configuration of the card completed from the macro
    • Calculates Dewpoint
    • Calculates Feels like Temperature (Australian Apparent Temperature)
    • Calculates Temperature Colour (Card Background)
    • Calculates Dewpoint Colour (Outer Ring Limits)
    • Calculates Temp and Humidity Alert Colour (Inner Circle Comfort Zone)
    • Calculates Temperature and Humidity Position and Blinking Warning (Floating Circle)
    • Configurable setpoints in the macros, to allow customisation.
  • Sensors that get inserted into the card
    • Dewpoint
    • Feels like Temperature (Australian Apparent Temperature)
    • Temperature Colour (Card Background)
    • Dewpoint Colour (Outer Ring Limits)
    • Temp and Humidity Alert Colour (Inner Circle Comfort Zone)
    • Temperature and Humidity Position and Blinking Warning (Floating Circle)

Background Overlay

  1. Using Visual Studio Code Editor or other, create a new folder called simple-air-comfort-card in config/www/community
  2. Right Click on you newly created folder /config/www/community/simple-air-comfort-card and select New File
  3. Name the new file sac_background_overlay.svg
  4. Click on the file and paste the following:
  5. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 150" />

MACRO CODE

  1. Using Visual Studio Code Editor or other, create a new folder called custom_templates in your config, if it is not already there.
  2. Right Click on you newly created folder /config/custom_templates and select New File
  3. Name the new file sac_macros.jinja

  1. Click on the file and paste the following: MACRO CODE
Macro Code
{# Dewpoint Temperature #}
{% macro calculate_dew_point(entity_temperature, entity_humidity) %}
  {% set T, RH = states(entity_temperature), states(entity_humidity) %}
  {% if (T == 'unknown') or (RH == 'unknown') %}
    unknown
  {% elif (T == 'unavailable') or (RH == 'unavailable') %}
    unavailable
  {% else %}
    {% set T = T | float %}
    {% set RH = RH | float %}
    {# Arden Buck Method constants #}
    {% set a = 6.1121 | float %}
    {% set b = 18.678 | float %}
    {% set c = 257.14 | float %}
    {% set d = 234.5 | float %}
    {% set e = 2.71828 | float %}
    {# Calculate gamma #}
    {% set gamma = log((RH/100)*e**((b-T/d)*(T/(c+T)))) | float %}
    {# Calculate dew point #}
    {% set dew_point = ((c * gamma) / (b - gamma)) | round(1) %}
    {{ dew_point }}
  {% endif %}
{% endmacro %}

{# Australian Apparent Temperature Feels Like #}
{% macro calculate_apparent_temperature(entity_temperature, entity_humidity, entity_wind_speed=None, Q=0) %}
  {% set T, RH = states(entity_temperature), states(entity_humidity) %}
  {% if (T == 'unknown') or (RH == 'unknown') %}
    unknown
  {% elif (T == 'unavailable') or (RH == 'unavailable') %}
    unavailable
  {% else %}
    {% set T = T | float %}
    {% set RH = RH | float %}
    {% if entity_wind_speed %}
      {% set WS = states(entity_wind_speed) %}
      {% if (WS == 'unknown') or (WS == 'unavailable') %}
        {% set WS = 0 %}
      {% else %}
        {% set WS = (WS | float) / 3.6 %}  {# Convert km/h to m/s #}
      {% endif %}
    {% else %}
      {% set WS = 0 %}
    {% endif %}
    {# Calculate water vapor pressure (e) #}
    {% set e = (RH / 100) * 6.105 * (2.71828 ** ((17.27 * T) / (237.7 + T))) %}
    {# Calculate apparent temperature (AT) #}
    {% set AT = T + 0.348 * e - 0.70 * WS + 0.70 * (Q / (WS + 10)) - 4.25 %}
    {{ AT | round(1) }}
  {% endif %}
{% endmacro %}

{# Dewpoint Comfort Text #}
{% macro calculate_text_dewpoint_comfort(sensor_dewpoint) %}
  {# Retrieve the current state of the dew point sensor #}
  {% set dewpoint = states(sensor_dewpoint) %}

  {# Check for 'unknown' or 'unavailable' states first #}
  {% if dewpoint == 'unknown' %}
    {% set comfort_level = 'Unknown' %}
  {% elif dewpoint == 'unavailable' %}
    {% set comfort_level = 'Unavailable' %}
  {% else %}
    {# Convert the dew point value to float and categorize it #}
    {% set dewpoint = dewpoint | float(0) %}
    {% if dewpoint < 5 %}
      {% set comfort_level = 'Very Dry' %}
    {% elif dewpoint >= 5 and dewpoint <= 10 %}
      {% set comfort_level = 'Dry' %}
    {% elif dewpoint >= 10.1 and dewpoint <= 12.79 %}
      {% set comfort_level = 'Pleasant' %}
    {% elif dewpoint >= 12.8 and dewpoint <= 15.49 %}
      {% set comfort_level = 'Comfortable' %}
    {% elif dewpoint >= 15.5 and dewpoint <= 18.39 %}
      {% set comfort_level = 'Sticky Humid' %}
    {% elif dewpoint >= 18.4 and dewpoint <= 21.19 %}
      {% set comfort_level = 'Muggy' %}
    {% elif dewpoint >= 21.2 and dewpoint <= 23.9 %}
      {% set comfort_level = 'Sweltering' %}
    {% else %}
      {% set comfort_level = 'Stifling' %}
    {% endif %}
  {% endif %}
  {# Return the comfort level text #}
  {{ comfort_level }}
{% endmacro %}

{# Temperature Comfort Text #}
{% macro calculate_text_temperature_comfort(sensor_temperature) %}
  {# Retrieve the current state of the temperature sensor #}
  {% set temperature = states(sensor_temperature) %}

  {# Check for 'unknown' or 'unavailable' states first #}
  {% if temperature == 'unknown' %}
    {% set comfort_level = 'N/A' %}
  {% elif temperature == 'unavailable' %}
    {% set comfort_level = 'N/A' %}
  {% else %}
    {# Convert the temperature value to float and categorize it #}
    {% set temperature = temperature | float(0) %}
    {% if temperature < 3 %}
      {% set comfort_level = 'FROSTY' %}
    {% elif temperature >= 3.1 and temperature <= 4.9 %}
      {% set comfort_level = 'COLD' %}
    {% elif temperature >= 5 and temperature <= 8.9 %}
      {% set comfort_level = 'CHILLY' %}
    {% elif temperature >= 9 and temperature <= 13.9 %}
      {% set comfort_level = 'COOL' %}
    {% elif temperature >= 14 and temperature <= 18.9 %}
      {% set comfort_level = 'MILD' %}
    {% elif temperature >= 19 and temperature <= 23.9 %}
      {% set comfort_level = 'PERFECT' %}
    {% elif temperature >= 24 and temperature <= 27.9 %}
      {% set comfort_level = 'WARM' %}
    {% elif temperature >= 28 and temperature <= 34.9 %}
      {% set comfort_level = 'HOT' %}
    {% else %}
      {% set comfort_level = 'BOILING' %}
    {% endif %}
  {% endif %}
  {# Return the comfort level text #}
  {{ comfort_level }}
{% endmacro %}

{# Humidity Comfort Text #}
{% macro calculate_text_humidity_comfort(entity_humidity) %}
  {# Get the current humidity value from the specified entity_humidity #}
  {% set humidity = states(entity_humidity) %}

  {# Check for 'unknown' or 'unavailable' states first #}
  {% if humidity == 'unknown' %}
    N/A
  {% elif humidity == 'unavailable' %}
    N/A
  {% else %}
    {# Convert the humidity value to float and categorize it #}
    {% set humidity = humidity | float(0) %}
    {# Check if the humidity is less than 40% #}
    {% if humidity < 40 %}
      DRY
    {# Check if the humidity is between 40% and 60% #}
    {% elif humidity >= 40 and humidity <= 60 %}
      COMFY
    {# If the humidity is greater than 60% #}
    {% else %}
      HUMID
    {% endif %}
  {% endif %}
{% endmacro %}

{# SAC Card Picture Elements Card Background #}
{% macro calculate_temperature_colour_card_background(entity) %}
  {% if is_state(entity, 'Unknown') or is_state(entity, 'Unavailable') or is_state(entity, 'N/A') %}
    radial-gradient(circle, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), dimgray) !important;
  {% elif is_state(entity, 'FROSTY') %}
    radial-gradient(circle, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), mediumblue) !important;
  {% elif is_state(entity, 'COLD') %}
    radial-gradient(circle, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), dodgerblue) !important;
  {% elif is_state(entity, 'CHILLY') %}
    --ha-card-background: radial-gradient(circle, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), deepskyblue) !important;
  {% elif is_state(entity, 'COOL') %}
    radial-gradient(circle, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), mediumaquamarine) !important;
  {% elif is_state(entity, 'MILD') %}
    radial-gradient(circle, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), seagreen) !important;
  {% elif is_state(entity, 'PERFECT') %}
    radial-gradient(circle, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), limegreen) !important;
  {% elif is_state(entity, 'WARM') %}
    radial-gradient(circle, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), gold) !important;
  {% elif is_state(entity, 'HOT') %}
    radial-gradient(circle, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), orange) !important;
  {% elif is_state(entity, 'BOILING') %}
    radial-gradient(circle, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15), crimson) !important;
  {% endif %}
{% endmacro %}

{# SAC Card Element Outer Ring Limits #}
{% macro calculate_dewpoint_colour_outer_ring_limits(entity) %}
  {# Set a default value for dewpoint_color #}
  {% set dewpoint_color = 'radial-gradient(circle, transparent, 55%, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15)) !important;' %}

  {% if is_state(entity, 'Unknown') or is_state(entity, 'Unavailable') or is_state(entity, 'N/A') or is_state(entity, 'unavailable') or is_state(entity, 'unknown') %}
    {% set dewpoint_color = 'radial-gradient(circle, dimgray, 55%, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15)) !important;' %}
  {% elif is_state(entity, 'Very Dry') %}
    {% set dewpoint_color = 'radial-gradient(circle, deepskyblue, 55%, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15)) !important;' %}
  {% elif is_state(entity, 'Dry') %}
    {% set dewpoint_color = 'radial-gradient(circle, mediumaquamarine, 55%, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15)) !important;' %}
  {% elif is_state(entity, 'Pleasant') %}
    {% set dewpoint_color = 'radial-gradient(circle, limegreen, 55%, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15)) !important;' %}
  {% elif is_state(entity, 'Comfortable') %}
    {% set dewpoint_color = 'radial-gradient(circle, yellowgreen, 55%, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15)) !important;' %}
  {% elif is_state(entity, 'Sticky Humid') %}
    {% set dewpoint_color = 'radial-gradient(circle, yellow, 55%, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15)) !important;' %}
  {% elif is_state(entity, 'Muggy') %}
    {% set dewpoint_color = 'radial-gradient(circle, gold, 55%, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15)) !important;' %}
  {% elif is_state(entity, 'Sweltering') %}
    {% set dewpoint_color = 'radial-gradient(circle, orange, 55%, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15)) !important;' %}
  {% elif is_state(entity, 'Stifling') %}
    {% set dewpoint_color = 'radial-gradient(circle, crimson, 55%, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.15)) !important;' %}
  {% endif %}

  {# Apply gradient #}
  {{ dewpoint_color }}
{% endmacro %}

{# SAC Card Element Inner Circle Comfort Zone #}
{% macro inner_circle_comfort_zone_alert_colour(humidity_entity, temperature_entity) %}
  {% set humidity_state = states(humidity_entity) %}
  {% set temperature_state = states(temperature_entity) %}

  {# Alert Levels #}
  {% set temp_boiling_alert = 34.9 %}
  {% set temp_hot_alert = 26.5 %}
  {% set temp_warm_alert = 24.0 %}
  {% set temp_mild_alert = 19.0 %}
  {% set temp_cool_alert = 14.0 %}
  {% set temp_chilly_alert = 9.0 %}
  {% set temp_cold_alert = 5.0 %}
  {% set temp_frosty_alert = 3.0 %}
  {% set humidity_min_alert = 40 %}
  {% set humidity_max_alert = 60 %}
  
  {# Default colors for unknown/unavailable states #}
  {% set default_color = 'dimgray' %}
  
  {# Determine humidity colour #}
  {% if humidity_state == 'unknown' or humidity_state == 'unavailable' %}
    {% set humidity_color = default_color %}
  {% else %}
    {% set humidity_float = humidity_state | float(0) %}
    {% if humidity_float > humidity_max_alert %}
      {% set humidity_color = 'hotpink' %}
    {% elif humidity_float >= humidity_min_alert and humidity_float <= humidity_max_alert %}
      {% set humidity_color = 'black' %}
    {% elif humidity_float < humidity_min_alert %}
      {% set humidity_color = 'hotpink' %}
    {% endif %}
  {% endif %}
  
  {# Determine temperature colour #}
  {% if temperature_state == 'unknown' or temperature_state == 'unavailable' %}
    {% set temperature_color = default_color %}
  {% else %}
    {% set temperature_float = temperature_state | float(0) %}
    {% if temperature_float > temp_boiling_alert %}
      {% set temperature_color = 'rgba(255, 69, 0, 0.8)' %}
    {% elif temperature_float > temp_hot_alert and temperature_float <= temp_boiling_alert %}
      {% set temperature_color = 'rgba(255, 69, 0, 0.8)' %}
    {% elif temperature_float > temp_warm_alert and temperature_float <= temp_hot_alert %}
      {% set temperature_color = 'dimgray' %}
    {% elif temperature_float > temp_mild_alert and temperature_float <= temp_warm_alert %}
      {% set temperature_color = 'dimgray' %}
    {% elif temperature_float > temp_cool_alert and temperature_float <= temp_mild_alert %}
      {% set temperature_color = 'rgba(0, 102, 255, 0.8)' %}
    {% elif temperature_float > temp_chilly_alert and temperature_float <= temp_cool_alert %}
      {% set temperature_color = 'rgba(0, 102, 255, 0.8)' %}
    {% elif temperature_float > temp_cold_alert and temperature_float <= temp_chilly_alert %}
      {% set temperature_color = 'rgba(0, 102, 255, 0.8)' %}
    {% elif temperature_float > temp_frosty_alert and temperature_float <= temp_cold_alert %}
      {% set temperature_color = 'rgba(0, 102, 255, 0.8)' %}   
    {% elif temperature_float < temp_frosty_alert %}
      {% set temperature_color = 'rgba(0, 102, 255, 0.8)' %}
    {% endif %}
  {% endif %}
  
  {# Apply gradient with spaces #}
    radial-gradient(circle, {{ humidity_color }} 0%, black, {{ temperature_color }} 70%) !important;
{% endmacro %}

{# SAC Card Element Floating Circle #}
{% macro card_mod_floating_circle_movement_and_color(humidity_entity, temperature_entity) %}
  {% set humidity_state = states(humidity_entity) %}
  {% set temperature_state = states(temperature_entity) %}
  {% set humidity_float = humidity_state | float(50) if humidity_state not in ['unknown', 'unavailable'] else 50 %}
  {% set temperature_float = temperature_state | float(22) if temperature_state not in ['unknown', 'unavailable'] else 22 %}

  {# Alert Levels and scaling factors #}
  {% set temp_min = 15 %}
  {% set temp_max = 35 %}
  {% set temp_min_warning = 18 %}
  {% set temp_max_warning = 26.4 %}
  {% set humidity_min = 40 %}
  {% set humidity_max = 60 %}

  {# Scaling Map temp to 0% (bottom) → 100% (top) of the card #}
  {% set temp_scaled = (((temperature_float - temp_min) * (100 - 0) / (temp_max - temp_min)) + 0) | round(1) %}
  {% set temp_scaled = [0, temp_scaled, 100] | sort | join(' ') | regex_replace('^.*? (.*?) .*$', '\\1') %}

  {# Check if the Floating Circle is outside Outer Ring Limits Temp or Inner Circle Comfort Zone Humidity #}
  {% set outside_limits = (humidity_float < humidity_min) or (humidity_float > humidity_max) or (temperature_float < temp_min_warning) or (temperature_float > temp_max_warning) or (humidity_float == 50) or (temperature_float == 22.0) %}

  {# Positioning the floating circle #}
  left: {{ humidity_float + 0.5 }}%;
  bottom: {{ temp_scaled }}%;
  transition: bottom 0.8s ease-in-out, left 0.8s ease-in-out;
  transform: translate(-50%, 50%);

  {# Ensures the icon itself does NOT blink #}
  ha-icon {
    color: white !important;
  }

  {# Default state: Fully transparent #}
  ha-card {
    border-radius: 50%;
    background: transparent !important;
    position: relative;
    aspect-ratio: 1 / 1;
    width: 100%;
    height: auto;
  }

  {# When outside limits, apply blinking red gradient #}
  {% if outside_limits %}
    ha-card::before {
      content: "";
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      aspect-ratio: 1 / 1;
      width: 100%;
      height: 100%;
      background: radial-gradient(circle, 
        rgba(255,0,0,0.8) 20%, 
        rgba(255,0,0,0.3) 50%, 
        rgba(255,0,0,0.1) 70%, 
        rgba(255,0,0,0) 100%
      ) !important;
      animation: blink 1s infinite alternate;
    }
  {% endif %}

  {# Define blinking animation #}
  @keyframes blink {
    0% { opacity: 1; }
    100% { opacity: 0.3; }
  }
{% endmacro %}

SENSOR CODE

  1. In your configuration.yaml file paste the following template: sensors: into your configuration to make a single card.
  2. Ensure you enter your Temperature and Humidity Sensors. Wind Speed Optional for Australian Apparent Temperature
  3. Any sensors that start with sensor.sac are calculated in the Macro.
  4. Rename i.e. living_room to suit your needs

SENSOR CODE

template:  
  - sensor:
      # Template Australian Apparent Temperature - Wind Speed Optional (km/h) - Utilising custom_templates/macros.jinja          
      - name: "sac_feels_like_temperature_living_room"
        unique_id: 4dc577f3-7d28-4083-a010-ac3825fd6fd1
        unit_of_measurement: "°C"
        icon: mdi:thermometer
        state: >
          {% from 'sac_macros.jinja' import calculate_apparent_temperature %}
          {{ calculate_apparent_temperature('sensor.insert_temperature', 'sensor.insert_humidity','sensor.insert_wind_speed') }}

      # Template Dew Point Sensors - Utilising custom_templates/macros.jinja
      - name: "dewpoint_living_room"
        unique_id: c5e921de-47d2-4338-9158-8097911df43e
        unit_of_measurement: "°C"
        icon: mdi:thermometer-water
        state: >
          {% from 'sac_macros.jinja' import calculate_dew_point %}
          {{ calculate_dew_point('sensor.insert_temperature', 'sensor.insert_humidity') }}

      # Template Simple Air Comfort Card - Dewpoint Colour - Outer Ring Limits - Living Room
      - name: "sac_dewpoint_colour_living_room"
        unique_id: c6639b18-ac02-441f-badd-815c6c810254
        state: >-
          {% from 'sac_macros.jinja' import calculate_dewpoint_colour_outer_ring_limits %}
          {{ calculate_dewpoint_colour_outer_ring_limits('sensor.sac_text_dewpoint_comfort_living_room') }}

      # Template Simple Air Comfort Card - Alert Colour - Inner Circle - Living Room
      - name: "sac_inner_circle_alert_colour_living_room"
        unique_id: a3c9d6d1-e51d-450b-a61c-1f7d9cc7baeb
        state: >-
          {% from 'sac_macros.jinja' import inner_circle_comfort_zone_alert_colour %}
          {{ inner_circle_comfort_zone_alert_colour('sensor.air_conditioning_humidity', 'air_conditioning_temperature') }}

      # Template Simple Air Comfort Card - Temperature Colour - Picture Elements Card Background - Living Room
      - name: "sac_temperature_colour_living_room"
        unique_id: dc6c834b-f775-4b36-a2f3-d43356632cc6
        state: >-
          {% from 'sac_macros.jinja' import calculate_temperature_colour_card_background %}
          {{ calculate_temperature_colour_card_background('sensor.sac_text_temperature_comfort_living_room') }}

      # Template Simple Air Comfort Card - Text Dewpoint Comfort - Living Room
      - name: "sac_text_dewpoint_comfort_living_room"
        unique_id: e8b76541-7fe7-407e-b53d-f012a3d6152b
        state: >-
          {% from 'sac_macros.jinja' import calculate_text_dewpoint_comfort %}
          {{ calculate_text_dewpoint_comfort('sensor.dewpoint_living_room') }}

      # Template Simple Air Comfort Card - Text Temperature Comfort - Living Room
      - name: "sac_text_temperature_comfort_living_room"
        unique_id: 5e2b7728-1630-4d3f-b511-04d48bf76cd4
        state: >-
          {% from 'sac_macros.jinja' import calculate_text_temperature_comfort %}
          {{ calculate_text_temperature_comfort('sensor.air_conditioning_temperature') }}

      # Template Simple Air Comfort Card - Text Humidity Comfort - Living Room
      - name: "sac_text_humidity_comfort_living_room"
        unique_id: f299c759-e50c-4f9f-a775-b99734dab132
        state: >-
          {% from 'sac_macros.jinja' import calculate_text_humidity_comfort %}
          {{ calculate_text_humidity_comfort('sensor.air_conditioning_humidity') }}

Thanks @LiQuid_cOOled for the input and @vajdum, may be of interest to you.

1 Like