CCCPaste

View Thermostat 3

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);
          }