Custom UI: Weather state card

I actually misread the thread and thought you had updated the weather state card to be this animated forecast by default; not the ability to do so

Can you link to the PR please…

What is the source of data? Can you share the config please?

Could something like this be implemented as an iframe that references the weather card generated elsewhere? For instance creating the necessary card with HTML and javascript residing in the /www folder… the using the iframe custom component card ( I saw one made here somewhere…) and the viola animated weather?

Is this possible to implement it as a secondary UI card? As per this manual? https://github.com/andrey-git/home-assistant-custom-ui/blob/master/docs/features.md

well custom cards won’t be merged until the frontend restructure.

here is a modified version to run as state-card

\\HASSIO\config\www\weather_icons\animated\*: https://www.amcharts.com/free-animated-svg-weather-icons/

\\HASSIO\config\www\custom_ui\custom-weather-card.html:

<dom-module id='custom-weather-card'>
  <template>
    <style>
      .clear {
        clear:both;
      }
      .card {
        margin:1em auto;
        padding-left: 1em;
        padding-right:1em;
        position: relative;
      }
      iron-icon {
        height: 20px;
        color: #c8c8c8;
      }
      .temp {
        font-weight: 300;
        font-size: 5em;
        color:#5b5b5b;
        position: absolute;
        right: .3em;
      }
      .variations {
        font-weight:300;
        color:#8c8c8c;
        list-style:none;
        margin-left:-2em;
        margin-top: 3.5em;
      }
      .variations.right {
        float: right;
        margin-left: 0;
        margin-right: 1em;
      }
      .unit {
        font-size:.8em;
      }
      .forecast {
        width:100%;
        margin:0 auto;
        height:9em;
      }
      .day {
        display:block;
        width: 25%;
        float:left;
        text-align:center;
        color: #5b5b5b;
        border-right:.1em solid #d9d9d9;
        line-height: 2;
        box-sizing: border-box;
      }
      .dayname {
        text-transform: uppercase;
      }

      .forecast .day:first-child {
        margin-left: 0;
      }

      .forecast .day:nth-last-child(2) {
        border-right:none;
        margin-right: 0;
      }
      .highTemp {
        font-weight:bold;
      }
      .lowTemp {
        color: #8c8c8c;
      }

      .icon.bigger {
          width: 10em;
          height: 10em;
          margin-top: -4em;
        position: absolute;
      }

      .icon {
          width: 34px;
          height: 34px;
          display: inline-block;
          vertical-align: middle;
          background-size: contain;
          background-position: center center;
          background-repeat: no-repeat;
          text-indent: -9999px;
      }
    </style>
    <div class="card">
      <span class="city"></span>
      <br>
      <i class="icon bigger" style="background: none, url(/local/weather_icons/animated/[[nowCond]].svg) no-repeat; background-size: contain;"></i>
      <span class="temp">[[roundedTemp]]&#176;</span>
      <span>
        <ul class="variations right">
          <template is="dom-if" if="[[weatherObj.attributes.humidity]]">
              <li><iron-icon icon="mdi:water-percent"></iron-icon>[[weatherObj.attributes.humidity]]<span class="unit">%</span></li>
        </template>
          <template is="dom-if" if="[[weatherObj.attributes.pressure]]">
              <li><iron-icon icon="mdi:gauge"></iron-icon>[[weatherObj.attributes.pressure]]<span class="unit">hPa</span></li>
        </template>
          <template is="dom-if" if="[[weatherObj.attributes.visibility]]">
              <li><iron-icon icon="mdi:weather-fog"></iron-icon>[[weatherObj.attributes.visibility]]<span class="unit">m</span></li>
        </template>
        </ul>
        <ul class="variations">
          <li>[[weatherObj.state]]</li>
          <template is="dom-if" if="[[weatherObj.attributes.wind_speed]]">
            <li><iron-icon icon="mdi:weather-windy"></iron-icon>[[windBearing]] [[weatherObj.attributes.wind_speed]]<span class="unit">m/s</span></li>
        </template>
        </ul>
      </span>

      <div class="forecast clear">
        <template is="dom-repeat" items="[[forecast]]">
        <div class="day"><span class="dayname">[[item.day]]</span>
          <template is="dom-if" if="[[item.condIcon]]">
          <br> <i class="icon" style="background: none, url(/local/weather_icons/animated/[[item.condIcon]].svg) no-repeat; background-size: contain;"></i> 
          </template>
          <template is="dom-if" if="[[item.tempHigh]]">
          <br> <span class="highTemp">[[item.tempHigh]]&#176;</span> 
          </template>
          <template is="dom-if" if="[[item.tempLow]]">
          <br> <span class="lowTemp">[[item.tempLow]]&#176;</span>
          </template>
        </div>
      </template>
      </div>
    </div>
  </template>
