mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 06:55:51 +01:00
Advancement on the weather widget
This commit is contained in:
236
src/components/modules/weather/WeatherInterface.ts
Normal file
236
src/components/modules/weather/WeatherInterface.ts
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
// To parse this data:
|
||||||
|
//
|
||||||
|
// import { Convert, WeatherResponse } from "./file";
|
||||||
|
//
|
||||||
|
// const weatherResponse = Convert.toWeatherResponse(json);
|
||||||
|
//
|
||||||
|
// These functions will throw an error if the JSON doesn't
|
||||||
|
// match the expected interface, even if the JSON is valid.
|
||||||
|
|
||||||
|
export interface WeatherResponse {
|
||||||
|
current_weather: CurrentWeather;
|
||||||
|
utc_offset_seconds: number;
|
||||||
|
latitude: number;
|
||||||
|
elevation: number;
|
||||||
|
longitude: number;
|
||||||
|
generationtime_ms: number;
|
||||||
|
daily_units: DailyUnits;
|
||||||
|
daily: Daily;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrentWeather {
|
||||||
|
winddirection: number;
|
||||||
|
windspeed: number;
|
||||||
|
time: string;
|
||||||
|
weathercode: number;
|
||||||
|
temperature: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Daily {
|
||||||
|
temperature_2m_max: number[];
|
||||||
|
time: Date[];
|
||||||
|
temperature_2m_min: number[];
|
||||||
|
weathercode: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DailyUnits {
|
||||||
|
temperature_2m_max: string;
|
||||||
|
temperature_2m_min: string;
|
||||||
|
time: string;
|
||||||
|
weathercode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts JSON strings to/from your types
|
||||||
|
// and asserts the results of JSON.parse at runtime
|
||||||
|
export class Convert {
|
||||||
|
public static toWeatherResponse(json: string): WeatherResponse {
|
||||||
|
return cast(JSON.parse(json), r('WeatherResponse'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static weatherResponseToJson(value: WeatherResponse): string {
|
||||||
|
return JSON.stringify(uncast(value, r('WeatherResponse')), null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalidValue(typ: any, val: any, key: any = ''): never {
|
||||||
|
if (key) {
|
||||||
|
throw Error(
|
||||||
|
`Invalid value for key "${key}". Expected type ${JSON.stringify(
|
||||||
|
typ
|
||||||
|
)} but got ${JSON.stringify(val)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw Error(`Invalid value ${JSON.stringify(val)} for type ${JSON.stringify(typ)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonToJSProps(typ: any): any {
|
||||||
|
if (typ.jsonToJS === undefined) {
|
||||||
|
const map: any = {};
|
||||||
|
typ.props.forEach((p: any) => (map[p.json] = { key: p.js, typ: p.typ }));
|
||||||
|
typ.jsonToJS = map;
|
||||||
|
}
|
||||||
|
return typ.jsonToJS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsToJSONProps(typ: any): any {
|
||||||
|
if (typ.jsToJSON === undefined) {
|
||||||
|
const map: any = {};
|
||||||
|
typ.props.forEach((p: any) => (map[p.js] = { key: p.json, typ: p.typ }));
|
||||||
|
typ.jsToJSON = map;
|
||||||
|
}
|
||||||
|
return typ.jsToJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transform(val: any, typ: any, getProps: any, key: any = ''): any {
|
||||||
|
function transformPrimitive(typ: string, val: any): any {
|
||||||
|
if (typeof typ === typeof val) return val;
|
||||||
|
return invalidValue(typ, val, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformUnion(typs: any[], val: any): any {
|
||||||
|
// val must validate against one typ in typs
|
||||||
|
const l = typs.length;
|
||||||
|
for (let i = 0; i < l; i++) {
|
||||||
|
const typ = typs[i];
|
||||||
|
try {
|
||||||
|
return transform(val, typ, getProps);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return invalidValue(typs, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformEnum(cases: string[], val: any): any {
|
||||||
|
if (cases.indexOf(val) !== -1) return val;
|
||||||
|
return invalidValue(cases, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformArray(typ: any, val: any): any {
|
||||||
|
// val must be an array with no invalid elements
|
||||||
|
if (!Array.isArray(val)) return invalidValue('array', val);
|
||||||
|
return val.map((el) => transform(el, typ, getProps));
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformDate(val: any): any {
|
||||||
|
if (val === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const d = new Date(val);
|
||||||
|
if (isNaN(d.valueOf())) {
|
||||||
|
return invalidValue('Date', val);
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformObject(props: { [k: string]: any }, additional: any, val: any): any {
|
||||||
|
if (val === null || typeof val !== 'object' || Array.isArray(val)) {
|
||||||
|
return invalidValue('object', val);
|
||||||
|
}
|
||||||
|
const result: any = {};
|
||||||
|
Object.getOwnPropertyNames(props).forEach((key) => {
|
||||||
|
const prop = props[key];
|
||||||
|
const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined;
|
||||||
|
result[prop.key] = transform(v, prop.typ, getProps, prop.key);
|
||||||
|
});
|
||||||
|
Object.getOwnPropertyNames(val).forEach((key) => {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(props, key)) {
|
||||||
|
result[key] = transform(val[key], additional, getProps, key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typ === 'any') return val;
|
||||||
|
if (typ === null) {
|
||||||
|
if (val === null) return val;
|
||||||
|
return invalidValue(typ, val);
|
||||||
|
}
|
||||||
|
if (typ === false) return invalidValue(typ, val);
|
||||||
|
while (typeof typ === 'object' && typ.ref !== undefined) {
|
||||||
|
typ = typeMap[typ.ref];
|
||||||
|
}
|
||||||
|
if (Array.isArray(typ)) return transformEnum(typ, val);
|
||||||
|
if (typeof typ === 'object') {
|
||||||
|
return typ.hasOwnProperty('unionMembers')
|
||||||
|
? transformUnion(typ.unionMembers, val)
|
||||||
|
: typ.hasOwnProperty('arrayItems')
|
||||||
|
? transformArray(typ.arrayItems, val)
|
||||||
|
: typ.hasOwnProperty('props')
|
||||||
|
? transformObject(getProps(typ), typ.additional, val)
|
||||||
|
: invalidValue(typ, val);
|
||||||
|
}
|
||||||
|
// Numbers can be parsed by Date but shouldn't be.
|
||||||
|
if (typ === Date && typeof val !== 'number') return transformDate(val);
|
||||||
|
return transformPrimitive(typ, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cast<T>(val: any, typ: any): T {
|
||||||
|
return transform(val, typ, jsonToJSProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uncast<T>(val: T, typ: any): any {
|
||||||
|
return transform(val, typ, jsToJSONProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function a(typ: any) {
|
||||||
|
return { arrayItems: typ };
|
||||||
|
}
|
||||||
|
|
||||||
|
function u(...typs: any[]) {
|
||||||
|
return { unionMembers: typs };
|
||||||
|
}
|
||||||
|
|
||||||
|
function o(props: any[], additional: any) {
|
||||||
|
return { props, additional };
|
||||||
|
}
|
||||||
|
|
||||||
|
function m(additional: any) {
|
||||||
|
return { props: [], additional };
|
||||||
|
}
|
||||||
|
|
||||||
|
function r(name: string) {
|
||||||
|
return { ref: name };
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeMap: any = {
|
||||||
|
WeatherResponse: o(
|
||||||
|
[
|
||||||
|
{ json: 'current_weather', js: 'current_weather', typ: r('CurrentWeather') },
|
||||||
|
{ json: 'utc_offset_seconds', js: 'utc_offset_seconds', typ: 0 },
|
||||||
|
{ json: 'latitude', js: 'latitude', typ: 3.14 },
|
||||||
|
{ json: 'elevation', js: 'elevation', typ: 3.14 },
|
||||||
|
{ json: 'longitude', js: 'longitude', typ: 3.14 },
|
||||||
|
{ json: 'generationtime_ms', js: 'generationtime_ms', typ: 3.14 },
|
||||||
|
{ json: 'daily_units', js: 'daily_units', typ: r('DailyUnits') },
|
||||||
|
{ json: 'daily', js: 'daily', typ: r('Daily') },
|
||||||
|
],
|
||||||
|
false
|
||||||
|
),
|
||||||
|
CurrentWeather: o(
|
||||||
|
[
|
||||||
|
{ json: 'winddirection', js: 'winddirection', typ: 0 },
|
||||||
|
{ json: 'windspeed', js: 'windspeed', typ: 3.14 },
|
||||||
|
{ json: 'time', js: 'time', typ: '' },
|
||||||
|
{ json: 'weathercode', js: 'weathercode', typ: 0 },
|
||||||
|
{ json: 'temperature', js: 'temperature', typ: 3.14 },
|
||||||
|
],
|
||||||
|
false
|
||||||
|
),
|
||||||
|
Daily: o(
|
||||||
|
[
|
||||||
|
{ json: 'temperature_2m_max', js: 'temperature_2m_max', typ: a(3.14) },
|
||||||
|
{ json: 'time', js: 'time', typ: a(Date) },
|
||||||
|
{ json: 'temperature_2m_min', js: 'temperature_2m_min', typ: a(3.14) },
|
||||||
|
{ json: 'weathercode', js: 'weathercode', typ: a(0) },
|
||||||
|
],
|
||||||
|
false
|
||||||
|
),
|
||||||
|
DailyUnits: o(
|
||||||
|
[
|
||||||
|
{ json: 'temperature_2m_max', js: 'temperature_2m_max', typ: '' },
|
||||||
|
{ json: 'temperature_2m_min', js: 'temperature_2m_min', typ: '' },
|
||||||
|
{ json: 'time', js: 'time', typ: '' },
|
||||||
|
{ json: 'weathercode', js: 'weathercode', typ: '' },
|
||||||
|
],
|
||||||
|
false
|
||||||
|
),
|
||||||
|
};
|
||||||
22
src/components/modules/weather/WeatherModule.story.tsx
Normal file
22
src/components/modules/weather/WeatherModule.story.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import withMock from 'storybook-addon-mock';
|
||||||
|
import WeatherComponent from './WeatherModule';
|
||||||
|
import mockdata from './mockdata.json';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Weather module',
|
||||||
|
decorators: [withMock],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = (args: any) => <WeatherComponent {...args} />;
|
||||||
|
Default.parameters = {
|
||||||
|
mockData: [
|
||||||
|
{
|
||||||
|
url: 'https://api.open-meteo.com/v1/forecast',
|
||||||
|
method: 'GET',
|
||||||
|
status: 200,
|
||||||
|
response: {
|
||||||
|
data: mockdata,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -1,8 +1,19 @@
|
|||||||
import { Group, Text, Title } from '@mantine/core';
|
import { Group, Text, Title, Tooltip } from '@mantine/core';
|
||||||
|
import axios from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Clock, Cloud } from 'tabler-icons-react';
|
import {
|
||||||
|
Cloud,
|
||||||
|
CloudFog,
|
||||||
|
CloudRain,
|
||||||
|
CloudSnow,
|
||||||
|
CloudStorm,
|
||||||
|
QuestionMark,
|
||||||
|
Snowflake,
|
||||||
|
Sun,
|
||||||
|
} from 'tabler-icons-react';
|
||||||
import { IModule } from '../modules';
|
import { IModule } from '../modules';
|
||||||
|
import { WeatherResponse, Convert } from './WeatherInterface';
|
||||||
|
|
||||||
export const WeatherModule: IModule = {
|
export const WeatherModule: IModule = {
|
||||||
title: 'Weather',
|
title: 'Weather',
|
||||||
@@ -11,29 +22,129 @@ export const WeatherModule: IModule = {
|
|||||||
component: WeatherComponent,
|
component: WeatherComponent,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 0 Clear sky
|
||||||
|
// 1, 2, 3 Mainly clear, partly cloudy, and overcast
|
||||||
|
// 45, 48 Fog and depositing rime fog
|
||||||
|
// 51, 53, 55 Drizzle: Light, moderate, and dense intensity
|
||||||
|
// 56, 57 Freezing Drizzle: Light and dense intensity
|
||||||
|
// 61, 63, 65 Rain: Slight, moderate and heavy intensity
|
||||||
|
// 66, 67 Freezing Rain: Light and heavy intensity
|
||||||
|
// 71, 73, 75 Snow fall: Slight, moderate, and heavy intensity
|
||||||
|
// 77 Snow grains
|
||||||
|
// 80, 81, 82 Rain showers: Slight, moderate, and violent
|
||||||
|
// 85, 86 Snow showers slight and heavy
|
||||||
|
// 95 * Thunderstorm: Slight or moderate
|
||||||
|
// 96, 99 * Thunderstorm with slight and heavy hail
|
||||||
|
export function WeatherIcon(props: any) {
|
||||||
|
const { code } = props;
|
||||||
|
let data: { icon: any; name: string };
|
||||||
|
switch (code) {
|
||||||
|
case 0: {
|
||||||
|
data = { icon: <Sun />, name: 'Clear' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
case 3: {
|
||||||
|
data = { icon: <Cloud />, name: 'Mainly clear' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 45:
|
||||||
|
case 48: {
|
||||||
|
data = { icon: <CloudFog />, name: 'Fog' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 51:
|
||||||
|
case 53:
|
||||||
|
case 55: {
|
||||||
|
data = { icon: <Cloud />, name: 'Drizzle' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 56:
|
||||||
|
case 57: {
|
||||||
|
data = { icon: <Snowflake />, name: 'Freezing drizzle' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 61:
|
||||||
|
case 63:
|
||||||
|
case 65: {
|
||||||
|
data = { icon: <CloudRain />, name: 'Rain' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 66:
|
||||||
|
case 67: {
|
||||||
|
data = { icon: <CloudRain />, name: 'Freezing rain' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 71:
|
||||||
|
case 73:
|
||||||
|
case 75: {
|
||||||
|
data = { icon: <CloudSnow />, name: 'Snow fall' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 77: {
|
||||||
|
data = { icon: <CloudSnow />, name: 'Snow grains' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 80:
|
||||||
|
case 81:
|
||||||
|
case 82: {
|
||||||
|
data = { icon: <CloudRain />, name: 'Rain showers' };
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 85:
|
||||||
|
case 86: {
|
||||||
|
data = { icon: <CloudSnow />, name: 'Snow showers' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 95: {
|
||||||
|
data = { icon: <CloudStorm />, name: 'Thunderstorm' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 96:
|
||||||
|
case 99: {
|
||||||
|
data = { icon: <CloudStorm />, name: 'Thunderstorm with hail' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
data = { icon: <QuestionMark />, name: 'Unknown' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <Tooltip label={data.name}>{data.icon}</Tooltip>;
|
||||||
|
}
|
||||||
|
|
||||||
export default function WeatherComponent(props: any) {
|
export default function WeatherComponent(props: any) {
|
||||||
const [date, setDate] = useState(new Date());
|
// Get location from browser
|
||||||
const hours = date.getHours();
|
const [location, setLocation] = useState({ lat: 0, lng: 0 });
|
||||||
const minutes = date.getMinutes();
|
const [weather, setWeather] = useState({} as WeatherResponse);
|
||||||
|
if ('geolocation' in navigator && location.lat === 0 && location.lng === 0) {
|
||||||
|
navigator.geolocation.getCurrentPosition((position) => {
|
||||||
|
setLocation({ lat: position.coords.latitude, lng: position.coords.longitude });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Change date on minute change
|
|
||||||
// Note: Using 10 000ms instead of 1000ms to chill a little :)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInterval(() => {
|
axios
|
||||||
setDate(new Date());
|
.get(
|
||||||
}, 10000);
|
`https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lng}&daily=weathercode,temperature_2m_max,temperature_2m_min¤t_weather=true&timezone=Europe%2FLondon`
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
setWeather(res.data);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
if (!weather.current_weather) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
console.log(weather.current_weather);
|
||||||
return (
|
return (
|
||||||
<Group p="sm" direction="column">
|
<Group p="sm" direction="column">
|
||||||
<Title>
|
<Title>{weather.current_weather.temperature}°C</Title>
|
||||||
{hours < 10 ? `0${hours}` : hours}:{minutes < 10 ? `0${minutes}` : minutes}
|
<WeatherIcon code={weather.current_weather.weathercode} />
|
||||||
</Title>
|
|
||||||
<Text size="xl">
|
<Text size="xl">
|
||||||
{
|
{
|
||||||
// Use dayjs to format the date
|
// Use dayjs to format the date
|
||||||
// https://day.js.org/en/getting-started/installation/
|
// https://day.js.org/en/getting-started/installation/
|
||||||
dayjs(date).format('dddd, MMMM D')
|
|
||||||
}
|
}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
58
src/components/modules/weather/mockdata.json
Normal file
58
src/components/modules/weather/mockdata.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"current_weather": {
|
||||||
|
"winddirection": 121,
|
||||||
|
"windspeed": 12.7,
|
||||||
|
"time": "2022-05-15T14:00",
|
||||||
|
"weathercode": 3,
|
||||||
|
"temperature": 28.7
|
||||||
|
},
|
||||||
|
"utc_offset_seconds": 3600,
|
||||||
|
"latitude": 48.86,
|
||||||
|
"elevation": 46.1875,
|
||||||
|
"longitude": 2.3599997,
|
||||||
|
"generationtime_ms": 0.36406517028808594,
|
||||||
|
"daily_units": {
|
||||||
|
"temperature_2m_max": "°C",
|
||||||
|
"temperature_2m_min": "°C",
|
||||||
|
"time": "iso8601",
|
||||||
|
"weathercode": "wmo code"
|
||||||
|
},
|
||||||
|
"daily": {
|
||||||
|
"temperature_2m_max": [
|
||||||
|
29.1,
|
||||||
|
25.4,
|
||||||
|
28.2,
|
||||||
|
29.7,
|
||||||
|
24.6,
|
||||||
|
27.1,
|
||||||
|
22.9
|
||||||
|
],
|
||||||
|
"time": [
|
||||||
|
"2022-05-15",
|
||||||
|
"2022-05-16",
|
||||||
|
"2022-05-17",
|
||||||
|
"2022-05-18",
|
||||||
|
"2022-05-19",
|
||||||
|
"2022-05-20",
|
||||||
|
"2022-05-21"
|
||||||
|
],
|
||||||
|
"temperature_2m_min": [
|
||||||
|
14.3,
|
||||||
|
16.9,
|
||||||
|
17.2,
|
||||||
|
17.7,
|
||||||
|
19.2,
|
||||||
|
19.1,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"weathercode": [
|
||||||
|
95,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
80,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user