Weather widget in Dashboard 2

Hello guys, I've just finished a simple but functional weather forecast card in Dashboard 2 using Open Meteo API, specifically designed to be nice on mobile.

The code is very basic, no message validation or optimization, most of it is hardcoded based on the JSON of the response, so be careful if you modify the Http request, it will likely break something.

City name is hardcoded here in Template and there is no direct connection to latitude and longitude:

    return {
      city: "Madrid"
    };

Days name are in this array, in my case in Italian:

    const days = ["Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab"];

you can easily switch to English or whatever:

    const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

Latitude and Longitude must be changed in Api Http request body.

https://api.open-meteo.com/v1/forecast?latitude=40.416775&longitude=-3.703790&current=temperature_2m,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max&timezone=GMT

Nothing special, code attached here:

[
    {
        "id": "abe58356fe41212b",
        "type": "http request",
        "z": "e756ecd6788c47e1",
        "name": "Madrid",
        "method": "GET",
        "ret": "txt",
        "paytoqs": "ignore",
        "url": "https://api.open-meteo.com/v1/forecast?latitude=40.416775&longitude=-3.703790&current=temperature_2m,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max&timezone=GMT",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 340,
        "y": 660,
        "wires": [
            [
                "e48e9e6375dc7dba"
            ]
        ]
    },
    {
        "id": "e48e9e6375dc7dba",
        "type": "json",
        "z": "e756ecd6788c47e1",
        "name": "",
        "property": "payload",
        "action": "",
        "pretty": false,
        "x": 490,
        "y": 660,
        "wires": [
            [
                "769b552931a52149"
            ]
        ]
    },
    {
        "id": "769b552931a52149",
        "type": "ui-template",
        "z": "e756ecd6788c47e1",
        "group": "925398646582192e",
        "page": "",
        "ui": "",
        "name": "Madrid",
        "order": 1,
        "width": "12",
        "height": "4",
        "head": "",
        "format": "<style>\n\n.hv_centered{\ndisplay: flex;\njustify-content: center;\nalign-items: center;\nheight:100%\n}\n\n.container {\n  display: grid;\n  grid-template-columns: repeat(5, 1fr);\n  grid-template-rows: 0.6fr 1.4fr 0.4fr 0.3fr 1fr 0.5fr;\n  gap: 2px;\n  grid-template-areas:\n    \"city city city city city\"\n    \"temp temp temp curr-weather-icon curr-weather-icon\"\n    \"day-name-1 day-name-2 day-name-3 day-name-4 day-name-5\"\n    \"prec1 prec2 prec3 prec4 prec5\"\n    \"weathericon1 weathericon2 weathericon3 weathericon4 weathericon5\"\n    \"minmax1 minmax2 minmax3 minmax4 minmax5\";\n}\n\n.city { \n      grid-area: city; \n      font-family: Roboto, \"Helvetica Neue\", sans-serif;\n      font-size: 1.5rem;\n      color: #5DADE2;\n    font-weight: 500;\n\n        }\n.temp { grid-area: temp; \n        font-size: 2.7rem;\n        padding-left: 10px;\n        }\n.curr-weather-icon { grid-area: curr-weather-icon; \n  font-size: 3rem;\n  display: flex;\n  justify-content: right;\n  padding-right: 30px;\n  }\n\n.day_text{\nfont-size: 1.2rem;\n}\n\n.weather_icon{\nfont-size: 2.5rem;\n\n}\n\n.temp_text{\n  font-size: 1rem;\n  display: flex;\n  // justify-content: center;\n  align-items: center;\n}\n\n.day-name-1 { grid-area: day-name-1; }\n.day-name-2 { grid-area: day-name-2; }\n.day-name-3 { grid-area: day-name-3; }\n.day-name-4 { grid-area: day-name-4; }\n.day-name-5 { grid-area: day-name-5; }\n.prec1 { grid-area: prec1; }\n.prec2 { grid-area: prec2; }\n.prec3 { grid-area: prec3; }\n.prec4 { grid-area: prec4; }\n.prec5 { grid-area: prec5; }\n.weathericon1 { grid-area: weathericon1}\n.weathericon2 { grid-area: weathericon2}\n.weathericon3 { grid-area: weathericon3}\n.weathericon4 { grid-area: weathericon4}\n.weathericon5 { grid-area: weathericon5}\n.minmax1 { grid-area: minmax1; }\n.minmax2 { grid-area: minmax2; }\n.minmax3 { grid-area: minmax3; }\n.minmax4 { grid-area: minmax4; }\n.minmax5 { grid-area: minmax5; }\n</style>\n\n<template>\n  <div class=\"container\">\n    <div class=\"city\">{{ city }}</div>\n    <div class=\"temp\">{{ curr_temp }}°C</div>\n    <div class=\"curr-weather-icon\"> {{curr_weather_code}}</div>\n    <div class=\"day-name-1 hv_centered day_text\">{{ today }}</div>\n    <div class=\"day-name-2 hv_centered day_text\">{{ today_plus1 }}</div>\n    <div class=\"day-name-3 hv_centered day_text\">{{ today_plus2 }}</div>\n    <div class=\"day-name-4 hv_centered day_text\">{{ today_plus3 }}</div>\n    <div class=\"day-name-5 hv_centered day_text\">{{ today_plus4 }}</div>\n    <div class=\"prec1 hv_centered\">{{prec0}}</div>\n    <div class=\"prec2 hv_centered\">{{prec1}}</div>\n    <div class=\"prec3 hv_centered\">{{prec2}}</div>\n    <div class=\"prec4 hv_centered\">{{prec3}}</div>\n    <div class=\"prec5 hv_centered\">{{prec4}}</div>\n    <div class=\"weathericon1 hv_centered weather_icon\">{{ icon0 }}</div>\n    <div class=\"weathericon2 hv_centered weather_icon\">{{ icon1 }}</div>\n    <div class=\"weathericon3 hv_centered weather_icon\">{{ icon2 }}</div>\n    <div class=\"weathericon4 hv_centered weather_icon\">{{ icon3 }}</div>\n    <div class=\"weathericon5 hv_centered weather_icon\">{{ icon4 }}</div>\n    <div class=\"minmax1 hv_centered temp_text\">{{minmax0}}</div>\n    <div class=\"minmax2 hv_centered temp_text\">{{minmax1}}</div>\n    <div class=\"minmax3 hv_centered temp_text\">{{minmax2}}</div>\n    <div class=\"minmax4 hv_centered temp_text\">{{minmax3}}</div>\n    <div class=\"minmax5 hv_centered temp_text\">{{minmax4}}</div>\n  </div>\n</template>\n\n<script>\nexport default {\n  data() {\n    return {\n      city: \"Madrid\",\n      curr_temp: 10,\n      curr_weather_code: 0,\n      today: \"\",\n      tomorrow: \"\",\n      today_plus2: \"\",\n      today_plus3: \"\",\n      today_plus4: \"\",\n      minmax0: '0/0',\n      minmax1: '0/0',\n      minmax2: '0/0',\n      minmax3: '0/0',\n      minmax4: '0/0',\n      icon0: 0,\n      icon1: 1,\n      icon2: 2,\n      icon3: 3,\n      icon4: 4,\n      prec0: 0,\n      prec1: 0,\n      prec2: 0,\n      prec3: 0,\n      prec4: 0\n    };\n  },\n  watch: {\n    msg: function () {\n    //  if (newMsg.payload && newMsg.payload.daily && newMsg.payload.daily.time) {\n        this.curr_temp = this.msg.payload.current.temperature_2m\n\n        this.curr_weather_code = this.getWeatherIcon(this.msg.payload.current.weather_code)\n\n        this.today = this.getdayname(this.msg.payload.daily.time[0]);\n        this.today_plus1 = this.getdayname(this.msg.payload.daily.time[1]);\n        this.today_plus2 = this.getdayname(this.msg.payload.daily.time[2]);\n        this.today_plus3 = this.getdayname(this.msg.payload.daily.time[3]);\n        this.today_plus4 = this.getdayname(this.msg.payload.daily.time[4]);\n        this.minmax0 = Math.round(this.msg.payload.daily.temperature_2m_min[0]) + \n        '/' + Math.round(this.msg.payload.daily.temperature_2m_max[0]);\n\n        this.prec0 = this.msg.payload.daily.precipitation_probability_max[0] + ' %'\n        this.prec1 = this.msg.payload.daily.precipitation_probability_max[1] + ' %'\n        this.prec2 = this.msg.payload.daily.precipitation_probability_max[2] + ' %'\n        this.prec3 = this.msg.payload.daily.precipitation_probability_max[3] + ' %'\n        this.prec4 = this.msg.payload.daily.precipitation_probability_max[4] + ' %'\n\n        this.minmax1 = Math.round(this.msg.payload.daily.temperature_2m_min[1]) +\n        '/' + Math.round(this.msg.payload.daily.temperature_2m_max[1]);\n\n        this.minmax2 = Math.round(this.msg.payload.daily.temperature_2m_min[2]) +\n        '/' + Math.round(this.msg.payload.daily.temperature_2m_max[2]);\n\n        this.minmax3 = Math.round(this.msg.payload.daily.temperature_2m_min[3]) +\n        '/' + Math.round(this.msg.payload.daily.temperature_2m_max[3]);\n\n        this.minmax4 = Math.round(this.msg.payload.daily.temperature_2m_min[4]) +\n        '/' + Math.round(this.msg.payload.daily.temperature_2m_max[4]);\n\n        this.minmax5 = Math.round(this.msg.payload.daily.temperature_2m_min[5]) +\n        '/' + Math.round(this.msg.payload.daily.temperature_2m_max[5]);\n\n        this.icon0 = this.getWeatherIcon(this.msg.payload.daily.weather_code[0])\n        this.icon1 = this.getWeatherIcon(this.msg.payload.daily.weather_code[1])\n        this.icon2 = this.getWeatherIcon(this.msg.payload.daily.weather_code[2])\n        this.icon3 = this.getWeatherIcon(this.msg.payload.daily.weather_code[3])\n        this.icon4 = this.getWeatherIcon(this.msg.payload.daily.weather_code[4])\n\n\n      }\n    //}\n  },\n  methods: {\n    getdayname: function (time_stamp) {\n    let day = new Date(time_stamp);\n    const days = [\"Dom\", \"Lun\", \"Mar\", \"Mer\", \"Gio\", \"Ven\", \"Sab\"];\n    return days[day.getDay()];\n    },\n\n    getWeatherIcon: function (code) {\n      if (code === 0) {\n          return '☀️';\n      } else if (code === 1) {\n          return '🌤️';\n      } else if (code === 2) {\n          return '⛅';\n      } else if (code === 3) {\n          return '☁️';\n      } else if ([45, 48].includes(code)) {\n          return '🌫️';\n      } else if ([51, 53, 55].includes(code)) {\n          return '🌦️';\n      } else if ([56, 57].includes(code)) {\n          return '🌧️';\n      } else if ([61, 63, 65].includes(code)) {\n          return '🌧️';\n      } else if ([66, 67].includes(code)) {\n          return '🌨️';\n      } else if ([71, 73, 75, 77].includes(code)) {\n          return '❄️';\n      } else if ([80, 81, 82].includes(code)) {\n          return '🌦️';\n      } else if ([85, 86].includes(code)) {\n          return '❄️';\n      } else if (code === 95) {\n          return '⛈️';\n      } else if ([96, 99].includes(code)) {\n          return '⛈️';\n      } else {\n          return '❓';\n      }\n    }\n\n  },\n  mounted() {\n    console.log(\"Component mounted\");\n  },\n  unmounted() {\n    console.log(\"Component removed\");\n  }\n};\n</script>\n",
        "storeOutMessages": true,
        "passthru": false,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 640,
        "y": 660,
        "wires": [
            []
        ]
    },
    {
        "id": "1dd137a565e1a50c",
        "type": "inject",
        "z": "e756ecd6788c47e1",
        "name": "Test",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "600",
        "crontab": "",
        "once": true,
        "onceDelay": "1",
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 110,
        "y": 600,
        "wires": [
            [
                "d799f7fd7c6bca7b",
                "abe58356fe41212b"
            ]
        ]
    },
    {
        "id": "925398646582192e",
        "type": "ui-group",
        "name": "Madrid",
        "page": "bf101ed8a33290dc",
        "width": 6,
        "height": 1,
        "order": 1,
        "showTitle": false,
        "className": "",
        "visible": "true",
        "disabled": "false",
        "groupType": "default"
    },
    {
        "id": "bf101ed8a33290dc",
        "type": "ui-page",
        "name": "Page Name",
        "ui": "43e57a187b2ebc1f",
        "path": "/page4",
        "icon": "home",
        "layout": "grid",
        "theme": "f019925b84983e76",
        "breakpoints": [
            {
                "name": "Default",
                "px": "0",
                "cols": "3"
            },
            {
                "name": "Tablet",
                "px": "576",
                "cols": "6"
            },
            {
                "name": "Small Desktop",
                "px": "768",
                "cols": "9"
            },
            {
                "name": "Desktop",
                "px": "1024",
                "cols": "12"
            }
        ],
        "order": 1,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "43e57a187b2ebc1f",
        "type": "ui-base",
        "name": "Max Passeri Dashboard",
        "path": "/dashboard",
        "appIcon": "",
        "includeClientData": true,
        "acceptsClientConfig": [
            "ui-notification",
            "ui-control"
        ],
        "showPathInSidebar": false,
        "showPageTitle": true,
        "navigationStyle": "temporary",
        "titleBarStyle": "fixed",
        "showReconnectNotification": true,
        "notificationDisplayTime": "5",
        "showDisconnectNotification": true
    },
    {
        "id": "f019925b84983e76",
        "type": "ui-theme",
        "name": "Dark",
        "colors": {
            "surface": "#303030",
            "primary": "#0094ce",
            "bgPage": "#303030",
            "groupBg": "#303030",
            "groupOutline": "#545454"
        },
        "sizes": {
            "density": "default",
            "pagePadding": "12px",
            "groupGap": "12px",
            "groupBorderRadius": "10px",
            "widgetGap": "12px"
        }
    }
]

Hope useful !

5 Likes

Well done. Easy enough to understand and modify if needed.
It just misses solution(s) for error cases. For sure such will hapen time to time.

1 Like

:+1: I'm using Open-Meteo as well for a while

1 Like

I'm also sharing a picture of my weather card, mainly as a source of inspiration. It's also based on open-meteo data. chartjs is used to create the temperature curve. CSS is pretty heavily overridden to ensure a consistent user experience on both large and small screens. Since this is a custom job and made for a specific use, I won't share the solution as a whole, but I can share advice on how to achieve something similar. But the prerequisite for this is a clear vision.

3 Likes