</dom-module>

<script>
(function () {
  'use strict';

  var _WEATHER_TO_ICON = {
    cloudy: 'cloudy',
    fog: 'cloudy',
    hail: 'rainy-7',
    lightning: 'lightning',
    'lightning-rainy': 'lightning',
    'Zwaar bewolkt': 'cloudy',
    partlycloudy: 'cloudy-day-2',
    pouring: 'rainy-7',
    rainy: 'rainy-5',
    snowy: 'snowy-5',
    'snowy-rainy': 'snowy-6',
    sunny: 'day',
    windy: 'cloudy',
    'windy-variant': '[]',
    exceptional: '!!',
  };

  var _DEGREE_TEXT = [
    'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
    'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', 'N'
  ];

  Polymer({
    is: 'custom-weather-card',

    properties: {
      hass: {
        type: Object,
      },
      stateObj: {
        type: Object,
      },
      weatherObj: {
        type: Object,
        observer: 'checkRequirements',
        computed: 'comptuteWeatherObj(hass, stateObj)',
      },
    },

    comptuteWeatherObj: function (hass, stateObj) {
      return stateObj && stateObj.attributes && stateObj.attributes.config && stateObj.attributes.config.weather ? hass.states[stateObj.attributes.config.weather] : null;
    },

    getForecastArray: function () {
      if (!this.weatherObj.attributes.forecast) {
        return [];
      }
	  
      var data = this.weatherObj.attributes.forecast;
	  var forecast = [];
	  var prevDay = '';
	  
      for (var i = 0; i < data.length; i++) {
        var day = new Date(data[i].datetime).toString().split(' ')[0];
        if (day != prevDay) {
          if (data[i].max_temp) {
            var tempHigh = Math.round(data[i].max_temp * 10) / 10;
          } else {
            var tempHigh = Math.round(data[i].temperature * 10) / 10;
          }
          var tempLow = Math.round(data[i].min_temp * 10) / 10;
          var condIcon = _WEATHER_TO_ICON[data[i].condition];
          forecast.push({day:day, tempHigh:tempHigh, tempLow:tempLow, condIcon:condIcon});
          prevDay = day;
        } else {
          if (data[i].max_temp) {
            var tempHigh = Math.round(data[i].max_temp * 10) / 10;
          } else {
            var tempHigh = Math.round(data[i].temperature * 10) / 10;
          }
          var tempLow = Math.round(data[i].tempLow * 10) / 10;
          if (tempLow > forecast[forecast.length-1].tempHigh) {
            forecast[forecast.length-1].tempHigh = tempLow;
          }
          if (tempHigh > forecast[forecast.length-1].tempHigh) {
            forecast[forecast.length-1].tempHigh = tempHigh;
          }
          if (!forecast[forecast.length-1].tempLow) {
            forecast[forecast.length-1].tempLow = tempHigh;
          }
          if (tempHigh < forecast[forecast.length-1].tempLow) {
            forecast[forecast.length-1].tempLow = tempHigh;
          }
          if (tempLow < forecast[forecast.length-1].tempLow) {
            forecast[forecast.length-1].tempLow = tempLow;
          }
        }
      }

      return forecast;
    },

    checkRequirements: function () {
      if (!this.weatherObj) {
        return;
      }
  
      this.nowCond = _WEATHER_TO_ICON[this.weatherObj.state];
      if (this.weatherObj.attributes.temperature) {
      this.roundedTemp = Math.round( this.weatherObj.attributes.temperature * 10) / 10;
      }
      if (this.weatherObj.attributes.windBearing) {
      this.windBearing = this.windBearingToText(this.weatherObj.attributes.windBearing);
      }
	    this.forecast = this.getForecastArray().slice(0, 4);
    },

    windBearingToText: function (degree) {
      return _DEGREE_TEXT[((parseInt(degree) + 5.63) / 11.25) | 0];
    },
  });
}());
</script>

