hassio-conf/www/plugins/weather-card.js
2024-10-17 21:45:45 +02:00

531 lines
13 KiB
JavaScript

const LitElement = customElements.get("hui-masonry-view") ? Object.getPrototypeOf(customElements.get("hui-masonry-view")) : Object.getPrototypeOf(customElements.get("hui-view"));
const html = LitElement.prototype.html;
const css = LitElement.prototype.css;
const weatherIconsDay = {
clear: "day",
"clear-night": "night",
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: "!!",
};
const weatherIconsNight = {
...weatherIconsDay,
clear: "night",
sunny: "night",
partlycloudy: "cloudy-night-3",
"windy-variant": "cloudy-night-3",
};
const windDirections = [
"N",
"NNE",
"NE",
"ENE",
"E",
"ESE",
"SE",
"SSE",
"S",
"SSW",
"SW",
"WSW",
"W",
"WNW",
"NW",
"NNW",
"N",
];
window.customCards = window.customCards || [];
window.customCards.push({
type: "weather-card",
name: "Weather Card",
description: "A custom weather card with animated icons.",
preview: true,
documentationURL: "https://github.com/bramkragten/weather-card",
});
const fireEvent = (node, type, detail, options) => {
options = options || {};
detail = detail === null || detail === undefined ? {} : detail;
const event = new Event(type, {
bubbles: options.bubbles === undefined ? true : options.bubbles,
cancelable: Boolean(options.cancelable),
composed: options.composed === undefined ? true : options.composed,
});
event.detail = detail;
node.dispatchEvent(event);
return event;
};
function hasConfigOrEntityChanged(element, changedProps) {
if (changedProps.has("_config")) {
return true;
}
const oldHass = changedProps.get("hass");
if (oldHass) {
return (
oldHass.states[element._config.entity] !==
element.hass.states[element._config.entity] ||
oldHass.states["sun.sun"] !== element.hass.states["sun.sun"]
);
}
return true;
}
class WeatherCard extends LitElement {
static get properties() {
return {
_config: {},
hass: {},
};
}
static async getConfigElement() {
await import("./weather-card-editor.js");
return document.createElement("weather-card-editor");
}
static getStubConfig(hass, unusedEntities, allEntities) {
let entity = unusedEntities.find((eid) => eid.split(".")[0] === "weather");
if (!entity) {
entity = allEntities.find((eid) => eid.split(".")[0] === "weather");
}
return { entity };
}
setConfig(config) {
if (!config.entity) {
throw new Error("Please define a weather entity");
}
this._config = config;
}
shouldUpdate(changedProps) {
return hasConfigOrEntityChanged(this, changedProps);
}
render() {
if (!this._config || !this.hass) {
return html``;
}
this.numberElements = 0;
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
return html`
<style>
.not-found {
flex: 1;
background-color: yellow;
padding: 8px;
}
</style>
<ha-card>
<div class="not-found">
Entity not available: ${this._config.entity}
</div>
</ha-card>
`;
}
return html`
<ha-card @click="${this._handleClick}">
${this._config.current !== false ? this.renderCurrent(stateObj) : ""}
${this._config.details !== false ? this.renderDetails(stateObj) : ""}
${this._config.forecast !== false
? this.renderForecast(stateObj.attributes.forecast)
: ""}
</ha-card>
`;
}
renderCurrent(stateObj) {
this.numberElements++;
return html`
<div class="current ${this.numberElements > 1 ? "spacer" : ""}">
<span
class="icon bigger"
style="background: none, url('${this.getWeatherIcon(
stateObj.state.toLowerCase(),
this.hass.states["sun.sun"]
)}') no-repeat; background-size: contain;"
>${stateObj.state}
</span>
${this._config.name
? html` <span class="title"> ${this._config.name} </span> `
: ""}
<span class="temp"
>${this.getUnit("temperature") == "°F"
? Math.round(stateObj.attributes.temperature)
: stateObj.attributes.temperature}</span
>
<span class="tempc"> ${this.getUnit("temperature")}</span>
</div>
`;
}
renderDetails(stateObj) {
const sun = this.hass.states["sun.sun"];
let next_rising;
let next_setting;
if (sun) {
next_rising = new Date(sun.attributes.next_rising);
next_setting = new Date(sun.attributes.next_setting);
}
this.numberElements++;
return html`
<ul class="variations ${this.numberElements > 1 ? "spacer" : ""}">
<li>
<ha-icon icon="mdi:water-percent"></ha-icon>
${stateObj.attributes.humidity}<span class="unit"> % </span>
</li>
<li>
<ha-icon icon="mdi:weather-windy"></ha-icon> ${windDirections[
parseInt((stateObj.attributes.wind_bearing + 11.25) / 22.5)
]}
${stateObj.attributes.wind_speed}<span class="unit">
${this.getUnit("length")}/h
</span>
</li>
<li>
<ha-icon icon="mdi:gauge"></ha-icon>
${stateObj.attributes.pressure}
<span class="unit">
${this.getUnit("air_pressure")}
</span>
</li>
<li>
<ha-icon icon="mdi:weather-fog"></ha-icon> ${stateObj.attributes
.visibility}<span class="unit">
${this.getUnit("length")}
</span>
</li>
${next_rising
? html`
<li>
<ha-icon icon="mdi:weather-sunset-up"></ha-icon>
${next_rising.toLocaleTimeString()}
</li>
`
: ""}
${next_setting
? html`
<li>
<ha-icon icon="mdi:weather-sunset-down"></ha-icon>
${next_setting.toLocaleTimeString()}
</li>
`
: ""}
</ul>
`;
}
renderForecast(forecast) {
if (!forecast || forecast.length === 0) {
return html``;
}
const lang = this.hass.selectedLanguage || this.hass.language;
this.numberElements++;
return html`
<div class="forecast clear ${this.numberElements > 1 ? "spacer" : ""}">
${forecast
.slice(
0,
this._config.number_of_forecasts
? this._config.number_of_forecasts
: 5
)
.map(
(daily) => html`
<div class="day">
<div class="dayname">
${this._config.hourly_forecast
? new Date(daily.datetime).toLocaleTimeString(lang, {
hour: "2-digit",
minute: "2-digit",
})
: new Date(daily.datetime).toLocaleDateString(lang, {
weekday: "short",
})}
</div>
<i
class="icon"
style="background: none, url('${this.getWeatherIcon(
daily.condition.toLowerCase()
)}') no-repeat; background-size: contain"
></i>
<div class="highTemp">
${daily.temperature}${this.getUnit("temperature")}
</div>
${daily.templow !== undefined
? html`
<div class="lowTemp">
${daily.templow}${this.getUnit("temperature")}
</div>
`
: ""}
${!this._config.hide_precipitation &&
daily.precipitation !== undefined &&
daily.precipitation !== null
? html`
<div class="precipitation">
${Math.round(daily.precipitation*10)/10} ${this.getUnit("precipitation")}
</div>
`
: ""}
${!this._config.hide_precipitation &&
daily.precipitation_probability !== undefined &&
daily.precipitation_probability !== null
? html`
<div class="precipitation_probability">
${Math.round(daily.precipitation_probability)} ${this.getUnit("precipitation_probability")}
</div>
`
: ""}
</div>
`
)}
</div>
`;
}
getWeatherIcon(condition, sun) {
return `${
this._config.icons
? this._config.icons
: "https://cdn.jsdelivr.net/gh/bramkragten/weather-card/dist/icons/"
}${
sun && sun.state == "below_horizon"
? weatherIconsNight[condition]
: weatherIconsDay[condition]
}.svg`;
}
getUnit(measure) {
const lengthUnit = this.hass.config.unit_system.length;
switch (measure) {
case "air_pressure":
return lengthUnit === "km" ? "hPa" : "inHg";
case "length":
return lengthUnit;
case "precipitation":
return lengthUnit === "km" ? "mm" : "in";
case "precipitation_probability":
return "%";
default:
return this.hass.config.unit_system[measure] || "";
}
}
_handleClick() {
fireEvent(this, "hass-more-info", { entityId: this._config.entity });
}
getCardSize() {
return 3;
}
static get styles() {
return css`
ha-card {
cursor: pointer;
margin: auto;
overflow: hidden;
padding-top: 1.3em;
padding-bottom: 1.3em;
padding-left: 1em;
padding-right: 1em;
position: relative;
}
.spacer {
padding-top: 1em;
}
.clear {
clear: both;
}
.title {
position: absolute;
left: 3em;
font-weight: 300;
font-size: 3em;
color: var(--primary-text-color);
}
.temp {
font-weight: 300;
font-size: 4em;
color: var(--primary-text-color);
position: absolute;
right: 1em;
}
.tempc {
font-weight: 300;
font-size: 1.5em;
vertical-align: super;
color: var(--primary-text-color);
position: absolute;
right: 1em;
margin-top: -14px;
margin-right: 7px;
}
@media (max-width: 460px) {
.title {
font-size: 2.2em;
left: 4em;
}
.temp {
font-size: 3em;
}
.tempc {
font-size: 1em;
}
}
.current {
padding: 1.2em 0;
margin-bottom: 3.5em;
}
.variations {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
font-weight: 300;
color: var(--primary-text-color);
list-style: none;
padding: 0 1em;
margin: 0;
}
.variations ha-icon {
height: 22px;
margin-right: 5px;
color: var(--paper-item-icon-color);
}
.variations li {
flex-basis: auto;
width: 50%;
}
.variations li:nth-child(2n) {
text-align: right;
}
.variations li:nth-child(2n) ha-icon {
margin-right: 0;
margin-left: 8px;
float: right;
}
.unit {
font-size: 0.8em;
}
.forecast {
width: 100%;
margin: 0 auto;
display: flex;
}
.day {
flex: 1;
display: block;
text-align: center;
color: var(--primary-text-color);
border-right: 0.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(1) {
border-right: none;
margin-right: 0;
}
.highTemp {
font-weight: bold;
}
.lowTemp {
color: var(--secondary-text-color);
}
.precipitation {
color: var(--primary-text-color);
font-weight: 300;
}
.icon.bigger {
width: 10em;
height: 10em;
margin-top: -4em;
position: absolute;
left: 0em;
}
.icon {
width: 50px;
height: 50px;
margin-right: 5px;
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: var(--primary-text-color);
text-align: left;
position: absolute;
top: -0.5em;
left: 6em;
word-wrap: break-word;
width: 30%;
}
`;
}
}
customElements.define("weather-card", WeatherCard);