diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 7afbaa5..064deba 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -465,7 +465,16 @@ export class ControllerRoute { 'This endpoint retrieves the terms and conditions for the application.'; }; }; + static WEATHER = class { + public static readonly ROUTE = 'weather'; + static ACTIONS = class { + public static readonly FETCH_WEATHER_DETAILS_SUMMARY = + 'Fetch Weather Details'; + public static readonly FETCH_WEATHER_DETAILS_DESCRIPTION = + 'This endpoint retrieves the current weather details for a specified location like temperature, humidity, etc.'; + }; + }; static PRIVACY_POLICY = class { public static readonly ROUTE = 'policy'; diff --git a/libs/common/src/util/calculate.aqi.ts b/libs/common/src/util/calculate.aqi.ts new file mode 100644 index 0000000..7911ad6 --- /dev/null +++ b/libs/common/src/util/calculate.aqi.ts @@ -0,0 +1,18 @@ +export function calculateAQI(pm2_5: number): number { + const breakpoints = [ + { pmLow: 0.0, pmHigh: 12.0, aqiLow: 0, aqiHigh: 50 }, + { pmLow: 12.1, pmHigh: 35.4, aqiLow: 51, aqiHigh: 100 }, + { pmLow: 35.5, pmHigh: 55.4, aqiLow: 101, aqiHigh: 150 }, + { pmLow: 55.5, pmHigh: 150.4, aqiLow: 151, aqiHigh: 200 }, + { pmLow: 150.5, pmHigh: 250.4, aqiLow: 201, aqiHigh: 300 }, + { pmLow: 250.5, pmHigh: 500.4, aqiLow: 301, aqiHigh: 500 }, + ]; + + const bp = breakpoints.find((b) => pm2_5 >= b.pmLow && pm2_5 <= b.pmHigh); + if (!bp) return pm2_5 > 500.4 ? 500 : 0; // Handle out-of-range values + + return Math.round( + ((bp.aqiHigh - bp.aqiLow) / (bp.pmHigh - bp.pmLow)) * (pm2_5 - bp.pmLow) + + bp.aqiLow, + ); +} diff --git a/src/app.module.ts b/src/app.module.ts index e674880..bde273d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -38,6 +38,7 @@ import { HealthModule } from './health/health.module'; import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger'; import { OccupancyModule } from './occupancy/occupancy.module'; +import { WeatherModule } from './weather/weather.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -79,6 +80,7 @@ import { OccupancyModule } from './occupancy/occupancy.module'; PowerClampModule, HealthModule, OccupancyModule, + WeatherModule, ], providers: [ { diff --git a/src/config/index.ts b/src/config/index.ts index d7d0014..b1d9833 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,4 +1,5 @@ import AuthConfig from './auth.config'; import AppConfig from './app.config'; import JwtConfig from './jwt.config'; -export default [AuthConfig, AppConfig, JwtConfig]; +import WeatherOpenConfig from './weather.open.config'; +export default [AuthConfig, AppConfig, JwtConfig, WeatherOpenConfig]; diff --git a/src/config/weather.open.config.ts b/src/config/weather.open.config.ts new file mode 100644 index 0000000..2182490 --- /dev/null +++ b/src/config/weather.open.config.ts @@ -0,0 +1,8 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'openweather-config', + (): Record => ({ + OPEN_WEATHER_MAP_API_KEY: process.env.OPEN_WEATHER_MAP_API_KEY, + }), +); diff --git a/src/weather/controllers/index.ts b/src/weather/controllers/index.ts new file mode 100644 index 0000000..8157369 --- /dev/null +++ b/src/weather/controllers/index.ts @@ -0,0 +1 @@ +export * from './weather.controller'; diff --git a/src/weather/controllers/weather.controller.ts b/src/weather/controllers/weather.controller.ts new file mode 100644 index 0000000..280e09d --- /dev/null +++ b/src/weather/controllers/weather.controller.ts @@ -0,0 +1,28 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; // Assuming this is where the routes are defined +import { WeatherService } from '../services'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { GetWeatherDetailsDto } from '../dto/get.weather.dto'; + +@ApiTags('Weather Module') +@Controller({ + version: EnableDisableStatusEnum.ENABLED, + path: ControllerRoute.WEATHER.ROUTE, // use the static route constant +}) +export class WeatherController { + constructor(private readonly weatherService: WeatherService) {} + + @Get() + @ApiOperation({ + summary: ControllerRoute.WEATHER.ACTIONS.FETCH_WEATHER_DETAILS_SUMMARY, + description: + ControllerRoute.WEATHER.ACTIONS.FETCH_WEATHER_DETAILS_DESCRIPTION, + }) + async fetchWeatherDetails( + @Query() query: GetWeatherDetailsDto, + ): Promise { + return await this.weatherService.fetchWeatherDetails(query); + } +} diff --git a/src/weather/dto/get.weather.dto.ts b/src/weather/dto/get.weather.dto.ts new file mode 100644 index 0000000..86aefdd --- /dev/null +++ b/src/weather/dto/get.weather.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNumber } from 'class-validator'; + +export class GetWeatherDetailsDto { + @ApiProperty({ + description: 'Latitude coordinate', + example: 35.6895, + }) + @IsNumber() + @Type(() => Number) + lat: number; + + @ApiProperty({ + description: 'Longitude coordinate', + example: 139.6917, + }) + @IsNumber() + @Type(() => Number) + lon: number; +} diff --git a/src/weather/services/index.ts b/src/weather/services/index.ts new file mode 100644 index 0000000..9b2cb64 --- /dev/null +++ b/src/weather/services/index.ts @@ -0,0 +1 @@ +export * from './weather.service'; diff --git a/src/weather/services/weather.service.ts b/src/weather/services/weather.service.ts new file mode 100644 index 0000000..11e1677 --- /dev/null +++ b/src/weather/services/weather.service.ts @@ -0,0 +1,39 @@ +import { HttpStatus, Injectable } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; +import { GetWeatherDetailsDto } from '../dto/get.weather.dto'; +import { calculateAQI } from '@app/common/util/calculate.aqi'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; + +@Injectable() +export class WeatherService { + constructor( + private readonly configService: ConfigService, + private readonly httpService: HttpService, + ) {} + + async fetchWeatherDetails( + query: GetWeatherDetailsDto, + ): Promise { + const { lat, lon } = query; + const weatherApiKey = this.configService.get( + 'OPEN_WEATHER_MAP_API_KEY', + ); + const url = `http://api.weatherapi.com/v1/current.json?key=${weatherApiKey}&q=${lat},${lon}&aqi=yes`; + + const response = await firstValueFrom(this.httpService.get(url)); + const pm2_5 = response.data.current.air_quality.pm2_5; // Raw PM2.5 (µg/m³) + + return new SuccessResponseDto({ + message: `Weather details fetched successfully`, + data: { + aqi: calculateAQI(pm2_5), // Converted AQI (0-500) + temperature: response.data.current.temp_c, + humidity: response.data.current.humidity, + }, + statusCode: HttpStatus.OK, + }); + } +} diff --git a/src/weather/weather.module.ts b/src/weather/weather.module.ts new file mode 100644 index 0000000..eb65b5b --- /dev/null +++ b/src/weather/weather.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { HttpModule } from '@nestjs/axios'; // <-- Import this! +import { WeatherController } from './controllers'; +import { WeatherService } from './services'; + +@Module({ + imports: [ConfigModule, HttpModule], + controllers: [WeatherController], + providers: [WeatherService], +}) +export class WeatherModule {}