smartplant 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -22
- package/package.json +121 -117
- package/src/cli.js +7 -0
- package/src/language/messages-de.js +42 -0
- package/src/language/messages-en.js +42 -0
- package/src/language/messages-es.js +42 -0
- package/src/language/messages-fr.js +42 -0
- package/src/language/messages-it.js +42 -0
- package/src/language/messages-ja.js +42 -0
- package/src/language/messages-nl.js +42 -0
- package/src/language/messages-pt.js +42 -0
- package/src/language/messages-ru.js +42 -0
- package/src/language/messages-zh.js +42 -0
- package/src/main.js +436 -0
- package/docs/banner.png +0 -0
- package/main.js +0 -606
package/main.js
DELETED
|
@@ -1,606 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import inquirer from 'inquirer';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
import { SerialPort } from 'serialport';
|
|
6
|
-
import { ReadlineParser } from '@serialport/parser-readline';
|
|
7
|
-
import chalk from 'chalk';
|
|
8
|
-
import { fileURLToPath } from 'url';
|
|
9
|
-
import axios from 'axios';
|
|
10
|
-
|
|
11
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
-
const __dirname = path.dirname(__filename);
|
|
13
|
-
|
|
14
|
-
function loadMessages(language) {
|
|
15
|
-
const languageFile = path.join(__dirname, `language/messages-${language}.json`);
|
|
16
|
-
try {
|
|
17
|
-
return JSON.parse(fs.readFileSync(languageFile, 'utf8'));
|
|
18
|
-
} catch (error) {
|
|
19
|
-
console.error(`Error loading language file: ${error.message}`);
|
|
20
|
-
return JSON.parse(fs.readFileSync(path.join(__dirname, 'language/messages-en.json'), 'utf8'));
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
class AIClient {
|
|
25
|
-
constructor(type, apiKey, localModel) {
|
|
26
|
-
this.type = type;
|
|
27
|
-
this.apiKey = apiKey;
|
|
28
|
-
this.localModel = localModel;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async generateResponse(prompt, language) {
|
|
32
|
-
const languagePrompt = `Respond in ${language}. `;
|
|
33
|
-
const fullPrompt = languagePrompt + prompt;
|
|
34
|
-
|
|
35
|
-
switch (this.type) {
|
|
36
|
-
case 'openai':
|
|
37
|
-
return this.generateOpenAIResponse(fullPrompt);
|
|
38
|
-
case 'local':
|
|
39
|
-
return this.generateLocalResponse(fullPrompt);
|
|
40
|
-
default:
|
|
41
|
-
throw new Error('Unsupported AI type');
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async generateOpenAIResponse(prompt) {
|
|
46
|
-
try {
|
|
47
|
-
const response = await axios.post('https://api.openai.com/v1/engines/davinci-codex/completions', {
|
|
48
|
-
prompt: prompt,
|
|
49
|
-
max_tokens: 500,
|
|
50
|
-
n: 1,
|
|
51
|
-
stop: null,
|
|
52
|
-
temperature: 0.7,
|
|
53
|
-
}, {
|
|
54
|
-
headers: {
|
|
55
|
-
'Authorization': `Bearer ${this.apiKey}`,
|
|
56
|
-
'Content-Type': 'application/json'
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
return response.data.choices[0].text.trim();
|
|
60
|
-
} catch (error) {
|
|
61
|
-
console.error('Error generating OpenAI response:', error);
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async generateLocalResponse(prompt) {
|
|
67
|
-
try {
|
|
68
|
-
const command = `ollama run ${this.localModel} "${this.sanitizeInput(prompt)}"`;
|
|
69
|
-
const output = execSync(command, { encoding: 'utf-8' });
|
|
70
|
-
return output.trim();
|
|
71
|
-
} catch (error) {
|
|
72
|
-
console.error('Error generating local response:', error);
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
sanitizeInput(input) {
|
|
78
|
-
return input.replace(/"/g, '\\"').replace(/\n/g, ' ');
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
class AIDetector {
|
|
83
|
-
async detectAI() {
|
|
84
|
-
try {
|
|
85
|
-
const output = execSync('ollama list', { encoding: 'utf-8' });
|
|
86
|
-
const models = output.split('\n')
|
|
87
|
-
.filter(line => line.trim() && !line.startsWith('NAME'))
|
|
88
|
-
.map(line => line.split(' ')[0]);
|
|
89
|
-
if (models.length > 0) {
|
|
90
|
-
return {
|
|
91
|
-
name: 'ollama',
|
|
92
|
-
models: models
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
} catch (error) {
|
|
96
|
-
console.error('Error detecting Ollama:', error.message);
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
class PlantDates {
|
|
103
|
-
constructor(name, type, aiClient, language) {
|
|
104
|
-
this.name = name;
|
|
105
|
-
this.type = type;
|
|
106
|
-
this.aiClient = aiClient;
|
|
107
|
-
this.language = language;
|
|
108
|
-
this.plantInfo = null;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async generateInfo() {
|
|
112
|
-
console.log(chalk.bold('🔍🌿 Generating plant information...'));
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const plantInfoPrompt = `Provide a comprehensive summary for ${this.name} (${this.type}) including: Lighting, Watering, Temperature, Humidity, Soil, Fertilization, Pruning, and Propagation. Also, provide specific ranges for Lighting (in lux), Temperature (in Celsius), and Humidity (in percentage) in the format: "Lighting: X-Y lux, Temperature: A-B°C, Humidity: C-D%".`;
|
|
116
|
-
|
|
117
|
-
const plantInfoResponse = await this.getAIResponse(plantInfoPrompt);
|
|
118
|
-
|
|
119
|
-
// Extract ranges from the response
|
|
120
|
-
const lightingMatch = plantInfoResponse.match(/Lighting:\s*(\d+)-(\d+)\s*lux/i);
|
|
121
|
-
const temperatureMatch = plantInfoResponse.match(/Temperature:\s*(\d+)-(\d+)\s*°C/i);
|
|
122
|
-
const humidityMatch = plantInfoResponse.match(/Humidity:\s*(\d+)-(\d+)\s*%/i);
|
|
123
|
-
|
|
124
|
-
this.plantInfo = {
|
|
125
|
-
summary: plantInfoResponse,
|
|
126
|
-
lighting: lightingMatch ? { min: parseInt(lightingMatch[1]), max: parseInt(lightingMatch[2]) } : { min: 50, max: 700 },
|
|
127
|
-
temperature: temperatureMatch ? { min: parseInt(temperatureMatch[1]), max: parseInt(temperatureMatch[2]) } : { min: 18, max: 24 },
|
|
128
|
-
humidity: humidityMatch ? { min: parseInt(humidityMatch[1]), max: parseInt(humidityMatch[2]) } : { min: 40, max: 60 }
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
return this.plantInfo;
|
|
132
|
-
} catch (error) {
|
|
133
|
-
console.error('Error generating plant info:', error);
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async getAIResponse(prompt) {
|
|
139
|
-
try {
|
|
140
|
-
const response = await this.aiClient.generateResponse(prompt, this.language);
|
|
141
|
-
await this.simulateTyping(response);
|
|
142
|
-
return response.trim();
|
|
143
|
-
} catch (error) {
|
|
144
|
-
console.error('Error getting AI response:', error);
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async simulateTyping(text) {
|
|
150
|
-
for (let i = 0; i < text.length; i++) {
|
|
151
|
-
process.stdout.write(text[i]);
|
|
152
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
153
|
-
}
|
|
154
|
-
console.log('\n');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
formatPlantInfo() {
|
|
158
|
-
if (!this.plantInfo) return 'No hay información disponible.';
|
|
159
|
-
|
|
160
|
-
return `
|
|
161
|
-
Rangos ideales:
|
|
162
|
-
🌞 ${this.plantInfo.lighting.min}-${this.plantInfo.lighting.max} lux
|
|
163
|
-
🌡️ ${this.plantInfo.temperature.min}-${this.plantInfo.temperature.max}°C
|
|
164
|
-
💦 ${this.plantInfo.humidity.min}-${this.plantInfo.humidity.max}%
|
|
165
|
-
`;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
class SmartPlant {
|
|
170
|
-
constructor() {
|
|
171
|
-
this.plantType = null;
|
|
172
|
-
this.plantName = '';
|
|
173
|
-
this.alerts = {};
|
|
174
|
-
this.language = 'en';
|
|
175
|
-
this.messages = null;
|
|
176
|
-
this.sensors = {
|
|
177
|
-
humidity: null,
|
|
178
|
-
light: null,
|
|
179
|
-
temperature: null
|
|
180
|
-
};
|
|
181
|
-
this.plantInfo = null;
|
|
182
|
-
this.platform = null;
|
|
183
|
-
this.serialPort = null;
|
|
184
|
-
this.aiClient = null;
|
|
185
|
-
this.aiDetector = new AIDetector();
|
|
186
|
-
this.historicalData = [];
|
|
187
|
-
this.isMonitoring = false;
|
|
188
|
-
this.hibernationMode = false;
|
|
189
|
-
this.hasSensors = false;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async init() {
|
|
193
|
-
this.messages = loadMessages(this.language);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async setLanguage(language) {
|
|
197
|
-
this.language = language;
|
|
198
|
-
this.messages = loadMessages(this.language);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
welcome() {
|
|
202
|
-
console.log('\n' + chalk.bold(this.messages.general.welcome) + '\n');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async start() {
|
|
206
|
-
await this.init();
|
|
207
|
-
this.welcome();
|
|
208
|
-
await this.selectLanguage();
|
|
209
|
-
await this.selectPlatform();
|
|
210
|
-
await this.selectAIMethod();
|
|
211
|
-
console.log(); // Add a space after AI connection
|
|
212
|
-
await this.selectPlantType();
|
|
213
|
-
await this.setPlantName();
|
|
214
|
-
await this.generatePlantInfo();
|
|
215
|
-
await this.setupSensors();
|
|
216
|
-
this.setupAlerts();
|
|
217
|
-
this.startMonitoring();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async selectLanguage() {
|
|
221
|
-
const { language } = await inquirer.prompt({
|
|
222
|
-
type: 'list',
|
|
223
|
-
name: 'language',
|
|
224
|
-
message: 'Select language:',
|
|
225
|
-
choices: [
|
|
226
|
-
{ name: 'English', value: 'en' },
|
|
227
|
-
{ name: 'Español', value: 'es' },
|
|
228
|
-
{ name: 'Français', value: 'fr' },
|
|
229
|
-
{ name: 'Deutsch', value: 'de' },
|
|
230
|
-
{ name: 'Italiano', value: 'it' },
|
|
231
|
-
{ name: 'Português', value: 'pt' },
|
|
232
|
-
{ name: 'Nederlands', value: 'nl' },
|
|
233
|
-
{ name: 'Русский', value: 'ru' },
|
|
234
|
-
{ name: '中文', value: 'zh' },
|
|
235
|
-
{ name: '日本語', value: 'ja' }
|
|
236
|
-
]
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
await this.setLanguage(language);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async selectPlatform() {
|
|
243
|
-
const { platform } = await inquirer.prompt({
|
|
244
|
-
type: 'list',
|
|
245
|
-
name: 'platform',
|
|
246
|
-
message: this.messages.general.selectPlatform,
|
|
247
|
-
choices: [
|
|
248
|
-
{ name: 'Raspberry Pi', value: 'raspberry' },
|
|
249
|
-
{ name: 'Arduino', value: 'arduino' }
|
|
250
|
-
]
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
this.platform = platform;
|
|
254
|
-
await this.setupPlatform();
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async setupPlatform() {
|
|
258
|
-
if (this.platform === 'raspberry') {
|
|
259
|
-
console.log(chalk.bold('Setting up Raspberry Pi...'));
|
|
260
|
-
this.hasSensors = true;
|
|
261
|
-
} else if (this.platform === 'arduino') {
|
|
262
|
-
console.log(chalk.bold('Setting up Arduino...'));
|
|
263
|
-
this.serialPort = new SerialPort({ path: '/dev/ttyACM0', baudRate: 9600 });
|
|
264
|
-
const parser = this.serialPort.pipe(new ReadlineParser({ delimiter: '\r\n' }));
|
|
265
|
-
parser.on('data', this.handleArduinoData.bind(this));
|
|
266
|
-
console.log('Arduino setup complete. Make sure arduino_dht22.ino is uploaded to your Arduino.');
|
|
267
|
-
this.hasSensors = true;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
handleArduinoData(data) {
|
|
272
|
-
const [temperature, humidity, light] = data.split(',').map(Number);
|
|
273
|
-
this.sensors.temperature = temperature;
|
|
274
|
-
this.sensors.humidity = humidity;
|
|
275
|
-
this.sensors.light = light;
|
|
276
|
-
this.checkAlerts();
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async selectAIMethod() {
|
|
280
|
-
const { method } = await inquirer.prompt({
|
|
281
|
-
type: 'list',
|
|
282
|
-
name: 'method',
|
|
283
|
-
message: 'Select AI method:',
|
|
284
|
-
choices: [
|
|
285
|
-
{ name: 'Local (Ollama)', value: 'local' },
|
|
286
|
-
{ name: 'OpenAI API', value: 'openai' }
|
|
287
|
-
]
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
if (method === 'local') {
|
|
291
|
-
await this.selectLocalModel();
|
|
292
|
-
} else {
|
|
293
|
-
const { apiKey } = await inquirer.prompt({
|
|
294
|
-
type: 'input',
|
|
295
|
-
name: 'apiKey',
|
|
296
|
-
message: 'Enter your API key:'
|
|
297
|
-
});
|
|
298
|
-
this.aiClient = new AIClient(method, apiKey);
|
|
299
|
-
}
|
|
300
|
-
console.log(chalk.green('AI successfully connected! 🤖✨'));
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async selectLocalModel() {
|
|
304
|
-
const aiModels = await this.aiDetector.detectAI();
|
|
305
|
-
if (aiModels && aiModels.models.length > 0) {
|
|
306
|
-
const { model } = await inquirer.prompt({
|
|
307
|
-
type: 'list',
|
|
308
|
-
name: 'model',
|
|
309
|
-
message: 'Select a local model:',
|
|
310
|
-
choices: aiModels.models
|
|
311
|
-
});
|
|
312
|
-
this.aiClient = new AIClient('local', null, model);
|
|
313
|
-
} else {
|
|
314
|
-
console.log('No local AI models found.');
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async selectPlantType() {
|
|
319
|
-
const { type } = await inquirer.prompt({
|
|
320
|
-
type: 'list',
|
|
321
|
-
name: 'type',
|
|
322
|
-
message: this.messages.general.selectPlantType,
|
|
323
|
-
choices: [
|
|
324
|
-
{ name: this.messages.general.indoor, value: 'indoor' },
|
|
325
|
-
{ name: this.messages.general.outdoor, value: 'outdoor' }
|
|
326
|
-
]
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
this.plantType = type;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async setPlantName() {
|
|
333
|
-
const { name } = await inquirer.prompt({
|
|
334
|
-
type: 'input',
|
|
335
|
-
name: 'name',
|
|
336
|
-
message: this.messages.general.enterPlantName
|
|
337
|
-
});
|
|
338
|
-
this.plantName = name;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
async generatePlantInfo() {
|
|
342
|
-
const plantDates = new PlantDates(this.plantName, this.plantType, this.aiClient, this.language);
|
|
343
|
-
this.plantInfo = await plantDates.generateInfo();
|
|
344
|
-
if (this.plantInfo) {
|
|
345
|
-
this.idealRanges = plantDates.formatPlantInfo();
|
|
346
|
-
} else {
|
|
347
|
-
console.log('No se pudo generar la información de la planta.');
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async setupSensors() {
|
|
352
|
-
console.log(chalk.bold(this.messages.general.settingUpSensors));
|
|
353
|
-
if (this.hasSensors) {
|
|
354
|
-
if (this.platform === 'raspberry') {
|
|
355
|
-
// For Raspberry Pi, we'll wait for actual sensor data
|
|
356
|
-
console.log('Waiting for sensor data from Raspberry Pi...');
|
|
357
|
-
}
|
|
358
|
-
// For Arduino, we're already set up in setupPlatform
|
|
359
|
-
} else {
|
|
360
|
-
console.log(chalk.yellow('No sensors detected. Running in simulation mode.'));
|
|
361
|
-
}
|
|
362
|
-
console.log(chalk.bold(this.messages.general.sensorsReady));
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
setupAlerts() {
|
|
366
|
-
if (this.plantInfo) {
|
|
367
|
-
this.alerts = {
|
|
368
|
-
humidity: { min: this.plantInfo.humidity.min, max: this.plantInfo.humidity.max },
|
|
369
|
-
light: { min: this.plantInfo.lighting.min, max: this.plantInfo.lighting.max },
|
|
370
|
-
temperature: { min: this.plantInfo.temperature.min, max: this.plantInfo.temperature.max }
|
|
371
|
-
};
|
|
372
|
-
console.log(this.messages.general.alertsSet);
|
|
373
|
-
} else {
|
|
374
|
-
console.log('No se pudieron configurar las alertas debido a la falta de información de la planta.');
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
calculatePercentage(value, min, max) {
|
|
379
|
-
if (value === null || value === undefined || isNaN(value)) {
|
|
380
|
-
return 0;
|
|
381
|
-
}
|
|
382
|
-
return Math.min(Math.max(((value - min) / (max - min)) * 100, 0), 100);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
getHumidityEmoji(percentage) {
|
|
386
|
-
if (percentage <= 0) return "🍂";
|
|
387
|
-
if (percentage <= 30) return "🍂";
|
|
388
|
-
if (percentage <= 60) return "🌿";
|
|
389
|
-
if (percentage <= 80) return "💧";
|
|
390
|
-
return "🌊";
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
getLightEmoji(percentage) {
|
|
394
|
-
if (percentage <= 0) return "🌑";
|
|
395
|
-
if (percentage <= 30) return "🌑";
|
|
396
|
-
if (percentage <= 60) return "🌥";
|
|
397
|
-
return "🌞";
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
getTemperatureEmoji(percentage) {
|
|
401
|
-
if (percentage <= 0) return "🧊";
|
|
402
|
-
if (percentage <= 30) return "🧊";
|
|
403
|
-
if (percentage <= 80) return "🌡️";
|
|
404
|
-
return "🔥";
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
getHappinessEmoji(averagePercentage) {
|
|
408
|
-
if (averagePercentage <= 0) return '😵';
|
|
409
|
-
if (averagePercentage >= 90) return '🤩';
|
|
410
|
-
if (averagePercentage >= 75) return '😊';
|
|
411
|
-
if (averagePercentage >= 60) return '😐';
|
|
412
|
-
if (averagePercentage >= 45) return '😞';
|
|
413
|
-
if (averagePercentage >= 30) return '😖';
|
|
414
|
-
return '🥵';
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
logPlantStatus() {
|
|
418
|
-
const allSensorsZero = Object.values(this.sensors).every(value => value === 0 || value === null || value === undefined);
|
|
419
|
-
|
|
420
|
-
if (!this.hasSensors || allSensorsZero) {
|
|
421
|
-
console.log(chalk.yellow('🔔 Warning: No data input or sensors are disconnected.'));
|
|
422
|
-
console.log('😵 | Lighting: 🌑 (0%) | Temperature: 🧊 (0.0°C) | Humidity: 🍂 (0%)');
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const humidityPercentage = this.calculatePercentage(this.sensors.humidity, this.alerts.humidity.min, this.alerts.humidity.max);
|
|
427
|
-
const lightPercentage = this.calculatePercentage(this.sensors.light, this.alerts.light.min, this.alerts.light.max);
|
|
428
|
-
const temperaturePercentage = this.calculatePercentage(this.sensors.temperature, this.alerts.temperature.min, this.alerts.temperature.max);
|
|
429
|
-
|
|
430
|
-
const averagePercentage = (humidityPercentage + lightPercentage + temperaturePercentage) / 3;
|
|
431
|
-
|
|
432
|
-
const happinessEmoji = this.getHappinessEmoji(averagePercentage);
|
|
433
|
-
const humidityEmoji = this.getHumidityEmoji(humidityPercentage);
|
|
434
|
-
const lightEmoji = this.getLightEmoji(lightPercentage);
|
|
435
|
-
const temperatureEmoji = this.getTemperatureEmoji(temperaturePercentage);
|
|
436
|
-
|
|
437
|
-
const status = `${happinessEmoji} | Lighting: ${lightEmoji} (${lightPercentage.toFixed(0)}%) | Temperature: ${temperatureEmoji} (${this.sensors.temperature?.toFixed(1) || 0.0}°C) | Humidity: ${humidityEmoji} (${humidityPercentage.toFixed(0)}%)`;
|
|
438
|
-
console.log(status);
|
|
439
|
-
|
|
440
|
-
this.historicalData.push({
|
|
441
|
-
timestamp: new Date(),
|
|
442
|
-
humidity: this.sensors.humidity,
|
|
443
|
-
light: this.sensors.light,
|
|
444
|
-
temperature: this.sensors.temperature
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
this.predictCriticalState();
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
predictCriticalState() {
|
|
451
|
-
if (this.historicalData.length < 10) return; // Need more data for prediction
|
|
452
|
-
|
|
453
|
-
const recentData = this.historicalData.slice(-10);
|
|
454
|
-
const trends = {
|
|
455
|
-
humidity: this.calculateTrend(recentData.map(d => d.humidity)),
|
|
456
|
-
light: this.calculateTrend(recentData.map(d => d.light)),
|
|
457
|
-
temperature: this.calculateTrend(recentData.map(d => d.temperature))
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
for (const [sensor, trend] of Object.entries(trends)) {
|
|
461
|
-
if (Math.abs(trend) > 0.5) { // Significant trend detected
|
|
462
|
-
const direction = trend > 0 ? 'increasing' : 'decreasing';
|
|
463
|
-
console.log(chalk.yellow.bold(`🔔 Warning: ${sensor} is ${direction} rapidly. Consider taking action.`));
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
calculateTrend(data) {
|
|
469
|
-
const n = data.length;
|
|
470
|
-
const sum_x = n * (n + 1) / 2;
|
|
471
|
-
const sum_y = data.reduce((a, b) => a + b, 0);
|
|
472
|
-
const sum_xy = data.reduce((sum, y, i) => sum + y * (i + 1), 0);
|
|
473
|
-
const sum_xx = n * (n + 1) * (2 * n + 1) / 6;
|
|
474
|
-
|
|
475
|
-
const slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x);
|
|
476
|
-
return slope;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
startMonitoring() {
|
|
480
|
-
console.log(this.messages.general.startMonitoring);
|
|
481
|
-
this.isMonitoring = true;
|
|
482
|
-
this.monitoringInterval = setInterval(() => {
|
|
483
|
-
if (!this.hibernationMode) {
|
|
484
|
-
this.logPlantStatus();
|
|
485
|
-
}
|
|
486
|
-
}, 60000); // Log every minute
|
|
487
|
-
|
|
488
|
-
// Enable keypress detection
|
|
489
|
-
process.stdin.setRawMode(true);
|
|
490
|
-
process.stdin.resume();
|
|
491
|
-
process.stdin.setEncoding('utf8');
|
|
492
|
-
process.stdin.on('data', (key) => {
|
|
493
|
-
if (key === '\u0003') { // Ctrl+C
|
|
494
|
-
process.exit();
|
|
495
|
-
} else if (key === '\u000F') { // Ctrl+O
|
|
496
|
-
this.pauseMonitoring();
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
pauseMonitoring() {
|
|
502
|
-
clearInterval(this.monitoringInterval);
|
|
503
|
-
this.isMonitoring = false;
|
|
504
|
-
this.displaySensorSettingsMenu();
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
async displaySensorSettingsMenu() {
|
|
508
|
-
const choices = [
|
|
509
|
-
'Ajustar configuración de sensores',
|
|
510
|
-
'Activar/Desactivar modo de hibernación',
|
|
511
|
-
'Volver a iniciar monitoreo',
|
|
512
|
-
'Salir'
|
|
513
|
-
];
|
|
514
|
-
|
|
515
|
-
const { option } = await inquirer.prompt({
|
|
516
|
-
type: 'list',
|
|
517
|
-
name: 'option',
|
|
518
|
-
message: 'Monitoreo detenido. ¿Qué deseas hacer?',
|
|
519
|
-
choices: choices,
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
switch (option) {
|
|
523
|
-
case choices[0]:
|
|
524
|
-
await this.adjustSensorSettings();
|
|
525
|
-
break;
|
|
526
|
-
case choices[1]:
|
|
527
|
-
await this.toggleHibernation();
|
|
528
|
-
break;
|
|
529
|
-
case choices[2]:
|
|
530
|
-
this.startMonitoring();
|
|
531
|
-
break;
|
|
532
|
-
case choices[3]:
|
|
533
|
-
console.log('Saliendo del menú de ajustes.');
|
|
534
|
-
process.exit();
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
async adjustSensorSettings() {
|
|
539
|
-
const sensorSettings = await inquirer.prompt([
|
|
540
|
-
{
|
|
541
|
-
type: 'input',
|
|
542
|
-
name: 'humidity',
|
|
543
|
-
message: `Humedad ideal (actual: ${this.plantInfo.humidity.min}-${this.plantInfo.humidity.max}%):`,
|
|
544
|
-
default: `${this.plantInfo.humidity.min}-${this.plantInfo.humidity.max}`,
|
|
545
|
-
},
|
|
546
|
-
{
|
|
547
|
-
type: 'input',
|
|
548
|
-
name: 'temperature',
|
|
549
|
-
message: `Temperatura ideal (actual: ${this.plantInfo.temperature.min}-${this.plantInfo.temperature.max}°C):`,
|
|
550
|
-
default: `${this.plantInfo.temperature.min}-${this.plantInfo.temperature.max}`,
|
|
551
|
-
},
|
|
552
|
-
{
|
|
553
|
-
type: 'input',
|
|
554
|
-
name: 'light',
|
|
555
|
-
message: `Luz ideal (actual: ${this.plantInfo.lighting.min}-${this.plantInfo.lighting.max} lux):`,
|
|
556
|
-
default: `${this.plantInfo.lighting.min}-${this.plantInfo.lighting.max}`,
|
|
557
|
-
},
|
|
558
|
-
]);
|
|
559
|
-
|
|
560
|
-
this.plantInfo.humidity = this.parseRange(sensorSettings.humidity);
|
|
561
|
-
this.plantInfo.temperature = this.parseRange(sensorSettings.temperature);
|
|
562
|
-
this.plantInfo.lighting = this.parseRange(sensorSettings.light);
|
|
563
|
-
|
|
564
|
-
this.setupAlerts();
|
|
565
|
-
|
|
566
|
-
console.log(`Nuevos valores ajustados:
|
|
567
|
-
Humedad: ${this.plantInfo.humidity.min}-${this.plantInfo.humidity.max}%
|
|
568
|
-
Temperatura: ${this.plantInfo.temperature.min}-${this.plantInfo.temperature.max}°C
|
|
569
|
-
Luz: ${this.plantInfo.lighting.min}-${this.plantInfo.lighting.max} lux`);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
parseRange(rangeString) {
|
|
573
|
-
const [min, max] = rangeString.split('-').map(Number);
|
|
574
|
-
return { min, max };
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
async toggleHibernation() {
|
|
578
|
-
const { hibernation } = await inquirer.prompt({
|
|
579
|
-
type: 'confirm',
|
|
580
|
-
name: 'hibernation',
|
|
581
|
-
message: '¿Activar modo de hibernación?',
|
|
582
|
-
default: false,
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
this.hibernationMode = hibernation;
|
|
586
|
-
console.log(`Modo de hibernación ${this.hibernationMode ? 'activado' : 'desactivado'}.`);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
checkAlerts() {
|
|
590
|
-
if (!this.hasSensors) return;
|
|
591
|
-
|
|
592
|
-
for (const [sensor, value] of Object.entries(this.sensors)) {
|
|
593
|
-
if (value === null || value === undefined || isNaN(value)) continue;
|
|
594
|
-
if (value < this.alerts[sensor].min) {
|
|
595
|
-
console.log(chalk.red(this.messages.alerts[sensor].low.replace('{value}', value.toFixed(2))));
|
|
596
|
-
} else if (value > this.alerts[sensor].max) {
|
|
597
|
-
console.log(chalk.red(this.messages.alerts[sensor].high.replace('{value}', value.toFixed(2))));
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const smartPlant = new SmartPlant();
|
|
604
|
-
smartPlant.start().catch(console.error);
|
|
605
|
-
|
|
606
|
-
export default SmartPlant;
|