Skip to content

Commit 90f8e29

Browse files
authored
Merge pull request #21 from TextureHQ/rcasto/weather-plus-add-cloudiness-some-types-and-maybe-more
Add cloudiness, sunrise + sunset (openweather), also some typing related updates
2 parents 35518a6 + 503c477 commit 90f8e29

16 files changed

+1975
-38
lines changed

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ console.log(weather);
8989

9090
One of the main benefits of this library is the ability to seamlessly switch between weather providers while maintaining a consistent API. This is particularly useful for:
9191

92-
Fallback Mechanism: Use a free provider by default and fallback to a paid provider if necessary.
93-
Coverage: Some providers may not support certain locations; having multiple providers ensures broader coverage.
94-
Cost Optimization: Reduce costs by prioritizing free or cheaper providers.
92+
- **Fallback Mechanism**: Use a free provider by default and fallback to a paid provider if necessary.
93+
- **Coverage**: Some providers may not support certain locations; having multiple providers ensures broader coverage.
94+
- **Cost Optimization**: Reduce costs by prioritizing free or cheaper providers.
9595

96-
Available Providers
96+
#### Available Providers
9797

9898
- 'nws' - National Weather Service
9999
- Notes:
@@ -325,10 +325,22 @@ interface IWeatherData {
325325
unit: string; // Always "string"
326326
original: string; // Original provider condition text
327327
};
328+
cloudiness: { // Percentage of cloud cover
329+
value: number;
330+
unit: string; // Always "percent"
331+
};
332+
sunrise: { // Available for some providers (e.g., OpenWeather)
333+
value: string; // Sunrise time
334+
unit: string; // Always "iso8601"
335+
};
336+
sunset: { // Available for some providers (e.g., OpenWeather)
337+
value: string; // Sunset time
338+
unit: string; // Always "iso8601"
339+
};
328340
}
329341
```
330342

331-
Note today the response is fairly basic, but we're working on adding more data all of the time.
343+
Note that the availability of specific data fields may vary depending on the weather provider being used. The library continues to expand with additional weather data over time.
332344

333345
### Complete Example
334346

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "weather-plus",
3-
"version": "1.0.4",
3+
"version": "1.1.0",
44
"description": "Weather Plus is a powerful wrapper around various Weather APIs that simplifies adding weather data to your application",
55
"main": "./dist/cjs/index.js",
66
"module": "./dist/esm/index.js",
@@ -16,6 +16,7 @@
1616
"test": "jest",
1717
"test:watch": "jest --watch",
1818
"prebuild": "rimraf ./dist",
19+
"prepack": "npm run build",
1920
"coverage": "jest --coverage"
2021
},
2122
"files": [

src/index.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ describe('WeatherPlus Library', () => {
9696
qualityControl: 'V',
9797
},
9898
textDescription: 'Sunny',
99+
cloudLayers: [
100+
{
101+
base: {
102+
unitCode: 'wmoUnit:m',
103+
value: 1000,
104+
},
105+
amount: 'CLR',
106+
},
107+
],
99108
},
100109
},
101110
];
@@ -130,8 +139,13 @@ describe('WeatherPlus Library', () => {
130139
unit: 'string',
131140
original: 'Sunny'
132141
},
142+
cloudiness: {
143+
value: 0,
144+
unit: 'percent',
145+
},
133146
provider: 'nws',
134147
cached: false,
148+
cachedAt: undefined,
135149
};
136150
expect(response).toEqual(expectedResponse);
137151
});
@@ -196,6 +210,7 @@ describe('WeatherPlus Library', () => {
196210
unit: 'string',
197211
original: 'light rain'
198212
});
213+
expect(response.cloudiness).toEqual({ value: 75, unit: 'percent' });
199214
});
200215

201216
it('should throw an error if all providers fail', async () => {
@@ -265,6 +280,15 @@ describe('WeatherPlus Library', () => {
265280
qualityControl: 'V',
266281
},
267282
textDescription: 'Sunny',
283+
cloudLayers: [
284+
{
285+
base: {
286+
unitCode: 'wmoUnit:m',
287+
value: 1000,
288+
},
289+
amount: 'CLR',
290+
},
291+
],
268292
},
269293
},
270294
];
@@ -293,6 +317,7 @@ describe('WeatherPlus Library', () => {
293317
expect(response1.humidity).toEqual(response2.humidity);
294318
expect(response1.temperature).toEqual(response2.temperature);
295319
expect(response1.conditions).toEqual(response2.conditions);
320+
expect(response1.cloudiness).toEqual(response2.cloudiness);
296321
});
297322

298323
it('should export InvalidProviderLocationError', () => {

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { WeatherService } from './weatherService';
22
import { InvalidProviderLocationError, ProviderNotSupportedError, WeatherProviderError } from './errors';
33
import { RedisClientType } from 'redis';
44
import { GetWeatherOptions } from './weatherService';
5+
import { IWeatherData } from './interfaces';
56

67
// Define the options interface for WeatherPlus
78
interface WeatherPlusOptions {
@@ -27,7 +28,7 @@ class WeatherPlus {
2728
}
2829

2930
// Public method to get weather data for a given latitude and longitude
30-
async getWeather(lat: number, lng: number, options?: GetWeatherOptions) {
31+
async getWeather(lat: number, lng: number, options?: GetWeatherOptions): Promise<IWeatherData> {
3132
return this.weatherService.getWeather(lat, lng, options);
3233
}
3334
}

src/interfaces.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,33 @@ export enum IWeatherUnits {
33
F = 'F',
44
percent = 'percent',
55
string = 'string',
6+
iso8601 = 'iso8601',
67
}
78

89
export enum IWeatherKey {
910
dewPoint = 'dewPoint',
1011
humidity = 'humidity',
1112
temperature = 'temperature',
1213
conditions = 'conditions',
14+
cloudiness = 'cloudiness',
15+
// Not available for NWS, but is available for OpenWeather
16+
// We could utilize this API: https://sunrise-sunset.org/api
17+
// To supply sunrise and sunset times for NWS. It's a free API, would need to add attribution.
18+
sunrise = 'sunrise',
19+
sunset = 'sunset',
1320
}
1421

15-
export interface IWeatherProviderWeatherData {
16-
[IWeatherKey.dewPoint]: IDewPoint;
17-
[IWeatherKey.humidity]: IRelativeHumidity;
18-
[IWeatherKey.temperature]: ITemperature;
19-
[IWeatherKey.conditions]: IConditions;
22+
export interface IWeatherProviderWeatherData extends Record<IWeatherKey, IBaseWeatherProperty<any, any>> {
23+
dewPoint: IDewPoint;
24+
humidity: IRelativeHumidity;
25+
temperature: ITemperature;
26+
conditions: IConditions;
27+
cloudiness: ICloudiness;
28+
sunrise: ISunriseSunset;
29+
sunset: ISunriseSunset;
2030
}
2131

22-
export interface IWeatherData extends IWeatherProviderWeatherData {
32+
export interface IWeatherData extends Partial<IWeatherProviderWeatherData> {
2333
provider: string;
2434
cached: boolean;
2535
cachedAt?: string; // ISO-8601 formatted date string
@@ -35,4 +45,6 @@ export type IDewPoint = IBaseWeatherProperty<number, IWeatherUnits.C | IWeatherU
3545
export type IConditions = IBaseWeatherProperty<string, IWeatherUnits.string> & {
3646
original?: string; // Original provider-specific condition value
3747
}
38-
export type ITemperature = IBaseWeatherProperty<number, IWeatherUnits.C | IWeatherUnits.F>;
48+
export type ITemperature = IBaseWeatherProperty<number, IWeatherUnits.C | IWeatherUnits.F>;
49+
export type ICloudiness = IBaseWeatherProperty<number, IWeatherUnits.percent>;
50+
export type ISunriseSunset = IBaseWeatherProperty<string, IWeatherUnits.iso8601>;

src/providers/IWeatherProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ import { IWeatherProviderWeatherData } from '../interfaces';
22

33
export interface IWeatherProvider {
44
name: string;
5-
getWeather(lat: number, lng: number): Promise<IWeatherProviderWeatherData>;
5+
getWeather(lat: number, lng: number): Promise<Partial<IWeatherProviderWeatherData>>;
66
}

src/providers/nws/client.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ describe('NWSProvider', () => {
3838
relativeHumidity: { value: 80 },
3939
temperature: { value: 20, unitCode: 'wmoUnit:degC' },
4040
textDescription: 'Clear',
41+
cloudLayers: [
42+
{ base: { unitCode: 'wmoUnit:m', value: 1000 }, amount: 'CLR' }
43+
],
4144
},
4245
};
4346

@@ -69,6 +72,10 @@ describe('NWSProvider', () => {
6972
unit: 'string',
7073
original: 'Clear'
7174
},
75+
cloudiness: {
76+
value: 0,
77+
unit: 'percent'
78+
},
7279
});
7380
});
7481

@@ -140,6 +147,10 @@ describe('NWSProvider', () => {
140147
unit: 'string',
141148
original: 'Clear'
142149
},
150+
cloudiness: {
151+
value: 0,
152+
unit: 'percent'
153+
},
143154
});
144155
});
145156
});

src/providers/nws/client.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import axios from 'axios';
22
import debug from 'debug';
3-
import { IWeatherData, IWeatherKey, IWeatherProviderWeatherData, IWeatherUnits } from '../../interfaces';
3+
import { IWeatherKey, IWeatherProviderWeatherData, IWeatherUnits } from '../../interfaces';
44
import {
55
IGridpointsStations,
66
IPointsLatLngResponse,
@@ -11,6 +11,7 @@ import { IWeatherProvider } from '../IWeatherProvider';
1111
import { InvalidProviderLocationError } from '../../errors'; // Import the error class
1212
import { isLocationInUS } from '../../utils/locationUtils';
1313
import { standardizeCondition } from './condition';
14+
import { getCloudinessFromCloudLayers } from './cloudiness';
1415

1516
const log = debug('weather-plus:nws:client');
1617

@@ -19,16 +20,16 @@ export const WEATHER_KEYS = Object.values(IWeatherKey);
1920
export class NWSProvider implements IWeatherProvider {
2021
name = 'nws';
2122

22-
public async getWeather(lat: number, lng: number): Promise<IWeatherProviderWeatherData> {
23+
public async getWeather(lat: number, lng: number): Promise<Partial<IWeatherProviderWeatherData>> {
2324
// Check if the location is within the US
2425
if (!isLocationInUS(lat, lng)) {
2526
throw new InvalidProviderLocationError(
2627
'The NWS provider only supports locations within the United States.'
2728
);
2829
}
2930

30-
const data: Partial<IWeatherData> = {};
31-
const weatherData: IWeatherProviderWeatherData[] = [];
31+
const data: Partial<IWeatherProviderWeatherData> = {};
32+
const weatherData: Partial<IWeatherProviderWeatherData>[] = [];
3233

3334
try {
3435
const observationStations = await fetchObservationStationUrl(lat, lng);
@@ -68,7 +69,7 @@ export class NWSProvider implements IWeatherProvider {
6869
for (const key of WEATHER_KEYS) {
6970
const value = weatherData.find((data) => data[key]);
7071

71-
if (value && value[key]?.value) {
72+
if (value && typeof value[key]?.value !== 'undefined') {
7273
data[key] = value[key] as never;
7374
}
7475
}
@@ -77,7 +78,7 @@ export class NWSProvider implements IWeatherProvider {
7778
throw new Error('Invalid observation data');
7879
}
7980

80-
return data as IWeatherData;
81+
return data;
8182
} catch (error) {
8283
log('Error in getWeather:', error);
8384
throw error;
@@ -150,23 +151,23 @@ async function fetchLatestObservation(
150151
}
151152
}
152153

153-
function convertToWeatherData(observation: any): IWeatherProviderWeatherData {
154+
function convertToWeatherData(observation: IObservationsLatest): Partial<IWeatherProviderWeatherData> {
154155
const properties = observation.properties;
155-
156+
156157
return {
157158
dewPoint: {
158-
value: properties.dewpoint.value,
159+
value: properties.dewpoint.value!,
159160
unit:
160161
properties.dewpoint.unitCode === 'wmoUnit:degC'
161162
? IWeatherUnits.C
162163
: IWeatherUnits.F,
163164
},
164165
humidity: {
165-
value: properties.relativeHumidity.value,
166+
value: properties.relativeHumidity.value!,
166167
unit: IWeatherUnits.percent,
167168
},
168169
temperature: {
169-
value: properties.temperature.value,
170+
value: properties.temperature.value!,
170171
unit:
171172
properties.temperature.unitCode === 'wmoUnit:degC'
172173
? IWeatherUnits.C
@@ -177,5 +178,9 @@ function convertToWeatherData(observation: any): IWeatherProviderWeatherData {
177178
unit: IWeatherUnits.string,
178179
original: properties.textDescription
179180
},
181+
cloudiness: {
182+
value: getCloudinessFromCloudLayers(properties.cloudLayers),
183+
unit: IWeatherUnits.percent,
184+
},
180185
};
181186
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { getCloudinessFromCloudLayers } from './cloudiness';
2+
import { ICloudLayer } from './interfaces';
3+
4+
describe('getCloudinessFromCloudLayers', () => {
5+
it('should return 0 for null or empty cloud layers', () => {
6+
expect(getCloudinessFromCloudLayers(null as any)).toBe(0);
7+
expect(getCloudinessFromCloudLayers([])).toBe(0);
8+
});
9+
10+
it('should correctly calculate cloudiness for a single cloud layer', () => {
11+
const testCases: Array<{ cloudLayer: ICloudLayer, expected: number }> = [
12+
{
13+
cloudLayer: { base: { unitCode: 'wmoUnit:m', value: 1000 }, amount: 'CLR' },
14+
expected: 0
15+
},
16+
{
17+
cloudLayer: { base: { unitCode: 'wmoUnit:m', value: 1000 }, amount: 'SKC' },
18+
expected: 0
19+
},
20+
{
21+
cloudLayer: { base: { unitCode: 'wmoUnit:m', value: 1000 }, amount: 'FEW' },
22+
expected: 20
23+
},
24+
{
25+
cloudLayer: { base: { unitCode: 'wmoUnit:m', value: 1000 }, amount: 'SCT' },
26+
expected: 40
27+
},
28+
{
29+
cloudLayer: { base: { unitCode: 'wmoUnit:m', value: 1000 }, amount: 'BKN' },
30+
expected: 75
31+
},
32+
{
33+
cloudLayer: { base: { unitCode: 'wmoUnit:m', value: 1000 }, amount: 'OVC' },
34+
expected: 100
35+
},
36+
{
37+
cloudLayer: { base: { unitCode: 'wmoUnit:m', value: 1000 }, amount: 'VV' },
38+
expected: 100
39+
},
40+
{
41+
cloudLayer: { base: { unitCode: 'wmoUnit:m', value: 1000 }, amount: 'UNKNOWN' },
42+
expected: 0
43+
}
44+
];
45+
46+
testCases.forEach(({ cloudLayer, expected }) => {
47+
expect(getCloudinessFromCloudLayers([cloudLayer])).toBe(expected);
48+
});
49+
});
50+
51+
it('should correctly calculate the average cloudiness for multiple cloud layers', () => {
52+
const cloudLayers: Array<ICloudLayer> = [
53+
{ base: { unitCode: 'wmoUnit:m', value: 1000 }, amount: 'FEW' }, // 20%
54+
{ base: { unitCode: 'wmoUnit:m', value: 2000 }, amount: 'SCT' }, // 40%
55+
{ base: { unitCode: 'wmoUnit:m', value: 3000 }, amount: 'BKN' } // 75%
56+
];
57+
58+
// Average of 20%, 40%, and 75% = 45%
59+
expect(getCloudinessFromCloudLayers(cloudLayers)).toBe(45);
60+
});
61+
62+
it('should round the average cloudiness to the nearest integer', () => {
63+
const cloudLayers: Array<ICloudLayer> = [
64+
{ base: { unitCode: 'wmoUnit:m', value: 1000 }, amount: 'FEW' }, // 20%
65+
{ base: { unitCode: 'wmoUnit:m', value: 2000 }, amount: 'BKN' } // 75%
66+
];
67+
68+
// Average of 20% and 75% = 47.5%, which should round to 48%
69+
expect(getCloudinessFromCloudLayers(cloudLayers)).toBe(48);
70+
});
71+
});

0 commit comments

Comments
 (0)