Home Assistant 系列美化篇 —— 替换天气 UI

Home Assistant 原生的天气平台不少,国内用户常用的有雅虎天气和 Darksky。其他论坛和社区也有分享自制的和风、彩云天气等。

依托国内天气服务商的组件优点自然是数据比较丰富,但前提是找服务商获取了 API。不过 API 的获取现在越来越难了,少不了隐私信息的提供,当然也可以通过抓包获取公共 API,但是持续有效性存疑。其实原生的天气平台本身信息并不少,数据出入不大,基本无需 API,即使需要流程也很简单。

原生天气组件生成的卡片 UI 实在是丑,雅虎的稍好一点懂得放大温度数字,但是同样没有完整的信息展示。而 Darksky 的 weather 组件信息丰富性远远比不上同款 sensor 组件。

官方英文论坛的 Bram Kragten 开发了一套天气组件使用的 UI(论坛讨论 见此 ),赏心悦目,国内爱好者也有利用此套 UI 做了魔改的天气组件。当然别人魔改的作品不在此文讨论内,这里讲一下在接入 Home Assistant 原生的天气平台 weather 下 (推荐雅虎 yweather),如何使用这套 UI。

首先,在 /www/custom_ui/ 文件夹下创建名为 custom-weather-card.html 的文件,没有相关文件夹请自行新建,文件内容为:(部分值已由我人工汉化过了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
<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: 4em;
color:#5b5b5b;
position: absolute;
right: .5em;
}
.tempc {
font-weight: 300;
font-size: 1.5em;
vertical-align: super;
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]]</span><span class="tempc">&#176;C</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> [[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>
</ul>
<ul class="variations">
<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>
<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>
</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;C</span>
</template>
<template is="dom-if" if="[[item.tempLow]]">
<br> <span class="lowTemp">[[item.tempLow]]&#176;C</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: '多云',
fog: '雾',
hail: '冰雹',
lightning: '雷电',
'lightning-rainy': '雷雨',
partlycloudy: '局部多云',
pouring: '大雨',
rainy: '雨',
snowy: '雪',
'snowy-rainy': '雨夹雪',
sunny: '晴',
windy: '多风',
'windy-variant': '阵风',
exceptional: '!',
};
var _DEGREE_TEXT = [
'北', '东北偏北', '东北', '东北偏东', '东', '东南偏东', '东南', '东南偏南',
'南', '西南偏南', '西南', '西南偏西', '西', '西北偏西', '西北', '西北偏北'
];
var _DAY_TO_DAY = {
Mon: '周一',
Tue: '周二',
Wed: '周三',
Thu: '周四',
Fri: '周五',
Sat: '周六',
Sun: '周日',
};
Polymer({
is: 'custom-weather-card',
properties: {
hass: {
type: Object,
},
stateObj: {
type: Object,
},
weatherObj: {
type: Object,
observer: 'checkRequirements',
computed: 'computeWeatherObj(hass, stateObj)',
},
},
computeWeatherObj: 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;
}
if (tempHigh == 0) {
tempHigh = '0'; // otherwise the value 0 will not be shown on the weather card
}
var tempLow = Math.round(data[i].templow * 10) / 10;
if (tempLow == 0) {
tempLow = '0'; // otherwise the value 0 will not be shown on the weather card
}
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.wind_bearing) {
this.windBearing = this.windBearingToText(this.weatherObj.attributes.wind_bearing);
}
this.forecast = this.getForecastArray().slice(0, 4);
},
windBearingToText: function (degree) {
// return _DEGREE_TEXT[((parseInt(degree) + 5.63) / 11.25) | 0];
return _DEGREE_TEXT[(parseInt((degree + 11.25) / 22.5))];
},
});
}());
</script>

接着,下载全套天气图标,解压后的 svg 文件放入 \www\weather_icons\animated\ 文件夹内。

最后,在配置文件 configuration.yaml 中添加相关设置(文档分离用户请自行判断):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#接入雅虎天气
weather:
- platform: yweather
woeid:
#引入引导组件
input_boolean:
weather:
#前端接入自定义UI
frontend:
extra_html_url:
- /local/custom_ui/custom-weather-card.html
extra_html_url_es5:
- /local/custom_ui/custom-weather-card.html
#调用 UI
customize:
input_boolean.weather:
custom_ui_state_card: custom-weather-card
config:
weather: weather.yweather
weather.yweather: #隐藏原 UI 组件
hidden: true
#合成群组
group:
weather:
view: no
name: 天气
entities:
- input_boolean.weather

上述操作完成后,重启 Home Assistant,你的天气卡片就焕然一新了~~

cxlwill wechat
欢迎扫码订阅“墨澜工作室”公众号,获取最新文章推送。