dev smartfarming #1

Merged
agrilink merged 53 commits from development into main 2024-12-30 05:53:19 +00:00
13 changed files with 217 additions and 180 deletions
Showing only changes of commit fac32f6978 - Show all commits

View File

@ -2,18 +2,18 @@ export interface ParameterSensor {
hour: number;
//for DHT sensor
vicitemperature_avg?: number;
vicihumidity_avg?: number;
viciluminosity_avg?: number;
vicitemperature?: number;
vicihumidity?: number;
viciluminosity?: number;
// for NPK1 & NPK2 sensor
soiltemperature_avg?: number;
soilhumidity_avg?: number;
soilconductivity_avg?: number;
soilph_avg?: number;
soilnitrogen_avg?: number;
soilphosphorus_avg?: number;
soilpotassium_avg?: number;
soiltemperature?: number;
soilhumidity?: number;
soilconductivity?: number;
soilph?: number;
soilnitrogen?: number;
soilphosphorus?: number;
soilpotassium?: number;
}
export interface ApiResponse {
@ -49,3 +49,8 @@ export interface SensorData {
npk2: NPKSensor;
}
export interface StatusRelay {
number: number;
current_status: boolean;
}

View File

@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ApiService } from './api.service';
describe('ApiService', () => {
let service: ApiService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ApiService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AuthService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -1,10 +1,12 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { catchError, tap } from 'rxjs/operators';
import { ApiService } from './api.service';
import { LoginData } from '../interface/auth';
import {jwtDecode}from 'jwt-decode';
import { StorageService } from './storage.service';
import { ToastrService } from 'ngx-toastr';
@Injectable({
providedIn: 'root'
@ -14,11 +16,12 @@ export class AuthService extends ApiService {
private logoutUrl = `${this.baseUrl}auth/logout`;
private registerUrl = `${this.baseUrl}auth/register`;
constructor(http: HttpClient) {
constructor(http: HttpClient,
private storageService: StorageService,
private toast: ToastrService) {
super(http);
}
login(data: LoginData): Observable<any> {
const headers = new HttpHeaders({
Authorization: 'Basic ' + btoa(`${data.email}:${data.password}`)
@ -30,20 +33,16 @@ export class AuthService extends ApiService {
return this.http.post<any>(this.authUrl, formData, { headers }).pipe(
tap(response => {
const accessToken = response.data.token;
this.saveTokens(accessToken);
this.storageService.saveToken(accessToken);
const jwtToken = response.data.jwtToken;
const decodedToken: any = jwtDecode(jwtToken);
this.saveUserDataToStorage(decodedToken.user.fullname, decodedToken.user.avatar);
this.storageService.saveUserData(decodedToken.user.fullname, decodedToken.user.avatar);
})
);
}
saveTokens(token: string) {
localStorage.setItem('accessToken', token);
}
logout(): Observable<any> {
const token = localStorage.getItem('accessToken');
const headers = new HttpHeaders({
@ -52,8 +51,12 @@ export class AuthService extends ApiService {
return this.http.post<any>(this.logoutUrl, {}, { headers }).pipe(
tap(() => {
localStorage.removeItem('accessToken');
this.clearUserDataFromStorage();
this.storageService.clearToken();
this.storageService.clearUserData();
}),
catchError(error => {
this.toast.error('Failed to logout');
return error;
})
);
}
@ -61,27 +64,18 @@ export class AuthService extends ApiService {
register(data: any): Observable<any> {
const headers = new HttpHeaders({});
return this.http.post<any>(this.registerUrl, data, { headers }).pipe(
// tap(response => {
// console.log('Registration response:', response);
// })
catchError(error => {
this.toast.error('Failed to register');
return error;
})
);
}
getUserFullName(): string | null {
return localStorage.getItem('userFullName');
return this.storageService.getUserFullName();
}
getAvatar(): string | null {
return localStorage.getItem('avatar');
}
private saveUserDataToStorage(fullName: string | null, avatar: string | null) {
localStorage.setItem('userFullName', fullName || '');
localStorage.setItem('avatar', avatar || '');
}
private clearUserDataFromStorage() {
localStorage.removeItem('userFullName');
localStorage.removeItem('avatar');
return this.storageService.getAvatar();
}
}

View File

@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { SensorService } from './sensor.service';
describe('SensorService', () => {
let service: SensorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SensorService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -3,53 +3,66 @@ import { HttpParams, HttpClient, HttpHeaders, HttpErrorResponse } from '@angular
import { catchError, Observable, throwError, tap } from 'rxjs';
import { ApiService } from './api.service';
import { ApiResponse } from '../interface/sensor-data';
import { StorageService } from './storage.service';
import { ToastrService } from 'ngx-toastr';
@Injectable({
providedIn: 'root'
})
export class SensorService extends ApiService {
constructor(http: HttpClient) {
constructor(http: HttpClient,
private storageService: StorageService,
private toast: ToastrService) {
super(http);
}
private getDataUrl = `${this.baseUrl}api/sensor/getData`;
private getLatestUrl = `${this.baseUrl}api/sensor/getLatest`;
private getStatusRelay = `${this.baseUrl}api/get-relay`;
private createAuthHeaders(): HttpHeaders {
const token = this.storageService.getToken();
return new HttpHeaders({
Authorization: `Bearer ${token}`
});
}
getSensorData(sensor: string, metric: string, startEnd: string, timeRange: string): Observable<ApiResponse> {
const url = `${this.baseUrl}api/sensor/getData`;
const params = new HttpParams()
.set('range[end]', startEnd)
.set('range[time_range]', timeRange)
.set('sensor', sensor)
.set('metric', metric);
const token = localStorage.getItem('accessToken');
const headers = new HttpHeaders({
Authorization: 'Bearer ' + token,
});
const headers= this.createAuthHeaders();
return this.http.get<ApiResponse>(url, { params, headers }).pipe(
catchError((error: HttpErrorResponse) => {
if (error.error instanceof ErrorEvent) {
console.error('An error occurred:', error.error.message);
} else {
console.error(`Backend returned code ${error.status}, body was: ${error.error}`);
}
return throwError('Something went wrong; please try again later.');
return this.http.get<ApiResponse>(this.getDataUrl, { params, headers }).pipe(
catchError(error => {
this.toast.error('Failed to get sensor data, please try again');
return throwError(error);
})
);
}
getLatestData(): Observable<ApiResponse> {
const url = `${this.baseUrl}api/sensor/getLatest`;
const headers = new HttpHeaders({
Authorization: 'Bearer ' + localStorage.getItem('accessToken'),
const headers = this.createAuthHeaders();
});
return this.http.get<any>(url, { headers }).pipe(
return this.http.get<any>(this.getLatestUrl, { headers }).pipe(
catchError(error => {
console.error('API Error:', error);
this.toast.error('Failed to get sensor data, please try again');
return throwError(error);
})
);
}
getRelayStatus(): Observable<ApiResponse> {
const headers = this.createAuthHeaders();
return this.http.get<any>(this.getStatusRelay, { headers }).pipe(
catchError(error => {
this.toast.error('Failed to get relay status, please try again');
return throwError(error);
})
);
}
}

View File

@ -0,0 +1,36 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StorageService {
saveToken(token: string): void {
localStorage.setItem('accessToken', token);
}
getToken(): string | null {
return localStorage.getItem('accessToken');
}
clearToken(): void {
localStorage.removeItem('accessToken');
}
saveUserData(fullName: string | null, avatar: string | null): void {
localStorage.setItem('userFullName', fullName || '');
localStorage.setItem('avatar', avatar || '');
}
getUserFullName(): string | null {
return localStorage.getItem('userFullName');
}
getAvatar(): string | null {
return localStorage.getItem('avatar');
}
clearUserData(): void {
localStorage.removeItem('userFullName');
localStorage.removeItem('avatar');
}
}

View File

@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { AuthService } from '../../cores/services/auth.service';
import { StorageService } from '../../cores/services/storage.service';
import { FormsModule } from '@angular/forms';
import { LoginData } from '../../cores/interface/auth';
import { ToastrService } from 'ngx-toastr';
@ -17,7 +18,7 @@ export class AuthComponent {
password: string = '';
rememberMe: boolean = false;
constructor(private authService: AuthService, private router: Router, private toastr: ToastrService) {}
constructor(private authService: AuthService, private storageService: StorageService, private router: Router, private toastr: ToastrService) {}
onSubmit() {
const loginData: LoginData = {
@ -28,7 +29,7 @@ export class AuthComponent {
this.authService.login(loginData).subscribe(
(response) => {
this.authService.saveTokens(response.data.token);
this.storageService.saveToken(response.data.token);
this.router.navigate(['/dashboard']);
this.toastr.success('Login successful');
},

View File

@ -8,10 +8,10 @@
</div>
<div>
<button [ngClass]="{'active-button': selectedButton === 'dht'}" (click)="selectSensor('dht')">DHT</button>
<button [ngClass]="{'active-button': selectedButton === 'dht'}" (click)="selectSensor('dht')">BHT</button>
<button [ngClass]="{'active-button': selectedButton === 'npk1'}" (click)="selectSensor('npk1')">NPK 1</button>
<button [ngClass]="{'active-button': selectedButton === 'npk2'}" (click)="selectSensor('npk2')">NPK 2</button>
<!-- <button [ngClass]="{'active-button': selectedButton === 'relay'}" (click)="selectSensor('relay')">Relay</button> -->
<button [ngClass]="{'active-button': selectedButton === 'relay'}" (click)="selectSensor('relay')">Relay</button>
</div>
<div *ngIf="isLoaded " class="loading">
@ -33,7 +33,7 @@
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.dht.humidity}}%</h3>
<h3>{{sensorData.dht.humidity}} %RH</h3>
<h6>Humidity</h6>
</div>
</div>
@ -48,13 +48,13 @@
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.moisture}}%</h3>
<h3>{{sensorData.npk1.moisture}} %RH</h3>
<h6>Moisture</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.conductivity}}mS/cm</h3>
<h3>{{sensorData.npk1.conductivity}} μS/cm</h3>
<h6>Conductivity</h6>
</div>
</div>
@ -66,19 +66,19 @@
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.nitrogen}}PPM</h3>
<h3>{{sensorData.npk1.nitrogen}} PPM</h3>
<h6>Nitrogen</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.phosphorus}}PPM</h3>
<h3>{{sensorData.npk1.phosphorus}} PPM</h3>
<h6>Phosphorus</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.potassium}}PPM</h3>
<h3>{{sensorData.npk1.potassium}} PPM</h3>
<h6>Potassium</h6>
</div>
</div>
@ -93,13 +93,13 @@
</div>
<div class="card-parameter">
<div>
<h3>{{ sensorData.npk1.moisture }}%</h3>
<h3>{{ sensorData.npk1.moisture }} %RH</h3>
<h6>Moisture</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{ sensorData.npk1.conductivity }}mS/cm</h3>
<h3>{{ sensorData.npk1.conductivity }} μS/cm</h3>
<h6>Conductivity</h6>
</div>
</div>
@ -111,39 +111,35 @@
</div>
<div class="card-parameter">
<div>
<h3>{{ sensorData.npk1.nitrogen}}PPM</h3>
<h3>{{ sensorData.npk1.nitrogen}} PPM</h3>
<h6>Nitrogen</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{ sensorData.npk2.phosphorus}}PPM</h3>
<h3>{{ sensorData.npk2.phosphorus}} PPM</h3>
<h6>Phosphorus</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{ sensorData.npk2.potassium }}PPM</h3>
<h3>{{ sensorData.npk2.potassium }} PPM</h3>
<h6>Potassium</h6>
</div>
</div>
</div>
<!-- <div *ngIf="selectedButton === 'relay'" class="card-container">
<div class="card-parameter">
<div *ngIf="!isLoaded && selectedButton === 'relay'" class="card-container">
<div class="card-parameter" *ngFor="let relay of relayStatus;">
<div>
<h3>ON</h3>
<h6>Nutrition</h6>
<h3 [ngClass]="relay.current_status ? 'status-on' : 'status-off'">
{{ relay.current_status ? 'ON' : 'OFF' }}
</h3>
<h6>Relay {{ relay.number }}</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>OFF</h3>
<h6>Nutrition</h6>
</div>
</div>
</div> -->
<div class="graph">
<div class="title-graph">Monitoring</div>

View File

@ -99,3 +99,11 @@ button {
text-align: center;
color: #888;
}
.status-on {
color: #16423C;
}
.status-off {
color: rgb(144, 6, 6);
}

View File

@ -4,7 +4,7 @@ import { SidebarComponent } from './layouts/sidebar/sidebar.component';
import { GraphComponent } from './page/graph/graph.component';
import { CommonModule } from '@angular/common';
import { SensorService } from '../../cores/services/sensor.service';
import { SensorData } from '../../cores/interface/sensor-data';
import { SensorData, StatusRelay } from '../../cores/interface/sensor-data';
import { interval } from 'rxjs';
@Component({
@ -24,6 +24,7 @@ export class DashboardComponent implements OnInit {
npk1: { temperature: 0, moisture: 0, conductivity: 0, ph: 0, nitrogen: 0, phosphorus: 0, potassium: 0 },
npk2: { temperature: 0, moisture: 0, conductivity: 0, ph: 0, nitrogen: 0, phosphorus: 0, potassium: 0 }
};
relayStatus: StatusRelay[] = [];
constructor(private apiService: SensorService) {}
@ -63,8 +64,12 @@ export class DashboardComponent implements OnInit {
selectSensor(param: string): void {
this.selectedButton = param;
if(param==='relay'){
this.loadRelayData();
}else{
this.loadData();
}
}
loadData(): void {
this.isLoaded = true;
@ -72,35 +77,36 @@ export class DashboardComponent implements OnInit {
this.apiService.getLatestData().subscribe(
(response) => {
const data = response.data;
console.log('Data:', data);
if (data.dht && data.dht.length > 0) {
this.sensorData.dht = {
lightIntensity: data.dht[0].viciluminosity_avg ?? 0,
temperature: data.dht[0].vicitemperature_avg ?? 0,
humidity: data.dht[0].vicihumidity_avg ?? 0
lightIntensity: data.dht[0].viciluminosity ?? 0,
temperature: data.dht[0].vicitemperature ?? 0,
humidity: data.dht[0].vicihumidity ?? 0
};
}
if (data.npk1 && data.npk1.length > 0) {
this.sensorData.npk1 = {
temperature: data.npk1[0].soiltemperature_avg ?? 0,
moisture: data.npk1[0].soilhumidity_avg ?? 0,
conductivity: data.npk1[0].soilconductivity_avg ?? 0,
ph: data.npk1[0].soilph_avg ?? 0,
nitrogen: data.npk1[0].soilnitrogen_avg ?? 0,
phosphorus: data.npk1[0].soilphosphorus_avg ?? 0,
potassium: data.npk1[0].soilpotassium_avg ?? 0
temperature: data.npk1[0].soiltemperature ?? 0,
moisture: data.npk1[0].soilhumidity ?? 0,
conductivity: data.npk1[0].soilconductivity ?? 0,
ph: data.npk1[0].soilph ?? 0,
nitrogen: data.npk1[0].soilnitrogen ?? 0,
phosphorus: data.npk1[0].soilphosphorus ?? 0,
potassium: data.npk1[0].soilpotassium ?? 0
};
}
if (data.npk2 && data.npk2.length > 0) {
this.sensorData.npk2 = {
temperature: data.npk2[0].soiltemperature_avg ?? 0,
moisture: data.npk2[0].soilhumidity_avg ?? 0,
conductivity: data.npk2[0].soilconductivity_avg ?? 0,
ph: data.npk2[0].soilph_avg ?? 0,
nitrogen: data.npk2[0].soilnitrogen_avg ?? 0,
phosphorus: data.npk2[0].soilphosphorus_avg ?? 0,
potassium: data.npk2[0].soilpotassium_avg ?? 0
temperature: data.npk2[0].soiltemperature ?? 0,
moisture: data.npk2[0].soilhumidity ?? 0,
conductivity: data.npk2[0].soilconductivity ?? 0,
ph: data.npk2[0].soilph ?? 0,
nitrogen: data.npk2[0].soilnitrogen ?? 0,
phosphorus: data.npk2[0].soilphosphorus ?? 0,
potassium: data.npk2[0].soilpotassium ?? 0
};
}
@ -113,5 +119,31 @@ export class DashboardComponent implements OnInit {
}
);
}
loadRelayData(): void {
this.isLoaded = true;
this.apiService.getRelayStatus().subscribe(
(response) => {
if (Array.isArray(response.data)) {
this.relayStatus = response.data.map((relay) => ({
id: relay.id,
number: relay.number,
enabled_at: relay.enabled_at,
disabled_at: relay.disabled_at,
created_at: relay.created_at,
current_status: relay.current_status
}));
}
console.log('Relay Data:', response);
this.isLoaded = false;
},
(error) => {
console.error('Error fetching relay data:', error);
this.isLoaded = false;
}
);
}
}

View File

@ -1,6 +1,6 @@
<div class="container-graph">
<select class="form-select" id="sensorSelect" (change)="onSensorChange($event)">
<option value="dht">DHT</option>
<option value="dht">BHT</option>
<option value="npk1">NPK 1</option>
<option value="npk2">NPK 2</option>
</select>

View File

@ -7,16 +7,16 @@ import { FormsModule } from '@angular/forms';
Chart.register(...registerables);
const parameterColors: { [key: string]: string } = {
vicitemperature_avg: '#8F5A62',
vicihumidity_avg: '#16423C',
viciluminosity_avg: '#DF9B55',
soiltemperature_avg: '#8F5A62',
soilhumidity_avg: '#54909c',
soilconductivity_avg: '#661311',
soilph_avg: '#664735',
soilnitrogen_avg: '#3a6635',
soilphosphorus_avg: '#3f3566',
soilpotassium_avg: '#5f3566',
vicitemperature: '#8F5A62',
vicihumidity: '#16423C',
viciluminosity: '#DF9B55',
soiltemperature: '#8F5A62',
soilhumidity: '#54909c',
soilconductivity: '#661311',
soilph: '#664735',
soilnitrogen: '#3a6635',
soilphosphorus: '#3f3566',
soilpotassium: '#5f3566',
};
@Component({
@ -34,22 +34,22 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
isLoading: boolean = true;
sensorParameters: { [key: string]: string[] } = {
dht: ['vicitemperature_avg', 'vicihumidity_avg', 'viciluminosity_avg'],
npk1: ['soiltemperature_avg', 'soilhumidity_avg', 'soilconductivity_avg', 'soilph_avg', 'soilnitrogen_avg', 'soilphosphorus_avg', 'soilpotassium_avg'],
npk2: ['soiltemperature_avg', 'soilhumidity_avg', 'soilconductivity_avg', 'soilph_avg', 'soilnitrogen_avg', 'soilphosphorus_avg', 'soilpotassium_avg'],
dht: ['vicitemperature', 'vicihumidity', 'viciluminosity'],
npk1: ['soiltemperature', 'soilhumidity', 'soilconductivity', 'soilph', 'soilnitrogen', 'soilphosphorus', 'soilpotassium'],
npk2: ['soiltemperature', 'soilhumidity', 'soilconductivity', 'soilph', 'soilnitrogen', 'soilphosphorus', 'soilpotassium'],
};
parameterDisplayNames: { [key: string]: string } = {
vicitemperature_avg: 'Temperature (°C)',
vicihumidity_avg: 'Humidity (%)',
viciluminosity_avg: 'Luminosity (lx)',
soiltemperature_avg: 'Soil Temperature (°C)',
soilhumidity_avg: 'Soil Humidity (%)',
soilconductivity_avg: 'Conductivity (mS/cm)',
soilph_avg: 'pH',
soilnitrogen_avg: 'Nitrogen (PPM)',
soilphosphorus_avg: 'Phosphorus (PPM)',
soilpotassium_avg: 'Potassium (PPM)',
vicitemperature: 'Temperature (°C)',
vicihumidity: 'Humidity (%)',
viciluminosity: 'Luminosity (lux)',
soiltemperature: 'Soil Temperature (°C)',
soilhumidity: 'Soil Humidity (%)',
soilconductivity: 'Conductivity (μS/cm)',
soilph: 'pH',
soilnitrogen: 'Nitrogen (PPM)',
soilphosphorus: 'Phosphorus (PPM)',
soilpotassium: 'Potassium (PPM)',
};
chart: Chart | undefined;
@ -61,7 +61,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
ngOnInit(): void {
this.selectedSensor = 'dht';
this.updateParameters();
this.selectedParameter = 'vicitemperature_avg';
this.selectedParameter = 'vicitemperature';
this.updateChart();
this.resizeListener = this.onResize.bind(this);
window.addEventListener('resize', this.resizeListener);
@ -168,9 +168,9 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
this.selectedSensor = select.value;
this.updateParameters();
if (this.selectedSensor === 'dht') {
this.selectedParameter = 'vicitemperature_avg';
this.selectedParameter = 'vicitemperature';
} else if (this.selectedSensor === 'npk1' || this.selectedSensor === 'npk2') {
this.selectedParameter = 'soiltemperature_avg';
this.selectedParameter = 'soiltemperature';
}
this.updateChart();
}