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 { 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 { WeatherResponse, Convert } from './WeatherInterface';
|
||||
|
||||
export const WeatherModule: IModule = {
|
||||
title: 'Weather',
|
||||
@@ -11,29 +22,129 @@ export const WeatherModule: IModule = {
|
||||
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) {
|
||||
const [date, setDate] = useState(new Date());
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
// Get location from browser
|
||||
const [location, setLocation] = useState({ lat: 0, lng: 0 });
|
||||
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(() => {
|
||||
setInterval(() => {
|
||||
setDate(new Date());
|
||||
}, 10000);
|
||||
axios
|
||||
.get(
|
||||
`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 (
|
||||
<Group p="sm" direction="column">
|
||||
<Title>
|
||||
{hours < 10 ? `0${hours}` : hours}:{minutes < 10 ? `0${minutes}` : minutes}
|
||||
</Title>
|
||||
<Title>{weather.current_weather.temperature}°C</Title>
|
||||
<WeatherIcon code={weather.current_weather.weathercode} />
|
||||
<Text size="xl">
|
||||
{
|
||||
// Use dayjs to format the date
|
||||
// https://day.js.org/en/getting-started/installation/
|
||||
dayjs(date).format('dddd, MMMM D')
|
||||
}
|
||||
</Text>
|
||||
</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