configuration.yaml

homeassistant:
  customize:
    input_boolean.weather:
      custom_ui_state_card: custom-weather-card
      config:
        weather: weather.yweather

frontend:
  extra_html_url:
    - /local/custom_ui/custom-weather-card.html
  extra_html_url_es5:
    - /local/custom_ui/custom-weather-card.html

weather:
  - platform: yweather

input_boolean:
  weather:

group:
  weather:
    name: Weather
    entities:
      - input_boolean.weather

config:
discovery:
7 Likes

Thank you so much for sharing that state card Eddi89 !!

Following your instructions, I had to correct a few things in the code of the state-card to make it work nicely, so I’ll add here if someone else need:
The lightning icon does not exist in the icon pack linked in the post so replace lightning to thunder for the icon name (not for the variable):

lightning: 'thunder',
 'lightning-rainy': 'thunder',

-the ‘data[i].min_temp’ must be replaced by ‘data[i].templow’
-the ‘data[i].tempLow’ must be replaced by ‘data[i].templow’

And then it all work well.
Thanks again!

1 Like

Thank you so much for this awesome state card, this should be the ha default weather card.

I made a change to add the possibility to change the language, just need to change the name in the mapping function inside the script.
I did also a small layout to improve the information display.
The result is this:
Nuova%20immagine%20bitmap

I have no java or html skill so i don’t know if this is the best way to do it, but work so i’m happy with it.
Thank again for sharing this.

I post the code in case someone need it or want to make the same change in a better way:

<dom-module id='custom-weather-card'>
  <template>
    <style>
      .clear {
        clear:both;
      }
      .card {
        margin:1em auto;
        padding-left: 1em;
        padding-right:1em;
        position: relative;
      }
      .iron-icon {
        height: 18px;
        color: #c8c8c8;
      }
      .temp {
        font-weight: 300;
        font-size: 5em;
        color:#5b5b5b;
        position: absolute;
        right: 0em;
      }
      .variations {
        font-weight:300;
        color:#8c8c8c;
        list-style:none;
        margin-left:-2em;
        margin-top: 2.5em;
      }
      .variations.right {
        float: right;
        margin-left: 0;
        margin-right: 1em;
      }
      .unit {
        font-size:.8em;
      }
      .forecast {
        width:100%;
        margin:0 auto;
        height:9em;
      }
      .day {
        display:block;
        width: 25%;
        float:left;
        text-align:center;
        color: #5b5b5b;
        border-right:.1em solid #d9d9d9;
        line-height: 2;
        box-sizing: border-box;
      }
      .dayname {
        text-transform: uppercase;
      }

      .forecast .day:first-child {
        margin-left: 0;
      }
      .forecast .day:nth-last-child(2) {
        border-right:none;
        margin-right: 0;
      }
      .highTemp {
        font-weight:bold;
      }
      .lowTemp {
        color: #8c8c8c;
      }
      .icon.bigger {
        width: 10em;
        height: 10em;
        margin-top: -4em;
        position: absolute;
        left: 0em;
      }
      .icon {
          width: 50px;
          height: 50px;
          display: inline-block;
          vertical-align: middle;
          background-size: contain;
          background-position: center center;
          background-repeat: no-repeat;
          text-indent: -9999px;
      }
       .weather {
        font-weight: 300;
        font-size: 1.5em;
        color:#5b5b5b;
        text-align:center;
        position: absolute;
        top: 0em;
        left: 6em;
      }

    </style>
    <div class="card">
      <span class="icon bigger" style="background: none, url(/local/weather_icons/animated/[[nowCond]].svg) no-repeat; background-size: contain;"></span>
      <span class="temp">[[roundedTemp]]&#176;</span>
      <span class="weather">[[nowCondIT]]</span>
      <br>
      <span>
        <ul class="variations right">
          <template is="dom-if" if="[[weatherObj.attributes.humidity]]">
              <li><iron-icon icon="mdi:water-percent"></iron-icon> Umidità [[weatherObj.attributes.humidity]]<span class="unit">%</span></li>
          </template>
          <template is="dom-if" if="[[weatherObj.attributes.pressure]]">
              <li><iron-icon icon="mdi:gauge"></iron-icon> Pressione [[weatherObj.attributes.pressure]]<span class="unit">hPa</span></li>
          </template>

        </ul>
        <ul class="variations">
          <template is="dom-if" if="[[weatherObj.attributes.wind_speed]]">
            <li><iron-icon icon="mdi:weather-windy"></iron-icon> Vento [[item.windBearing]] [[weatherObj.attributes.wind_speed]]<span class="unit">m/s</span></li>
          </template>
          <template is="dom-if" if="[[weatherObj.attributes.visibility]]">
              <li><iron-icon icon="mdi:weather-fog"></iron-icon> Visibilità [[weatherObj.attributes.visibility]]<span class="unit">m</span></li>
          </template>
        </ul>
      </span>

      <div class="forecast clear">
        <template is="dom-repeat" items="[[forecast]]">
        <div class="day"><span class="dayname">[[item.dayIT]]</span>
          <template is="dom-if" if="[[item.condIcon]]">
          <br> <i class="icon" style="background: none, url(/local/weather_icons/animated/[[item.condIcon]].svg) no-repeat; background-size: contain;"></i> 
          </template>
          <template is="dom-if" if="[[item.tempHigh]]">
          <br> <span class="highTemp">[[item.tempHigh]]&#176;</span> 
          </template>
          <template is="dom-if" if="[[item.tempLow]]">
          <br> <span class="lowTemp">[[item.tempLow]]&#176;</span>
          </template>
        </div>
      </template>
      </div>
    </div>
  </template>
