Weather card without chart (#1117)

* Weather card without chart

* Lint

* Add iron-icon

* Add night icon and translation

* More translations

* Lint

* Feedback

* More Flexbox KungFu

* Finishing

* Use hass lang instead of browser lang

* Add more info card

* Lint

* Improve support for other providers

* Support non-daily forcasts

* Here we go
This commit is contained in:
c727 2018-04-26 23:16:02 +02:00 committed by Paulus Schoutsen
parent f21db486eb
commit a983a5dbc5
6 changed files with 423 additions and 208 deletions

View File

@ -15,7 +15,6 @@
"fecha": "~2.3.0", "fecha": "~2.3.0",
"font-roboto-local": "~1.0.1", "font-roboto-local": "~1.0.1",
"font-roboto": "PolymerElements/font-roboto-local#~1.0.1", "font-roboto": "PolymerElements/font-roboto-local#~1.0.1",
"google-apis": "GoogleWebComponents/google-apis#~2.0.0",
"iron-autogrow-textarea": "PolymerElements/iron-autogrow-textarea#^2.0.0", "iron-autogrow-textarea": "PolymerElements/iron-autogrow-textarea#^2.0.0",
"iron-flex-layout": "PolymerElements/iron-flex-layout#^2.0.0", "iron-flex-layout": "PolymerElements/iron-flex-layout#^2.0.0",
"iron-icon": "PolymerElements/iron-icon#^2.0.0", "iron-icon": "PolymerElements/iron-icon#^2.0.0",

View File

@ -3,7 +3,7 @@ import computeStateDomain from './compute_state_domain.js';
const DOMAINS_WITH_MORE_INFO = [ const DOMAINS_WITH_MORE_INFO = [
'alarm_control_panel', 'automation', 'camera', 'climate', 'configurator', 'alarm_control_panel', 'automation', 'camera', 'climate', 'configurator',
'cover', 'fan', 'group', 'history_graph', 'light', 'lock', 'media_player', 'script', 'cover', 'fan', 'group', 'history_graph', 'light', 'lock', 'media_player', 'script',
'sun', 'updater', 'vacuum', 'input_datetime', 'sun', 'updater', 'vacuum', 'input_datetime', 'weather'
]; ];
const HIDE_MORE_INFO = [ const HIDE_MORE_INFO = [

View File

@ -1,6 +1,5 @@
<link rel='import' href='../../bower_components/polymer/polymer-element.html'> <link rel='import' href='../../bower_components/polymer/polymer-element.html'>
<link rel='import' href='../../bower_components/iron-flex-layout/iron-flex-layout-classes.html'> <link rel='import' href='../../bower_components/iron-icon/iron-icon.html'>
<link rel='import' href='../../bower_components/google-apis/google-legacy-loader.html'>
<link rel='import' href='../components/ha-card.html'> <link rel='import' href='../components/ha-card.html'>
<link rel='import' href='../util/hass-mixins.html'> <link rel='import' href='../util/hass-mixins.html'>
@ -8,217 +7,214 @@
<dom-module id='ha-weather-card'> <dom-module id='ha-weather-card'>
<template> <template>
<style> <style>
.content { :host {
padding: 0 16px 16px; cursor: pointer;
} }
.attribution { .content {
color: var(--secondary-text-color); padding: 0 20px 20px;
text-align: right; }
}
.condicon { iron-icon {
text-align: center; color: var(--paper-item-icon-color);
font-size: 4em; }
}
.condtemp { .now {
text-align: center; display: flex;
font-size: 4em; justify-content: space-between;
} }
div.cond {
display: inline; .state {
} --iron-icon-height: 72px;
div.conddetails { --iron-icon-width: 72px;
float: right; font-size: 52px;
display: block table; }
overflow: auto;
text-align: right; .now-text {
} font-size: 24px;
span.line { }
display: block;
} .forecast {
.condsmall { margin-top: 24px;
font-size: 1em; display: flex;
} justify-content: space-between;
.clear { }
clear: both;
} .forecast div {
flex: 0 0 auto;
text-align: center;
}
.forecast .icon {
margin: 8px 0;
text-align: center;
}
.weekday {
font-weight: bold;
}
.attributes,
.templow {
color: var(--secondary-text-color);
}
</style> </style>
<google-legacy-loader on-api-load='googleApiLoaded'></google-legacy-loader> <ha-card header='[[stateObj.attributes.friendly_name]]'>
<ha-card header='[[computeTitle(stateObj)]]'>
<div class='content'> <div class='content'>
<div class='condpanel'> <div class="now">
<div class='cond condicon'>[[nowCond]]</div> <div class="state">
<div class='cond condtemp'>[[attr.temperature]]°</div> <template is="dom-if" if="[[showWeatherIcon(stateObj.state)]]">
<div class='cond conddetails'> <iron-icon icon="[[getWeatherIcon(stateObj.state)]]"></iron-icon>
<div class="condsmall" hidden$="[[!attr.wind_speed]]"> </template>
[[localize('attribute.weather.wind_speed')]]: [[getTemperature(stateObj.attributes.temperature)]]
<span hidden$="[[!attr.wind_speed]]"> </div>
[[attr.wind_speed]] <div class="attributes">
</span> <div>
<span hidden$="[[!windBearing]]">[[windBearing]]</span> [[localize('ui.card.weather.attributes.humidity')]]:
[[stateObj.attributes.humidity]] %
</div> </div>
<div class="condsmall" hidden$="[[!attr.humidity]]"> <template is="dom-if" if="[[stateObj.attributes.visibility]]">
[[localize('attribute.weather.humidity')]]: [[attr.humidity]]% <div>
</div> [[localize('ui.card.weather.attributes.visibility')]]:
<div class="condsmall" hidden$="[[!attr.visibility]]"> [[getVisibilty(stateObj.attributes.visibility)]]
[[localize('attribute.weather.visibility')]]: [[attr.visibility]] </div>
</template>
<div>
[[localize('ui.card.weather.attributes.wind_speed')]]:
[[getWind(stateObj.attributes.wind_speed, stateObj.attributes.wind_bearing, localize)]]
</div> </div>
</div> </div>
<div class='clear'></div>
</div> </div>
<div id='chart_id' hidden$="[[!attr.forecast]]"></div> <div class="now-text">[[computeState(stateObj.state, localize)]]</div>
<template is="dom-if" if="[[forecast]]">
<div class="forecast">
<template is='dom-repeat' items='[[forecast]]'>
<div>
<div class="weekday">[[computeDateTime(item.datetime)]]</div>
<template is="dom-if" if="[[item.condition]]">
<div class="icon">
<iron-icon icon="[[getWeatherIcon(item.condition)]]"></iron-icon>
</div>
</template>
<div class="temp">[[item.temperature]]°</div>
<template is="dom-if" if="[[item.templow]]">
<div class="templow">[[item.templow]]°</div>
</template>
</div>
</template>
</div>
</template>
</div> </div>
</ha-card> </ha-card>
</template> </template>
</dom-module> </dom-module>
<script> <script>
{ /*
var _WEATHER_TO_ICON = { * @appliesMixin window.hassMixins.LocalizeMixin
cloudy: '\u2601\ufe0f', */
fog: '\uD83C\uDF2B\ufe0f', class HaWeatherCard extends
hail: 'Hail', window.hassMixins.LocalizeMixin(window.hassMixins.EventsMixin(Polymer.Element)) {
lightning: '\uD83C\uDF29\ufe0f', static get is() { return 'ha-weather-card'; }
'lightning-rainy': '\u26c8\ufe0f', static get properties() {
partlycloudy: '\u26c5\ufe0f', return {
pouring: '\uD83D\uDCA7\ufe0f', hass: Object,
rainy: '\uD83C\uDF27\ufe0f', stateObj: Object,
snowy: '\uD83C\uDF28\ufe0f', forecast: {
'snowy-rainy': '\uD83C\uDF28\ufe0f', type: Array,
sunny: '\u2600\ufe0f', computed: 'computeForecast(stateObj.attributes.forecast)'
windy: '\uD83C\uDF2C\ufe0f',
'windy-variant': '\uD83C\uDF2C\ufe0f',
exceptional: '\u2b55\ufe0f',
};
var _DEGREE_TEXT = [
'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', 'N'
];
/*
* @appliesMixin window.hassMixins.LocalizeMixin
*/
class HaWeatherCard extends window.hassMixins.LocalizeMixin(Polymer.Element) {
static get is() { return 'ha-weather-card'; }
static get properties() {
return {
hass: Object,
stateObj: {
type: Object,
observer: 'checkRequirements'
},
attr: Object,
windBearing: Object,
nowCond: Object,
};
}
computeTitle(stateObj) {
return stateObj.attributes.friendly_name;
}
getDataArray() {
var dataArray = [];
var data = this.stateObj.attributes.forecast;
var i;
if (!this.stateObj.attributes.forecast) {
return [];
} }
};
for (i = 0; i < data.length; i++) { }
var d = data[i];
if (!d.condition) { constructor() {
dataArray.push([new Date(d.datetime), null, d.temperature, d.templow]); super();
} else { this.cardinalDirections = [
dataArray.push([new Date(d.datetime), _WEATHER_TO_ICON[data[i].condition], 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
d.temperature, d.templow]); 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', 'N'
} ];
} this.weatherIcons = {
'clear-night': 'mdi:weather-night',
return dataArray; cloudy: 'mdi:weather-cloudy',
} fog: 'mdi:weather-fog',
hail: 'mdi:weather-hail',
checkRequirements() { lightning: 'mid:weather-lightning',
if (!this.stateObj) { 'lightning-rainy': 'mdi:weather-lightning-rainy',
return; partlycloudy: 'mdi:weather-partlycloudy',
} pouring: 'mdi:weather-pouring',
rainy: 'mdi:weather-rainy',
if (!this.stateObj.attributes) { snowy: 'mdi:weather-snowy',
return; 'snowy-rainy': 'mdi:weather-snowy-rainy',
} sunny: 'mdi:weather-sunny',
windy: 'mdi:weather-windy',
this.attr = this.stateObj.attributes; 'windy-variant': 'mdi:weather-windy-variant'
};
this.nowCond = _WEATHER_TO_ICON[this.stateObj.state]; }
this.windBearing = this.windBearingToText(this.attr.wind_bearing);
ready() {
if (!window.google || !window.google.visualization) { this.addEventListener('click', this._onClick);
return; super.ready();
} }
if (!this.chartEngine) { _onClick() {
this.chartEngine = new window.google.visualization.LineChart(this.$.chart_id); this.fire('hass-more-info', { entityId: this.stateObj.entity_id });
} }
if (!this.attr.forecast) { computeForecast(forecast) {
return; return forecast && forecast.slice(0, 7);
} }
this.drawChart(); getUnit(unit) {
} return this.hass.config.core.unit_system[unit] || '';
drawChart() { }
var dataGrid = new window.google.visualization.DataTable();
var options = { computeState(state, localize) {
legend: { return localize(`state.weather.${state.replace('-', '_')}`) || state;
position: 'top' }
},
interpolateNulls: true, showWeatherIcon(condition) {
titlePosition: 'none', return condition in this.weatherIcons;
chartArea: { }
left: 25,
top: 5, getWeatherIcon(condition) {
height: '100%', return this.weatherIcons[condition];
width: '90%', }
bottom: 25,
}, getTemperature(temp) {
curveType: 'function', return `${temp}${this.getUnit('temperature')}`;
focusTarget: 'category', }
colors: ['red', 'blue'],
annotations: { windBearingToText(degree) {
textStyle: { const degreenum = parseInt(degree);
fontSize: '24', if (isFinite(degreenum)) {
} return this.cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
} }
}; return degree;
}
dataGrid.addColumn('datetime', 'Time');
dataGrid.addColumn({ type: 'string', role: 'annotation' }); getWind(speed, bearing, localize) {
dataGrid.addColumn('number', 'Temperature'); if (bearing != null) {
dataGrid.addColumn('number', 'Temperature Low'); const cardinalDirection = this.windBearingToText(bearing);
return `${speed} ${this.getUnit('length')}/h (${localize(cardinalDirection.toLowerCase()) || cardinalDirection})`;
var dataArray = this.getDataArray(); }
dataGrid.addRows(dataArray); return `${speed} ${this.getUnit('length')}/h`;
}
this.chartEngine.draw(dataGrid, options);
} getVisibilty(visibilty) {
return `${visibilty} ${this.getUnit('length')}`;
googleApiLoaded() { }
window.google.load('visualization', '1', {
packages: ['corechart'], computeDateTime(data) {
callback: function () { const date = new Date(data);
this.checkRequirements(); const provider = this.stateObj.attributes.attribution;
}.bind(this), if (provider === 'Powered by Dark Sky' || provider === 'Data provided by OpenWeatherMap') {
}); return date.toLocaleTimeString(
} this.hass.selectedLanguage || this.hass.language,
{ hour: 'numeric' }
windBearingToText(degree) { );
var degreenum = parseInt(degree); }
if (isFinite(degreenum)) { return date.toLocaleDateString(this.hass.selectedLanguage || this.hass.language, { weekday: 'short' });
return _DEGREE_TEXT[(((degreenum + 11.25) / 22.5) | 0) % 16];
}
return '';
}
} }
customElements.define(HaWeatherCard.is, HaWeatherCard);
} }
customElements.define(HaWeatherCard.is, HaWeatherCard);
</script> </script>

View File

@ -18,6 +18,7 @@
<link rel='import' href='more-info-updater.html'> <link rel='import' href='more-info-updater.html'>
<link rel='import' href='more-info-vacuum.html'> <link rel='import' href='more-info-vacuum.html'>
<link rel='import' href='more-info-input_datetime.html'> <link rel='import' href='more-info-input_datetime.html'>
<link rel='import' href='more-info-weather.html'>
<script> <script>
class MoreInfoContent extends Polymer.Element { class MoreInfoContent extends Polymer.Element {

View File

@ -0,0 +1,182 @@
<link rel='import' href='../../../../bower_components/polymer/polymer-element.html'>
<link rel='import' href='../../../../bower_components/iron-icon/iron-icon.html'>
<link rel='import' href='../../../util/hass-mixins.html'>
<dom-module id='more-info-weather'>
<template>
<style>
iron-icon {
color: var(--paper-item-icon-color);
}
.section {
margin: 16px 0 8px 0;
font-size: 1.2em;
}
.flex {
display: flex;
height: 32px;
align-items: center;
}
.main {
flex: 1;
margin-left: 24px;
}
.temp,
.templow {
min-width: 48px;
text-align: right;
}
.templow {
margin: 0 16px;
color: var(--secondary-text-color);
}
.attribution {
color: var(--secondary-text-color);
text-align: center;
}
</style>
<div class="flex">
<iron-icon icon="mdi:thermometer"></iron-icon>
<div class="main">[[localize('ui.card.weather.attributes.temperature')]]</div>
<div>[[stateObj.attributes.temperature]] [[getUnit('temperature')]]</div>
</div>
<div class="flex">
<iron-icon icon="mdi:water-percent"></iron-icon>
<div class="main">[[localize('ui.card.weather.attributes.humidity')]]</div>
<div>[[stateObj.attributes.humidity]] %</div>
</div>
<template is="dom-if" if="[[stateObj.attributes.visibility]]">
<div class="flex">
<iron-icon icon="mdi:eye"></iron-icon>
<div class="main">[[localize('ui.card.weather.attributes.visibility')]]</div>
<div>[[stateObj.attributes.visibility]] [[getUnit('length')]]</div>
</div>
</template>
<div class="flex">
<iron-icon icon="mdi:weather-windy"></iron-icon>
<div class="main">[[localize('ui.card.weather.attributes.wind_speed')]]</div>
<div>[[getWind(stateObj.attributes.wind_speed, stateObj.attributes.wind_bearing, localize)]]</div>
</div>
<template is="dom-if" if="[[stateObj.attributes.pressure]]">
<div class="flex">
<iron-icon icon="mdi:gauge"></iron-icon>
<div class="main">[[localize('ui.card.weather.attributes.air_pressure')]]</div>
<div>[[stateObj.attributes.pressure]] hPa</div>
</div>
</template>
<template is="dom-if" if="[[stateObj.attributes.forecast]]">
<div class="section">[[localize('ui.card.weather.forecast')]]:</div>
<template is='dom-repeat' items='[[stateObj.attributes.forecast]]'>
<div class="flex">
<template is="dom-if" if="[[item.condition]]">
<iron-icon icon="[[getWeatherIcon(item.condition)]]"></iron-icon>
</template>
<div class="main">[[computeDateTime(item.datetime)]]</div>
<template is="dom-if" if="[[item.templow]]">
<div class="templow">[[item.templow]] [[getUnit('temperature')]]</div>
</template>
<div class="temp">[[item.temperature]] [[getUnit('temperature')]]</div>
</div>
</template>
</template>
<template is="dom-if" if="stateObj.attributes.attribution">
<div class="attribution">[[stateObj.attributes.attribution]]</div>
</template>
</template>
</dom-module>
<script>
/*
* @appliesMixin window.hassMixins.LocalizeMixin
*/
class MoreInfoWeather extends window.hassMixins.LocalizeMixin(Polymer.Element) {
static get is() { return 'more-info-weather'; }
static get properties() {
return {
hass: Object,
stateObj: Object
};
}
constructor() {
super();
this.cardinalDirections = [
'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', 'N'
];
this.weatherIcons = {
'clear-night': 'mdi:weather-night',
cloudy: 'mdi:weather-cloudy',
fog: 'mdi:weather-fog',
hail: 'mdi:weather-hail',
lightning: 'mid:weather-lightning',
'lightning-rainy': 'mdi:weather-lightning-rainy',
partlycloudy: 'mdi:weather-partlycloudy',
pouring: 'mdi:weather-pouring',
rainy: 'mdi:weather-rainy',
snowy: 'mdi:weather-snowy',
'snowy-rainy': 'mdi:weather-snowy-rainy',
sunny: 'mdi:weather-sunny',
windy: 'mdi:weather-windy',
'windy-variant': 'mdi:weather-windy-variant'
};
}
computeDateTime(data) {
const date = new Date(data);
const provider = this.stateObj.attributes.attribution;
if (provider === 'Powered by Dark Sky' || provider === 'Data provided by OpenWeatherMap') {
if (new Date().getDay() === date.getDay()) {
return date.toLocaleTimeString(
this.hass.selectedLanguage || this.hass.language,
{ hour: 'numeric' }
);
}
return date.toLocaleDateString(
this.hass.selectedLanguage || this.hass.language,
{ weekday: 'long', hour: 'numeric' }
);
}
return date.toLocaleDateString(
this.hass.selectedLanguage || this.hass.language,
{ weekday: 'long', month: 'short', day: 'numeric' }
);
}
getUnit(unit) {
return this.hass.config.core.unit_system[unit] || '';
}
windBearingToText(degree) {
const degreenum = parseInt(degree);
if (isFinite(degreenum)) {
return this.cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
}
return degree;
}
getWind(speed, bearing, localize) {
if (bearing != null) {
const cardinalDirection = this.windBearingToText(bearing);
return `${speed} ${this.getUnit('length')}/h (${localize(cardinalDirection.toLowerCase()) || cardinalDirection})`;
}
return `${speed} ${this.getUnit('length')}/h`;
}
getWeatherIcon(condition) {
return this.weatherIcons[condition];
}
}
customElements.define(MoreInfoWeather.is, MoreInfoWeather);
</script>

View File

@ -1,11 +1,4 @@
{ {
"attribute": {
"weather": {
"humidity": "Humidity",
"visibility": "Visibility",
"wind_speed": "Wind speed"
}
},
"domain": { "domain": {
"alarm_control_panel": "Alarm control panel", "alarm_control_panel": "Alarm control panel",
"automation": "Automation", "automation": "Automation",
@ -269,6 +262,22 @@
"off": "[%key:state::default::off%]", "off": "[%key:state::default::off%]",
"on": "[%key:state::default::on%]" "on": "[%key:state::default::on%]"
}, },
"weather": {
"clear_night": "Clear, night",
"cloudy": "Cloudy",
"fog": "Fog",
"hail": "Hail",
"lightning": "Lightning",
"lightning_rainy": "Lightning, rainy",
"partlycloudy": "Partly cloudy",
"pouring": "Pouring",
"rainy": "Rainy",
"snowy": "Snowy",
"snowy_rainy": "Snowy, rainy",
"sunny": "Sunny",
"windy": "Windy",
"windy_variant": "Windy"
},
"zwave": { "zwave": {
"default": { "default": {
"initializing": "Initializing", "initializing": "Initializing",
@ -317,6 +326,34 @@
}, },
"script": { "script": {
"execute": "Execute" "execute": "Execute"
},
"weather": {
"attributes": {
"air_pressure": "Air pressure",
"humidity": "Humidity",
"temperature": "Temperature",
"visibility": "Visibility",
"wind_speed": "Wind speed"
},
"cardinal_direction": {
"e": "E",
"ene": "ENE",
"ese": "ESE",
"n": "N",
"ne": "NE",
"nne": "NNE",
"nw": "NW",
"nnw": "NNW",
"s": "S",
"se": "SE",
"sse": "SSE",
"ssw": "SSW",
"sw": "SW",
"w": "W",
"wnw": "WNW",
"wsw": "WSW"
},
"forecast": "Forecast"
} }
}, },
"common": { "common": {