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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'general' : {
|
|
3
|
+
'welcome' : 'Bem-vindo ao SmartPlant!',
|
|
4
|
+
'selectInputMethod' : 'Selecione o método de entrada:',
|
|
5
|
+
'local' : 'Local',
|
|
6
|
+
'external' : 'Externo',
|
|
7
|
+
'localSelected' : 'Método local selecionado.',
|
|
8
|
+
'externSelected' : 'Método externo selecionado.',
|
|
9
|
+
'selectAIModel' : 'Selecione um modelo de IA local:',
|
|
10
|
+
'selectedAIModel' : 'Modelo de IA selecionado: {model}',
|
|
11
|
+
'noLocalAI' : 'Nenhum modelo de IA local detectado.',
|
|
12
|
+
'enterAPIKey' : 'Digite sua chave API:',
|
|
13
|
+
'invalidMethod' : 'Método selecionado inválido.',
|
|
14
|
+
'selectPlantType' : 'Você tem uma planta de interior ou de exterior?',
|
|
15
|
+
'indoor' : 'Interior',
|
|
16
|
+
'outdoor' : 'Exterior',
|
|
17
|
+
'invalidType' : 'Tipo de planta selecionado inválido.',
|
|
18
|
+
'enterPlantName' : 'Digite o nome da planta:',
|
|
19
|
+
'plantNameSet' : 'Nome da planta definido como: {name}',
|
|
20
|
+
'settingUpSensors' : 'Configurando sensores...',
|
|
21
|
+
'sensorsReady' : 'Sensores prontos!',
|
|
22
|
+
'alertsConfigured' : 'Alertas configurados.',
|
|
23
|
+
'alertsActivated' : 'Alertas ativados.',
|
|
24
|
+
'startingMonitoring' : 'Iniciando o monitoramento da planta...',
|
|
25
|
+
'checkingStatus' : 'Verificando o status da planta...',
|
|
26
|
+
},
|
|
27
|
+
'alerts' : {
|
|
28
|
+
'moisture' : {
|
|
29
|
+
'low' : 'Aviso: Umidade do solo baixa ({value}%).',
|
|
30
|
+
'high' : 'Aviso: Umidade do solo alta ({value}%).',
|
|
31
|
+
},
|
|
32
|
+
'light' : {
|
|
33
|
+
'low' : 'Aviso: Nível de luz baixo ({value} lux).',
|
|
34
|
+
'high' : 'Aviso: Nível de luz alto ({value} lux).',
|
|
35
|
+
},
|
|
36
|
+
'temperature' : {
|
|
37
|
+
'low' : 'Aviso: Temperatura baixa ({value}°C).',
|
|
38
|
+
'high' : 'Aviso: Temperatura alta ({value}°C).',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'general' : {
|
|
3
|
+
'welcome' : 'Добро пожаловать в SmartPlant!',
|
|
4
|
+
'selectInputMethod' : 'Выберите метод ввода:',
|
|
5
|
+
'local' : 'Локально',
|
|
6
|
+
'external' : 'Внешне',
|
|
7
|
+
'localSelected' : 'Выбран локальный метод.',
|
|
8
|
+
'externSelected' : 'Выбран внешний метод.',
|
|
9
|
+
'selectAIModel' : 'Выберите локальную модель ИИ:',
|
|
10
|
+
'selectedAIModel' : 'Выбранная модель ИИ: {model}',
|
|
11
|
+
'noLocalAI' : 'Локальные модели ИИ не обнаружены.',
|
|
12
|
+
'enterAPIKey' : 'Введите ваш API-ключ:',
|
|
13
|
+
'invalidMethod' : 'Выбранный метод недействителен.',
|
|
14
|
+
'selectPlantType' : 'У вас есть растение для помещений или на улице?',
|
|
15
|
+
'indoor' : 'Для помещений',
|
|
16
|
+
'outdoor' : 'На улице',
|
|
17
|
+
'invalidType' : 'Выбранный тип растения недействителен.',
|
|
18
|
+
'enterPlantName' : 'Введите имя растения:',
|
|
19
|
+
'plantNameSet' : 'Имя растения установлено на: {name}',
|
|
20
|
+
'settingUpSensors' : 'Настройка датчиков...',
|
|
21
|
+
'sensorsReady' : 'Датчики готовы!',
|
|
22
|
+
'alertsConfigured' : 'Настроены предупреждения.',
|
|
23
|
+
'alertsActivated' : 'Предупреждения активированы.',
|
|
24
|
+
'startingMonitoring' : 'Запуск мониторинга растений...',
|
|
25
|
+
'checkingStatus' : 'Проверка состояния растения...',
|
|
26
|
+
},
|
|
27
|
+
'alerts' : {
|
|
28
|
+
'moisture' : {
|
|
29
|
+
'low' : 'Предупреждение: Влажность почвы низкая ({value}%).',
|
|
30
|
+
'high' : 'Предупреждение: Влажность почвы высокая ({value}%).',
|
|
31
|
+
},
|
|
32
|
+
'light' : {
|
|
33
|
+
'low' : 'Предупреждение: Уровень света низкий ({value} lux).',
|
|
34
|
+
'high' : 'Предупреждение: Уровень света высокий ({value} lux).',
|
|
35
|
+
},
|
|
36
|
+
'temperature' : {
|
|
37
|
+
'low' : 'Предупреждение: Температура низкая ({value}°C).',
|
|
38
|
+
'high' : 'Предупреждение: Температура высокая ({value}°C).',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'general' : {
|
|
3
|
+
'welcome' : '欢迎使用 SmartPlant!',
|
|
4
|
+
'selectInputMethod' : '选择输入方法:',
|
|
5
|
+
'local' : '本地',
|
|
6
|
+
'external' : '外部',
|
|
7
|
+
'localSelected' : '已选择本地方法。',
|
|
8
|
+
'externSelected' : '已选择外部方法。',
|
|
9
|
+
'selectAIModel' : '选择本地 AI 模型:',
|
|
10
|
+
'selectedAIModel' : '选择的 AI 模型:{model}',
|
|
11
|
+
'noLocalAI' : '未检测到本地 AI 模型。',
|
|
12
|
+
'enterAPIKey' : '输入您的 API 密钥:',
|
|
13
|
+
'invalidMethod' : '选择的方法无效。',
|
|
14
|
+
'selectPlantType' : '您的植物是室内植物还是户外植物?',
|
|
15
|
+
'indoor' : '室内',
|
|
16
|
+
'outdoor' : '户外',
|
|
17
|
+
'invalidType' : '选择的植物类型无效。',
|
|
18
|
+
'enterPlantName' : '输入植物的名称:',
|
|
19
|
+
'plantNameSet' : '植物名称设置为:{name}',
|
|
20
|
+
'settingUpSensors' : '设置传感器中...',
|
|
21
|
+
'sensorsReady' : '传感器已准备好!',
|
|
22
|
+
'alertsConfigured' : '警报已配置。',
|
|
23
|
+
'alertsActivated' : '警报已激活。',
|
|
24
|
+
'startingMonitoring' : '开始植物监控...',
|
|
25
|
+
'checkingStatus' : '检查植物状态...',
|
|
26
|
+
},
|
|
27
|
+
'alerts' : {
|
|
28
|
+
'moisture' : {
|
|
29
|
+
'low' : '警告:土壤湿度低 ({value}%)。',
|
|
30
|
+
'high' : '警告:土壤湿度高 ({value}%)。',
|
|
31
|
+
},
|
|
32
|
+
'light' : {
|
|
33
|
+
'low' : '警告:光线水平低 ({value} lux)。',
|
|
34
|
+
'high' : '警告:光线水平高 ({value} lux)。',
|
|
35
|
+
},
|
|
36
|
+
'temperature' : {
|
|
37
|
+
'low' : '警告:温度低 ({value}°C)。',
|
|
38
|
+
'high' : '警告:温度高 ({value}°C)。',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
package/src/main.js
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { ReadlineParser } from '@serialport/parser-readline';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import enquirer from 'enquirer';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { SerialPort } from 'serialport';
|
|
6
|
+
|
|
7
|
+
import MsgDe from './language/messages-de.js';
|
|
8
|
+
import MsgEn from './language/messages-en.js';
|
|
9
|
+
import MsgEs from './language/messages-es.js';
|
|
10
|
+
import MsgFr from './language/messages-fr.js';
|
|
11
|
+
import MsgIt from './language/messages-it.js';
|
|
12
|
+
import MsgJa from './language/messages-ja.js';
|
|
13
|
+
import MsgPt from './language/messages-pt.js';
|
|
14
|
+
import MsgRu from './language/messages-ru.js';
|
|
15
|
+
import MsgZh from './language/messages-zh.js';
|
|
16
|
+
|
|
17
|
+
const messagesMap = { de: MsgDe, en: MsgEn, es: MsgEs, fr: MsgFr, ja: MsgJa, it: MsgIt, pt: MsgPt, ru: MsgRu, zh: MsgZh };
|
|
18
|
+
|
|
19
|
+
function loadMessages(lang) {
|
|
20
|
+
return messagesMap[lang] || MsgEn;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class AIService {
|
|
24
|
+
constructor(provider, apiKey = null, localModel = null) {
|
|
25
|
+
this.provider = provider;
|
|
26
|
+
this.apiKey = apiKey;
|
|
27
|
+
this.localModel = localModel;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async generateResponse(prompt, language) {
|
|
31
|
+
const fullPrompt = `Responde en ${language}. ${prompt}`;
|
|
32
|
+
try {
|
|
33
|
+
switch (this.provider) {
|
|
34
|
+
case 'openai': return this._openAICompatible(fullPrompt, 'https://api.openai.com/v1/chat/completions', 'gpt-4o-mini');
|
|
35
|
+
case 'grok': return this._openAICompatible(fullPrompt, 'https://api.x.ai/v1/chat/completions', 'grok-beta');
|
|
36
|
+
case 'claude': return this._claude(fullPrompt);
|
|
37
|
+
case 'gemini': return this._gemini(fullPrompt);
|
|
38
|
+
case 'local': return this._ollama(fullPrompt);
|
|
39
|
+
default: throw new Error('Proveedor no soportado');
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(chalk.red(`Error en ${this.provider}:`), err.message);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async _openAICompatible(prompt, url, model) {
|
|
48
|
+
const res = await fetch(url, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }], max_tokens: 600, temperature: 0.7 }),
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
54
|
+
const data = await res.json();
|
|
55
|
+
return data.choices[0].message.content.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async _claude(prompt) {
|
|
59
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'x-api-key': this.apiKey, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ model: 'claude-3-5-sonnet-20241022', max_tokens: 600, messages: [{ role: 'user', content: prompt }] }),
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
return data.content[0].text.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async _gemini(prompt) {
|
|
70
|
+
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${this.apiKey}`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }),
|
|
74
|
+
});
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
if (res.status === 429) throw new Error('429 - Límite de Gemini alcanzado. Espera 60 segundos.');
|
|
77
|
+
if (res.status === 404) throw new Error('404 - Modelo Gemini no encontrado. Usa OpenAI o Grok.');
|
|
78
|
+
throw new Error(`HTTP ${res.status}`);
|
|
79
|
+
}
|
|
80
|
+
const data = await res.json();
|
|
81
|
+
return data.candidates[0].content.parts[0].text.trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async _ollama(prompt) {
|
|
85
|
+
const cmd = `ollama run ${this.localModel} "${prompt.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`;
|
|
86
|
+
const out = execSync(cmd, { encoding: 'utf-8' });
|
|
87
|
+
return out.trim();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class SmartPlant {
|
|
92
|
+
constructor() {
|
|
93
|
+
this.language = 'en';
|
|
94
|
+
this.messages = null;
|
|
95
|
+
this.platform = null;
|
|
96
|
+
this.aiClient = null;
|
|
97
|
+
this.plantName = '';
|
|
98
|
+
this.plantType = null;
|
|
99
|
+
this.plantInfo = null;
|
|
100
|
+
this.alerts = {};
|
|
101
|
+
this.sensors = { humidity: null, light: null, temperature: null };
|
|
102
|
+
this.historicalData = [];
|
|
103
|
+
this.isMonitoring = false;
|
|
104
|
+
this.hibernationMode = false;
|
|
105
|
+
this.hasSensors = false;
|
|
106
|
+
this.serialPort = null;
|
|
107
|
+
this.monitoringInterval = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async init() { this.messages = loadMessages(this.language); }
|
|
111
|
+
|
|
112
|
+
async setLanguage(lang) {
|
|
113
|
+
this.language = lang;
|
|
114
|
+
this.messages = loadMessages(lang);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
welcome() {
|
|
118
|
+
console.log('\n' + chalk.bold(this.messages.general.welcome) + '\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async start() {
|
|
122
|
+
await this.init();
|
|
123
|
+
this.welcome();
|
|
124
|
+
await this.selectLanguage();
|
|
125
|
+
await this.selectPlatform();
|
|
126
|
+
await this.selectAIMethod();
|
|
127
|
+
console.log();
|
|
128
|
+
await this.selectPlantType();
|
|
129
|
+
await this.setPlantName();
|
|
130
|
+
await this.generatePlantInfo();
|
|
131
|
+
await this.setupSensors();
|
|
132
|
+
this.setupAlerts();
|
|
133
|
+
this.startMonitoring();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async selectLanguage() {
|
|
137
|
+
const { language } = await enquirer.prompt({
|
|
138
|
+
type: 'autocomplete',
|
|
139
|
+
name: 'language',
|
|
140
|
+
message: 'Select language:',
|
|
141
|
+
choices: [
|
|
142
|
+
{ name: 'English', value: 'en' }, { name: 'Español', value: 'es' },
|
|
143
|
+
{ name: 'Français', value: 'fr' }, { name: 'Deutsch', value: 'de' },
|
|
144
|
+
{ name: 'Italiano', value: 'it' }, { name: 'Português', value: 'pt' },
|
|
145
|
+
{ name: 'Nederlands', value: 'nl' }, { name: 'Русский', value: 'ru' },
|
|
146
|
+
{ name: '中文', value: 'zh' }, { name: '日本語', value: 'ja' },
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
await this.setLanguage(language);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async selectPlatform() {
|
|
153
|
+
const { platform } = await enquirer.prompt({
|
|
154
|
+
type: 'select',
|
|
155
|
+
name: 'platform',
|
|
156
|
+
message: this.messages.general.selectPlatform,
|
|
157
|
+
choices: [
|
|
158
|
+
{ name: 'Raspberry Pi', value: 'raspberry' },
|
|
159
|
+
{ name: 'Arduino', value: 'arduino' },
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
this.platform = platform;
|
|
163
|
+
await this.setupPlatform();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async setupPlatform() {
|
|
167
|
+
if (this.platform === 'raspberry') {
|
|
168
|
+
console.log(chalk.bold('Setting up Raspberry Pi...'));
|
|
169
|
+
this.hasSensors = true;
|
|
170
|
+
} else if (this.platform === 'arduino') {
|
|
171
|
+
console.log(chalk.bold('Setting up Arduino...'));
|
|
172
|
+
this.serialPort = new SerialPort({ path: '/dev/ttyACM0', baudRate: 9600 });
|
|
173
|
+
this.serialPort.pipe(new ReadlineParser({ delimiter: '\r\n' })).on('data', data => this.handleArduinoData(data));
|
|
174
|
+
console.log('Arduino setup complete. Upload arduino_dht22.ino first.');
|
|
175
|
+
this.hasSensors = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
handleArduinoData(data) {
|
|
180
|
+
const [temperature, humidity, light] = data.split(',').map(Number);
|
|
181
|
+
this.sensors.temperature = temperature;
|
|
182
|
+
this.sensors.humidity = humidity;
|
|
183
|
+
this.sensors.light = light;
|
|
184
|
+
this.checkAlerts();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async selectAIMethod() {
|
|
188
|
+
const { provider } = await enquirer.prompt({
|
|
189
|
+
type: 'select',
|
|
190
|
+
name: 'provider',
|
|
191
|
+
message: 'Elige proveedor de IA:',
|
|
192
|
+
choices: [
|
|
193
|
+
{ message: 'OpenAI (GPT-4o-mini)', value: 'openai' },
|
|
194
|
+
{ message: 'Grok (xAI)', value: 'grok' },
|
|
195
|
+
{ message: 'Claude (Anthropic)', value: 'claude' },
|
|
196
|
+
{ message: 'Gemini (Google)', value: 'gemini' },
|
|
197
|
+
{ message: 'Local (Ollama)', value: 'local' },
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
let apiKey = null;
|
|
202
|
+
let localModel = null;
|
|
203
|
+
|
|
204
|
+
if (provider !== 'local') {
|
|
205
|
+
const { key } = await enquirer.prompt({
|
|
206
|
+
type: 'password',
|
|
207
|
+
name: 'key',
|
|
208
|
+
message: `Introduce tu API key de ${provider.toUpperCase()}:`,
|
|
209
|
+
});
|
|
210
|
+
apiKey = key;
|
|
211
|
+
} else {
|
|
212
|
+
localModel = await this.selectLocalModel();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.aiClient = new AIService(provider, apiKey, localModel);
|
|
216
|
+
console.log(chalk.green('✅ IA conectada correctamente! 🤖✨'));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async selectLocalModel() {
|
|
220
|
+
try {
|
|
221
|
+
const output = execSync('ollama list', { encoding: 'utf-8' });
|
|
222
|
+
const models = output.split('\n').filter(l => l.trim() && !l.startsWith('NAME')).map(l => l.split(' ')[0]);
|
|
223
|
+
if (models.length === 0) throw new Error();
|
|
224
|
+
const { model } = await enquirer.prompt({
|
|
225
|
+
type: 'select',
|
|
226
|
+
name: 'model',
|
|
227
|
+
message: 'Selecciona modelo local:',
|
|
228
|
+
choices: models,
|
|
229
|
+
});
|
|
230
|
+
return model;
|
|
231
|
+
} catch {
|
|
232
|
+
console.log(chalk.yellow('No se encontraron modelos Ollama.'));
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async selectPlantType() {
|
|
238
|
+
const { type } = await enquirer.prompt({
|
|
239
|
+
type: 'select',
|
|
240
|
+
name: 'type',
|
|
241
|
+
message: this.messages.general.selectPlantType,
|
|
242
|
+
choices: [
|
|
243
|
+
{ name: this.messages.general.indoor, value: 'indoor' },
|
|
244
|
+
{ name: this.messages.general.outdoor, value: 'outdoor' },
|
|
245
|
+
],
|
|
246
|
+
});
|
|
247
|
+
this.plantType = type;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async setPlantName() {
|
|
251
|
+
const { name } = await enquirer.prompt({
|
|
252
|
+
type: 'input',
|
|
253
|
+
name: 'name',
|
|
254
|
+
message: this.messages.general.enterPlantName,
|
|
255
|
+
});
|
|
256
|
+
this.plantName = name;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async generatePlantInfo() {
|
|
260
|
+
console.log(chalk.bold('🔍🌿 Generando información de la planta...'));
|
|
261
|
+
const prompt = `Provide a comprehensive summary for ${this.plantName} (${this.plantType}) including: Lighting, Watering, Temperature, Humidity, Soil, Fertilization, Pruning, and Propagation. Also give ranges in format: "Lighting: X-Y lux, Temperature: A-B°C, Humidity: C-D%".`;
|
|
262
|
+
const response = await this.aiClient.generateResponse(prompt, this.language);
|
|
263
|
+
if (!response) {
|
|
264
|
+
console.log(chalk.red('❌ No se pudo generar la información de la planta.'));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const lightingMatch = response.match(/Lighting:\s*(\d+)-(\d+)\s*lux/i);
|
|
269
|
+
const tempMatch = response.match(/Temperature:\s*(\d+)-(\d+)\s*°C/i);
|
|
270
|
+
const humMatch = response.match(/Humidity:\s*(\d+)-(\d+)%/i);
|
|
271
|
+
|
|
272
|
+
this.plantInfo = {
|
|
273
|
+
summary: response,
|
|
274
|
+
lighting: lightingMatch ? { min: +lightingMatch[1], max: +lightingMatch[2] } : { min: 50, max: 700 },
|
|
275
|
+
temperature: tempMatch ? { min: +tempMatch[1], max: +tempMatch[2] } : { min: 18, max: 24 },
|
|
276
|
+
humidity: humMatch ? { min: +humMatch[1], max: +humMatch[2] } : { min: 40, max: 60 },
|
|
277
|
+
};
|
|
278
|
+
console.log(chalk.green('✅ Información de planta generada!'));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async setupSensors() {
|
|
282
|
+
console.log(chalk.bold(this.messages.general.settingUpSensors));
|
|
283
|
+
if (!this.hasSensors) console.log(chalk.yellow('Modo simulación activado.'));
|
|
284
|
+
console.log(chalk.bold(this.messages.general.sensorsReady));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
setupAlerts() {
|
|
288
|
+
if (!this.plantInfo) return;
|
|
289
|
+
this.alerts = {
|
|
290
|
+
humidity: { min: this.plantInfo.humidity.min, max: this.plantInfo.humidity.max },
|
|
291
|
+
light: { min: this.plantInfo.lighting.min, max: this.plantInfo.lighting.max },
|
|
292
|
+
temperature: { min: this.plantInfo.temperature.min, max: this.plantInfo.temperature.max },
|
|
293
|
+
};
|
|
294
|
+
if (this.messages.general.alertsSet) console.log(this.messages.general.alertsSet);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
calculatePercentage(v, min, max) {
|
|
298
|
+
if (!v || isNaN(v)) return 0;
|
|
299
|
+
return Math.min(Math.max(((v - min) / (max - min)) * 100, 0), 100);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
getHumidityEmoji(p) { if (p <= 0) return '🍂'; if (p <= 30) return '🍂'; if (p <= 60) return '🌿'; if (p <= 80) return '💧'; return '🌊'; }
|
|
303
|
+
getLightEmoji(p) { if (p <= 0) return '🌑'; if (p <= 30) return '🌑'; if (p <= 60) return '🌥'; return '🌞'; }
|
|
304
|
+
getTemperatureEmoji(p) { if (p <= 0) return '🧊'; if (p <= 30) return '🧊'; if (p <= 80) return '🌡️'; return '🔥'; }
|
|
305
|
+
getHappinessEmoji(avg) {
|
|
306
|
+
if (avg <= 0) return '😵';
|
|
307
|
+
if (avg >= 90) return '🤩';
|
|
308
|
+
if (avg >= 75) return '😊';
|
|
309
|
+
if (avg >= 60) return '😐';
|
|
310
|
+
if (avg >= 45) return '😞';
|
|
311
|
+
if (avg >= 30) return '😖';
|
|
312
|
+
return '🥵';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
logPlantStatus() {
|
|
316
|
+
const allSensorsZero = Object.values(this.sensors).every(value => value === 0 || value === null || value === undefined);
|
|
317
|
+
if (!this.hasSensors || allSensorsZero) {
|
|
318
|
+
console.log(chalk.yellow('🔔 Warning: No data input or sensors are disconnected.'));
|
|
319
|
+
console.log('😵 | Lighting: 🌑 (0%) | Temperature: 🧊 (0.0°C) | Humidity: 🍂 (0%)');
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const humidityPercentage = this.calculatePercentage(this.sensors.humidity, this.alerts.humidity.min, this.alerts.humidity.max);
|
|
324
|
+
const lightPercentage = this.calculatePercentage(this.sensors.light, this.alerts.light.min, this.alerts.light.max);
|
|
325
|
+
const temperaturePercentage = this.calculatePercentage(this.sensors.temperature, this.alerts.temperature.min, this.alerts.temperature.max);
|
|
326
|
+
const averagePercentage = (humidityPercentage + lightPercentage + temperaturePercentage) / 3;
|
|
327
|
+
|
|
328
|
+
const happinessEmoji = this.getHappinessEmoji(averagePercentage);
|
|
329
|
+
const humidityEmoji = this.getHumidityEmoji(humidityPercentage);
|
|
330
|
+
const lightEmoji = this.getLightEmoji(lightPercentage);
|
|
331
|
+
const temperatureEmoji = this.getTemperatureEmoji(temperaturePercentage);
|
|
332
|
+
|
|
333
|
+
const status = `${happinessEmoji} | Lighting: ${lightEmoji} (${lightPercentage.toFixed(0)}%) | Temperature: ${temperatureEmoji} (${this.sensors.temperature?.toFixed(1) || 0.0}°C) | Humidity: ${humidityEmoji} (${humidityPercentage.toFixed(0)}%)`;
|
|
334
|
+
console.log(status);
|
|
335
|
+
|
|
336
|
+
this.historicalData.push({ timestamp: new Date(), humidity: this.sensors.humidity, light: this.sensors.light, temperature: this.sensors.temperature });
|
|
337
|
+
this.predictCriticalState();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
predictCriticalState() {
|
|
341
|
+
if (this.historicalData.length < 10) return;
|
|
342
|
+
const recentData = this.historicalData.slice(-10);
|
|
343
|
+
const trends = {
|
|
344
|
+
humidity: this.calculateTrend(recentData.map(d => d.humidity)),
|
|
345
|
+
light: this.calculateTrend(recentData.map(d => d.light)),
|
|
346
|
+
temperature: this.calculateTrend(recentData.map(d => d.temperature)),
|
|
347
|
+
};
|
|
348
|
+
for (const [sensor, trend] of Object.entries(trends)) {
|
|
349
|
+
if (Math.abs(trend) > 0.5) {
|
|
350
|
+
const direction = trend > 0 ? 'increasing' : 'decreasing';
|
|
351
|
+
console.log(chalk.yellow.bold(`🔔 Warning: ${sensor} is ${direction} rapidly. Consider taking action.`));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
calculateTrend(data) {
|
|
357
|
+
const n = data.length;
|
|
358
|
+
const sum_x = n * (n + 1) / 2;
|
|
359
|
+
const sum_y = data.reduce((a, b) => a + b, 0);
|
|
360
|
+
const sum_xy = data.reduce((sum, y, i) => sum + y * (i + 1), 0);
|
|
361
|
+
const sum_xx = n * (n + 1) * (2 * n + 1) / 6;
|
|
362
|
+
return (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
startMonitoring() {
|
|
366
|
+
console.log(this.messages.general.startMonitoring || 'Monitoreo iniciado...');
|
|
367
|
+
this.isMonitoring = true;
|
|
368
|
+
this.monitoringInterval = setInterval(() => {
|
|
369
|
+
if (!this.hibernationMode) this.logPlantStatus();
|
|
370
|
+
}, 60000);
|
|
371
|
+
|
|
372
|
+
process.stdin.setRawMode(true);
|
|
373
|
+
process.stdin.resume();
|
|
374
|
+
process.stdin.setEncoding('utf8');
|
|
375
|
+
process.stdin.on('data', key => {
|
|
376
|
+
if (key === '\u0003') process.exit();
|
|
377
|
+
if (key === '\u000F') this.pauseMonitoring();
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
pauseMonitoring() {
|
|
382
|
+
clearInterval(this.monitoringInterval);
|
|
383
|
+
this.isMonitoring = false;
|
|
384
|
+
this.displaySensorSettingsMenu();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async displaySensorSettingsMenu() {
|
|
388
|
+
const choices = ['Ajustar configuración de sensores', 'Activar/Desactivar modo de hibernación', 'Volver a iniciar monitoreo', 'Salir'];
|
|
389
|
+
const { option } = await enquirer.prompt({ type: 'select', name: 'option', message: 'Monitoreo detenido. ¿Qué deseas hacer?', choices });
|
|
390
|
+
if (option === choices[0]) await this.adjustSensorSettings();
|
|
391
|
+
else if (option === choices[1]) await this.toggleHibernation();
|
|
392
|
+
else if (option === choices[2]) this.startMonitoring();
|
|
393
|
+
else if (option === choices[3]) process.exit();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async adjustSensorSettings() {
|
|
397
|
+
const sensorSettings = await enquirer.prompt([
|
|
398
|
+
{ type: 'input', name: 'humidity', message: `Humedad ideal (actual: ${this.plantInfo.humidity.min}-${this.plantInfo.humidity.max}%):`, default: `${this.plantInfo.humidity.min}-${this.plantInfo.humidity.max}` },
|
|
399
|
+
{ type: 'input', name: 'temperature', message: `Temperatura ideal (actual: ${this.plantInfo.temperature.min}-${this.plantInfo.temperature.max}°C):`, default: `${this.plantInfo.temperature.min}-${this.plantInfo.temperature.max}` },
|
|
400
|
+
{ type: 'input', name: 'light', message: `Luz ideal (actual: ${this.plantInfo.lighting.min}-${this.plantInfo.lighting.max} lux):`, default: `${this.plantInfo.lighting.min}-${this.plantInfo.lighting.max}` },
|
|
401
|
+
]);
|
|
402
|
+
this.plantInfo.humidity = this.parseRange(sensorSettings.humidity);
|
|
403
|
+
this.plantInfo.temperature = this.parseRange(sensorSettings.temperature);
|
|
404
|
+
this.plantInfo.lighting = this.parseRange(sensorSettings.light);
|
|
405
|
+
this.setupAlerts();
|
|
406
|
+
console.log(`Nuevos valores ajustados:\nHumedad: ${this.plantInfo.humidity.min}-${this.plantInfo.humidity.max}%\nTemperatura: ${this.plantInfo.temperature.min}-${this.plantInfo.temperature.max}°C\nLuz: ${this.plantInfo.lighting.min}-${this.plantInfo.lighting.max} lux`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
parseRange(rangeString) {
|
|
410
|
+
const [min, max] = rangeString.split('-').map(Number);
|
|
411
|
+
return { min, max };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async toggleHibernation() {
|
|
415
|
+
const { hibernation } = await enquirer.prompt({
|
|
416
|
+
type: 'confirm',
|
|
417
|
+
name: 'hibernation',
|
|
418
|
+
message: '¿Activar modo de hibernación?',
|
|
419
|
+
default: false,
|
|
420
|
+
});
|
|
421
|
+
this.hibernationMode = hibernation;
|
|
422
|
+
console.log(`Modo de hibernación ${this.hibernationMode ? 'activado' : 'desactivado'}.`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
checkAlerts() {
|
|
426
|
+
if (!this.hasSensors) return;
|
|
427
|
+
for (const [sensor, value] of Object.entries(this.sensors)) {
|
|
428
|
+
if (value === null || value === undefined || isNaN(value)) continue;
|
|
429
|
+
if (value < this.alerts[sensor].min) console.log(chalk.red(this.messages.alerts[sensor].low.replace('{value}', value.toFixed(2))));
|
|
430
|
+
else if (value > this.alerts[sensor].max) console.log(chalk.red(this.messages.alerts[sensor].high.replace('{value}', value.toFixed(2))));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const plant = new SmartPlant();
|
|
436
|
+
plant.start().catch(err => console.error(chalk.red('❌ Error al iniciar:'), err.message));
|
package/docs/banner.png
DELETED
|
Binary file
|