</dom-module>

<script>
(function () {
  'use strict';

  var _WEATHER_TO_ICON = {
    cloudy: 'cloudy',
    fog: 'cloudy',
    hail: 'rainy-7',
    lightning: 'thunder',
    'lightning-rainy': 'thunder',
    partlycloudy: 'cloudy-day-3',
    pouring: 'rainy-6',
    rainy: 'rainy-5',
    snowy: 'snowy-6',
    'snowy-rainy': 'rainy-7',
    sunny: 'day',
    windy: 'cloudy',
    'windy-variant': 'cloudy-day-3',
    exceptional: '!!',
  };
  
  var _WEATHER_TO_NAME = {
    cloudy: 'Nuvoloso',
    fog: 'Nebbia',
    hail: 'Grandine',
    lightning: 'Temporale',
    'lightning-rainy': 'Temporale',
    partlycloudy: 'Parzialmente Nuvoloso',
    pouring: 'Pioggia Forte',
    rainy: 'Pioggia',
    snowy: 'Neve',
    'snowy-rainy': 'Nevischio',
    sunny: 'Soleggiato',
    windy: 'Ventoso',
    'windy-variant': 'Variabile Ventoso',
    exceptional: '!',
  };


  var _DEGREE_TEXT = [
    'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SO', 'OSO', 'O', 'ONO', 'NO', 'NNO', 'N',
  ];
  
  var _DAY_TO_DAY = {
    Mon: 'lunedì',
    Tue: 'martedì',
    Wed : 'mercoledì',
    Thu: 'giovedì',
    Fri: 'venerdì',
    Sat: 'sabato',
    Sun: 'domemica',
  };

  Polymer({
    is: 'custom-weather-card',

    properties: {
      hass: {
        type: Object,
      },
      stateObj: {
        type: Object,
      },
      weatherObj: {
        type: Object,
        observer: 'checkRequirements',
        computed: 'comptuteWeatherObj(hass, stateObj)',
      },
    },

    comptuteWeatherObj: function (hass, stateObj) {
      return stateObj && stateObj.attributes && stateObj.attributes.config && stateObj.attributes.config.weather ? hass.states[stateObj.attributes.config.weather] : null;
    },

    getForecastArray: function () {
      if (!this.weatherObj.attributes.forecast) {
        return [];
      }
      
      var data = this.weatherObj.attributes.forecast;
      var forecast = [];
      var prevDay = '';
      
      for (var i = 0; i < data.length; i++) {
        var day = new Date(data[i].datetime).toString().split(' ')[0];
        if (day != prevDay) {
          if (data[i].max_temp) {
            var tempHigh = Math.round(data[i].max_temp * 10) / 10;
          } else {
            var tempHigh = Math.round(data[i].temperature * 10) / 10;
          }
          var tempLow = Math.round(data[i].templow * 10) / 10;
          var condIcon = _WEATHER_TO_ICON[data[i].condition];
          var dayIT = _DAY_TO_DAY[day];
          forecast.push({dayIT:dayIT, tempHigh:tempHigh, tempLow:tempLow, condIcon:condIcon});
          prevDay = day;
        } else {
          if (data[i].max_temp) {
            var tempHigh = Math.round(data[i].max_temp * 10) / 10;
          } else {
            var tempHigh = Math.round(data[i].temperature * 10) / 10;
          }
          var tempLow = Math.round(data[i].tempLow * 10) / 10;
          if (tempLow > forecast[forecast.length-1].tempHigh) {
            forecast[forecast.length-1].tempHigh = tempLow;
          }
          if (tempHigh > forecast[forecast.length-1].tempHigh) {
            forecast[forecast.length-1].tempHigh = tempHigh;
          }
          if (!forecast[forecast.length-1].tempLow) {
            forecast[forecast.length-1].tempLow = tempHigh;
          }
          if (tempHigh < forecast[forecast.length-1].tempLow) {
            forecast[forecast.length-1].tempLow = tempHigh;
          }
          if (tempLow < forecast[forecast.length-1].tempLow) {
            forecast[forecast.length-1].tempLow = tempLow;
          }
        }
      }

      return forecast;
    },

    checkRequirements: function () {
      if (!this.weatherObj) {
        return;
      }
  
      this.nowCond = _WEATHER_TO_ICON[this.weatherObj.state];
      this.nowCondIT = _WEATHER_TO_NAME[this.weatherObj.state];
      if (this.weatherObj.attributes.temperature) {
      this.roundedTemp = Math.round( this.weatherObj.attributes.temperature * 10) / 10;
      }
      if (this.weatherObj.attributes.windBearing) {
      this.windBearing = this.windBearingToText(this.weatherObj.attributes.windBearing);
      }
        this.forecast = this.getForecastArray().slice(0, 4);
    },

    windBearingToText: function (degree) {
      return _DEGREE_TEXT[((parseInt(degree) + 5.63) / 11.25) | 0];
    },
  });
}());
</script>
15 Likes

