import { Injectable, InternalServerErrorException, Logger, OnApplicationShutdown, } from '@nestjs/common'; import WS1080 from './module/ws1080'; import { Between, LessThan, MoreThan, Repository } from 'typeorm'; import { WeatherEntity } from './entities/weather.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Cron } from '@nestjs/schedule'; import { HistoryQueryDto } from './dtos/history-query.dto'; import { GraphQueryDto } from './dtos/graph-query.dto'; @Injectable() export class AppService implements OnApplicationShutdown { private logger = new Logger(AppService.name); public station?: WS1080; constructor( @InjectRepository(WeatherEntity) private readonly weatherRepository: Repository, ) {} /** * Get current weather information. * @returns Weather data */ async getWeather() { try { // Retrieve from USB await this.createStation(); const data = await this.station.read(); // Save weather data to database const entity = this.weatherRepository.create({ date: new Date(), ...data, }); await this.weatherRepository.save(entity); entity.fresh = true; entity.rain24h = await this.rainFall24h(); return entity; } catch (error) { // USB errors likely mean we are not connected anymore if (error.message?.includes('USB')) { try { this.station?.close(); } catch {} this.station = null; } this.logger.error('Failed to retrieve weather data:', error.stack); // Retrieve previous entry on error const previous = await this.getPreviousEntry(); if (!previous) throw new InternalServerErrorException(); return previous; } } /** * Get previous weather entry. * @returns Previous weather data entry */ async getPreviousEntry() { const [previous] = await this.weatherRepository.find({ order: { date: -1 }, take: 1, }); if (!previous) return null; previous.rain24h = await this.rainFall24h(previous.date); previous.fresh = false; return previous; } /** * Get weather history. * @param query Weather history search * @returns Paginated history list */ async getWeatherHistory( query: HistoryQueryDto, select?: (keyof WeatherEntity)[], ) { const pageSize = Number(query.pageSize) || 100; const page = Number(query.page) || 1; const [list, rowCount] = await this.weatherRepository.findAndCount({ select, where: query.since || query.until ? { date: query.since && query.until ? Between(new Date(query.since), new Date(query.until)) : query.since ? MoreThan(new Date(query.since)) : LessThan(new Date(query.until)), } : undefined, order: { date: -1 }, take: pageSize, skip: (page - 1) * pageSize, }); const pageCount = Math.ceil(rowCount / pageSize); return { list, pagination: { page, pageSize, pageCount, rowCount, }, }; } /** * Get graph datasets for weather. * @param query Weather history search * @returns Paginated graph dataset */ async graphWeatherHistory(query: GraphQueryDto) { const { list, pagination } = await this.getWeatherHistory(query, [ ...(query.columns as (keyof WeatherEntity)[]), 'date', ]); const datasets = query.columns.reduce( (mass, key: keyof WeatherEntity) => ({ ...mass, [key]: { label: String(key), data: [] }, }), {}, ); list.forEach((entry) => { Object.keys(entry) .filter((key) => !['id', 'date'].includes(key)) .forEach((key) => { datasets[key].data.push({ x: entry.date.getTime(), y: entry[key], }); }); }); return { list: Object.values(datasets), pagination, }; } /** * Get rainfall in the last 24h since `since` start point. * @param since Time start point * @returns Rainfall in mm */ async rainFall24h(since = new Date()) { const { rainfall } = await this.weatherRepository .createQueryBuilder('weather') .select('SUM(rainDiff)', 'rainfall') .where('date >= :date', { date: new Date(since.getTime() - 24 * 60 * 60 * 1000), }) .getRawOne(); return Number(rainfall) || 0; } /** * Create station instance. */ async createStation() { if (this.station) return; this.station = WS1080.fromDevice(); const previous = await this.getPreviousEntry(); if (previous) { this.station.previousRain = previous.totalRain; } } @Cron('0 * * * *') scheduledPulls() { this.getWeather().catch(() => { // do nothing }); } onApplicationShutdown() { this.station?.close(); } }