homeweatherapi/src/app.service.ts

192 lines
4.8 KiB
TypeScript

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<WeatherEntity>,
) {}
/**
* 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;
}
}
onApplicationShutdown() {
this.station?.close();
}
}