Wow, this is great!
Thanks @eddi89, @PItax and @Bram_Kragten :smile:

1 Like

I followed all of the config but just getting a blank card. Any ideas what I missed? I’m using Safari on both a Mac and iOS. My other custom_ui files work ok. But I usually have a different es5 version. Could that be it?

53%20PM

Try to remove all unnecessary lines in the weather component

weather:
  - platform: yweather

refresh your browser cache

I found out it’s Yahoo Weather which isn’t properly getting my location without setting the WOEID.

Mine got “a” location but not sure it was very accurate. Si I added woeid.

Was trying to make it work with dark sky, but it was partial.
Couldn’t get the visibility and forecast

Is it possible to combine the weather forecast with my own outdoor sensors? For example, I would like to display in the weather card the current temperature from my sensor and forecast from Yahoo. I tried to understand the code, but I have no experience in programming

Any idea why my svg icons are so blury???

30%20PM

how did you fixed this blank card i have the same problem

@Kem for me it was the yahoo weather component that wasn’t working. Check to see if you have weather.yweather entity_ids. You may have to specify the WOEID manually of your location.

thanks for the answer i add the woeid but stil same problem maybe is the icons
do you have to download the icons?
i did download the icons and put them in here hassio/config/www/weather_icons/animated
maybe the path is wrong?

If it’s completely blank then it’s most likely it’s not getting any weather. Did you verify you have weather.yweather entities?