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: "fog", hail: "rainy-7", lightning: "thunder", "lightning-rainy": "lightning-rainy", partlycloudy: "cloudy-day-3", pouring: "rainy-6", rainy: "rainy-5", snowy: "snowy-6", "snowy-rainy": "snowy-rainy", sunny: "day", windy: "windy", "windy-variant": "windy", exceptional: "!!", }; const DefaultSensors = [ ["cloudCoverEntity", "_cloud_cover"], ["rainChanceEntity", "_rain_chance"], ["freezeChanceEntity", "_freeze_chance"], ["snowChanceEntity", "_snow_chance"], ["uvEntity", "_uv"], ["rainForecastEntity", "_next_rain"], ]; const weatherIconsNight = { ...weatherIconsDay, clear: "night", sunny: "night", partlycloudy: "cloudy-night-3", }; const windDirections = [ "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSO", "SO", "OSO", "O", "ONO", "NO", "NNO", "N", ]; const phenomenaText = { clear: "Ciel dégagé", "clear-night": "Nuit claire", cloudy: "Nuageux", fog: "Brouillard", hail: "Risque de grèle", lightning: "Orages", "lightning-rainy": "Pluies orageuses", partlycloudy: "Eclaircies", pouring: "Pluie forte", rainy: "Pluie", snowy: "Neige", "snowy-rainy": "Pluie verglaçante", sunny: "Ensoleillé", windy: "Venteux", "windy-variant": "Venteux variable", exceptional: "Exceptionnel", }; const phenomenaNightText = { ...phenomenaText, sunny: "Nuit claire", }; const rainForecastValues = new Map([ ["Pas de valeur", 0.1], ["Temps sec", 0.1], ["Pluie faible", 0.4], ["Pluie modérée", 0.7], ["Pluie forte", 1], ]); window.customCards = window.customCards || []; window.customCards.push({ type: "meteofrance-weather-card", name: "Carte Météo France par HACF", description: "Carte pour l'intégration Météo France.", preview: true, documentationURL: "https://github.com/hacf-fr/lovelace-meteofrance-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) { const entityName = element._config.entity.split(".")[1]; return ( oldHass.states[element._config.entity] !== element.hass.states[element._config.entity] || oldHass.states["sun.sun"] !== element.hass.states["sun.sun"] || !DefaultSensors.every((sensor) => { const sensorName = "sensor." + entityName + sensor[1]; oldHass.states[sensorName] == element.hass.states[sensorName]; }) ); } return true; } class MeteofranceWeatherCard extends LitElement { static get properties() { return { _config: {}, hass: {}, }; } static async getConfigElement() { await import("./meteofrance-weather-card-editor.js"); return document.createElement("meteofrance-weather-card-editor"); } static getStubConfig(hass, unusedEntities, allEntities) { let entity = this.getDefaultWeatherEntity(unusedEntities, allEntities); let entities = { entity }; if (entity) { let sensors = this.getWeatherEntitiesFromEntity( hass, entity.split(".")[1], allEntities ); entities = { ...entities, ...sensors, }; } return entities; } static getDefaultWeatherEntity(unusedEntities, allEntities) { let entity = unusedEntities.find((eid) => eid.split(".")[0] === "weather"); if (!entity) { entity = allEntities.find((eid) => eid.split(".")[0] === "weather"); } return entity; } static getWeatherEntitiesFromEntity(hass, entityName, allEntities) { let entities = {}; DefaultSensors.forEach((sensor) => { const sensorName = "sensor." + entityName + sensor[0]; if (hass.states[sensorName] !== undefined) { let sensor = allEntities[sensorName]; if (!sensor) { entities = { ...entities, [sensor[1]]: sensorName, }; } } }); return entities; } setConfig(config) { if (!config.entity) { throw new Error("Please define a weather entity"); } this._config = config; } shouldUpdate(changedProps) { return hasConfigOrEntityChanged(this, changedProps); } isSelected(option) { return option === undefined || option === true; } render() { if (!this._config || !this.hass) { return html``; } this.numberElements = 0; const stateObj = this.hass.states[this._config.entity]; if (!stateObj) { return html`
Entity not available: ${this._config.entity}
`; } return html` ${this.isSelected(this._config.current) ? this.renderCurrent(stateObj) : ""} ${this.isSelected(this._config.details) ? this.renderDetails(stateObj) : ""} ${this.isSelected(this._config.alert_forecast) ? this.renderAlertForecast() : ""} ${this.isSelected(this._config.one_hour_forecast) ? this.renderOneHourForecast() : ""} ${this.isSelected(this._config.forecast) ? this.renderForecast(stateObj.attributes.forecast) : ""} `; } renderCurrent(stateObj) { this.numberElements++; return html` `; } 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` `; } renderMeteoFranceDetail(entity) { return entity !== undefined ? this.renderDetail( entity.state, entity.attributes.friendly_name, entity.attributes.icon, entity.attributes.unit_of_measurement ) : ""; } renderDetail(state, label, icon, unit) { return html`
  • ${state} ${unit ? html`${unit}` : ""}
  • `; } renderOneHourForecast() { const rainForecast = this.hass.states[this._config.rainForecastEntity]; if (!rainForecast || rainForecast.length === 0) { return html``; } this.numberElements++; let [startTime, endTime] = this.getOneHourForecastTime(rainForecast); return html` `; } renderAlertForecast() { const alertForecast = this.hass.states[this._config.alertEntity]; if (!alertForecast) { return html``; } const alerts = this.getAlertForecast(alertForecast); if (alerts.length == 0) return html``; this.numberElements++; return html`
    ${alerts.map( (phenomenon) => html`
    ` )}
    `; } renderForecast(forecast) { if (!forecast || forecast.length === 0) { return html``; } const lang = this.hass.selectedLanguage || this.hass.language; const isDaily = this.isDailyForecast(forecast); this.numberElements++; return html` `; } renderDailyForecast(daily, lang, isDaily) { return html`
  • `; } isDailyForecast(forecast) { const diff = new Date(forecast[1].datetime) - new Date(forecast[0].datetime); return diff > 3600000; } isNightTime(datetimehourly) { const sun = this.hass.states["sun.sun"]; if (!sun) { return false; } let nextrising = new Date(sun.attributes.next_rising); let nextsetting = new Date(sun.attributes.next_setting); const thistime = datetimehourly ? new Date(datetimehourly) : new Date(); return ( (thistime > nextsetting && thistime < nextrising) || (thistime < nextsetting && thistime < nextrising && nextrising < nextsetting) ); } getOneHourForecast(rainForecastEntity) { let rainForecastList = []; for (let [time, value] of Object.entries( rainForecastEntity.attributes["1_hour_forecast"] )) { if (time != undefined && time.match(/[0-9]*min/g)) { time = time.replace("min", "").trim(); rainForecastList.push([time, rainForecastValues.get(value), value]); } } return rainForecastList; } getOneHourForecastTime(rainForecastEntity) { let rainForecastTimeRef = new Date( rainForecastEntity.attributes["forecast_time_ref"] ); let rainForecastStartTime = rainForecastTimeRef.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); rainForecastTimeRef.setHours(rainForecastTimeRef.getHours() + 1); let rainForecastEndTime = rainForecastTimeRef.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); return [rainForecastStartTime, rainForecastEndTime]; } getOneHourNextRainText(rainForecastEntity) { for (let [time, value] of Object.entries( rainForecastEntity.attributes["1_hour_forecast"] )) { if (time != undefined && rainForecastValues.get(value) > 0.1) { let timeStr = time.replace(/([345])5/g, "$10"); return ( value + (time == "0 min" ? " actuellement." : " dans " + timeStr + ".") ); } } return "Pas de pluie dans l'heure."; } getAlertForecast(alertEntity) { let phenomenaList = []; if (alertEntity == undefined) { return []; } if ( !this._config.hide_alertVentViolent && alertEntity.attributes["Vent violent"] ) { phenomenaList.push({ name: "Vent violent", icon: "mdi:weather-windy", color: alertEntity.attributes["Vent violent"], }); } if ( !this._config.hide_alertPluieInondation && alertEntity.attributes["Pluie-inondation"] ) { phenomenaList.push({ name: "Pluie-inondation", icon: "mdi:weather-pouring", color: alertEntity.attributes["Pluie-inondation"], }); } if (!this._config.hide_alertOrages && alertEntity.attributes["Orages"]) { phenomenaList.push({ name: "Orages", icon: "mdi:weather-lightning", color: alertEntity.attributes["Orages"], }); } if ( !this._config.hide_alertInondation && alertEntity.attributes["Inondation"] ) { phenomenaList.push({ name: "Inondation", icon: "mdi:home-flood", color: alertEntity.attributes["Inondation"], }); } if ( !this._config.hide_alertNeigeVerglas && alertEntity.attributes["Neige-verglas"] ) { phenomenaList.push({ name: "Neige-verglas", icon: "mdi:weather-snowy-heavy", color: alertEntity.attributes["Neige-verglas"], }); } if ( !this._config.hide_alertCanicule && alertEntity.attributes["Canicule"] ) { phenomenaList.push({ name: "Canicule", icon: "mdi:weather-sunny-alert", color: alertEntity.attributes["Canicule"], }); } if ( !this._config.hide_alertGrandFroid && alertEntity.attributes["Grand-froid"] ) { phenomenaList.push({ name: "Grand-froid", icon: "mdi:snowflake", color: alertEntity.attributes["Grand-froid"], }); } if ( !this._config.hide_alertAvalanches && alertEntity.attributes["Avalanches"] ) { phenomenaList.push({ name: "Avalanches", icon: "mdi:image-filter-hdr", color: alertEntity.attributes["Avalanches"], }); } if ( !this._config.hide_alertVaguesSubmersion && alertEntity.attributes["Vagues-submersion"] ) { phenomenaList.push({ name: "Vagues-submersion", icon: "mdi:waves", color: alertEntity.attributes["Vagues-submersion"], }); } return phenomenaList; } getWeatherIcon(condition, isNight) { return `${ this._config.icons ? this._config.icons : "/local/community/lovelace-meteofrance-weather-card/icons/" }${isNight ? weatherIconsNight[condition] : weatherIconsDay[condition]}${ this.isSelected(this._config.animated_icons) ? "" : "-static" }.svg`; } getPhenomenaText(phenomena, isNight) { return `${ isNight ? phenomenaNightText[phenomena] : phenomenaText[phenomena] }`; } 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 "%"; case "speed": return lengthUnit === "km" ? "km/h" : "mph"; 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: 0.5em 1em; position: relative; } ha-card ul { list-style: none; padding: 0; margin: 0; } .spacer { padding-top: 1em; } .clear { clear: both; } .flow-row { display: flex; flex-flow: row wrap; } .flow-column { display: flex; flex-flow: column wrap; } .ha-icon { height: 0.8em; margin-right: 5px; color: var(--paper-item-icon-color); } /* Current Forecast */ .current { flex-wrap: nowrap; } .current > *:first-child { min-width: 100px; height: 100px; margin-right: 10px; } .current > *:last-child { margin-left: auto; min-width: max-content; text-align: right; } .current > *:last-child sup { font-size: initial; } .current > li { font-size: 2em; line-height: 1.2; align-self: center; } .current > li > *:last-child { line-height: 1; font-size: 0.6em; color: var(--secondary-text-color); } /* Details */ .details { justify-content: space-between; font-weight: 300; } .details ha-icon { height: 22px; margin-right: 5px; color: var(--paper-item-icon-color); } .details > li { flex-basis: auto; width: 50%; } .details > li:nth-child(2n) { text-align: right; } .details > li:nth-child(2n) ha-icon { margin-right: 0; margin-left: 8px; float: right; } /* One Hour Forecast */ .oneHour { height: 1em; } .oneHour > li { background-color: var(--paper-item-icon-color); border-right: 1px solid var(--lovelace-background, var(--primary-background-color)); } .oneHour > li:first-child { border-top-left-radius: 5px; border-bottom-left-radius: 5px; } .oneHour > li:last-child { border-top-right-radius: 5px; border-bottom-right-radius: 5px; border: 0; } /* One Hour Labels */ .rain-0min, .rain-5min, .rain-10min, .rain-15min, .rain-20min, .rain-25min { flex: 1 1 0; } .rain-35min, .rain-45min, .rain-55min { flex: 2 1 0; } .oneHourLabel > li { flex: 1 1 0; } /* One Hour Header */ .oneHourHeader { justify-content: space-between; } .oneHourHeader li:last-child { text-align: right; } /* Alert */ .alertForecast { text-align: center; flex-wrap: nowrap; } .alertForecast > div { flex: 1; color: var(--paper-item-icon-color); color: grey; border: 0; border-radius: 5px; margin-left: 1px; margin-right: 1px; } .alertForecastVert { } .alertForecastJaune { background-color: yellow; } .alertForecastOrange { background-color: orange; } .alertForecastRouge { background-color: red; } /* Forecast */ .forecast { justify-content: space-between; flex-wrap: nowrap; } .forecast > li { flex: 1; border-right: 0.1em solid #d9d9d9; } .forecast > *:last-child { border-right: 0; } .forecast ul.day { align-items: center; } .forecast ul.day > *:first-child { text-transform: uppercase; } .forecast ul.day .highTemp { font-weight: bold; } .forecast ul.day .lowTemp { color: var(--secondary-text-color); } .forecast ul.day .icon { width: 50px; height: 50px; margin-right: 5px; } `; } } customElements.define("meteofrance-weather-card", MeteofranceWeatherCard);