smartplant 0.1.4 → 0.1.6
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 +74 -84
- package/main.js +606 -0
- package/package.json +3 -3
- package/comingsoon.js +0 -6
package/README.md
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
|
|
2
|
-
# SmartPlant
|
|
2
|
+
# SmartPlant by *PIGEONPOSSE*
|
|
3
3
|
|
|
4
4
|
[](https://github.com/pigeonposse)
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
**SmartPlant** is a
|
|
7
|
+
**SmartPlant** is a library designed to simplify plant care through *the integration of advanced artificial intelligence models*. This technology not only researches detailed information about each type of plant but also determines the optimal conditions for their care, thereby maximizing their growth and health. Thanks to this functionality, users can efficiently monitor and manage the environment of their plants *using sensors that measure humidity, light, and temperature*.
|
|
8
|
+
|
|
9
|
+
The core idea behind SmartPlant is to *pave the way for advancements in plant care technology*. It serves as a foundation for developing more sophisticated solutions and experimenting with innovative devices that meet the needs of plants. By leveraging this library, developers can contribute to the evolution of the smart plant care ecosystem.
|
|
10
|
+
|
|
11
|
+
> [!WARNING]
|
|
12
|
+
> Currently in phase `Beta`
|
|
13
|
+
|
|
14
|
+
## AI and the future intertwine: connect with your environment.
|
|
8
15
|
|
|
9
16
|
## Features
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
- 🤖 **Integrated Artificial Intelligence:** Optimizes the care of each plant.
|
|
19
|
+
- 📊 **Real-Time Monitoring:** Collects humidity, light, and temperature with sensors.
|
|
20
|
+
- 🔔 **Customized Alerts:** Notifications for out-of-range conditions.
|
|
21
|
+
- 💬 **Multilingual Support:** English, Español, Français, Deutsch, Italiano, Português, Nederlands, Русский, 中文, 日本語.
|
|
22
|
+
- 🎛 **Easy Setup:** Intuitive process for Raspberry Pi or Arduino.
|
|
23
|
+
- 📈 **Data History:** Environmental trend analysis.
|
|
24
|
+
- 🔮 **Critical Condition Prediction:** Prevents significant changes in plant health.
|
|
25
|
+
- 🌍 **Open-source:** MIT licensed, available for public use and contributions.
|
|
15
26
|
|
|
16
27
|
## Installation
|
|
17
28
|
|
|
@@ -21,98 +32,77 @@ To install the library, use npm:
|
|
|
21
32
|
npm install smartplant
|
|
22
33
|
```
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
To start using SmartPlant, follow these steps:
|
|
27
|
-
|
|
28
|
-
1. **Import the Library**
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
import SmartPlant from 'smartplant'
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
2. **Create an Instance and Configure**
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
const smartPlant = new SmartPlant();
|
|
39
|
-
|
|
40
|
-
// Select language
|
|
41
|
-
smartPlant.setLanguage('en'); // Options: 'en', 'es', 'fr', 'de', 'it', 'pt', 'nl', 'ru', 'zh', 'ja'
|
|
42
|
-
|
|
43
|
-
// Select input method
|
|
44
|
-
smartPlant.setInputMethod('local'); // Options: 'local', 'extern'
|
|
45
|
-
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
3. **Configure the Plant**
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
smartPlant.setPlantType('indoor'); // Options: 'indoor', 'outdoor'
|
|
53
|
-
smartPlant.setPlantName('Ficus'); // Replace 'Ficus' with the name of your plant
|
|
54
|
-
|
|
55
|
-
// Configure alerts
|
|
56
|
-
smartPlant.configureAlerts();
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
4. **Start Monitoring**
|
|
61
|
-
|
|
62
|
-
```
|
|
63
|
-
smartPlant.startMonitoring();
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
## Language Configuration
|
|
68
|
-
|
|
69
|
-
You can configure the language as follows:
|
|
35
|
+
# Benefits of the emojis system
|
|
70
36
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
37
|
+
This approach with emojis provides a simplified and visually attractive user experience.It allows even those without deep technical knowledge to quickly understand the state of the plant and take the necessary measures.It is a way to offer immediate and clear feedback, improving interaction with the system and promoting more regular and careful maintenance of the plant.
|
|
38
|
+
|
|
39
|
+
## Emojis scales for parameter monitoring
|
|
40
|
+
|
|
41
|
+
To make the plant monitoring more intuitive and visually accessible, we have implemented an emojis system that represents different levels of each critical parameter: moisture, light and temperature.Each emoji offers a rapid representation of the current status of the parameter, which facilitates interpretation without analyzing specific numbers.
|
|
42
|
+
|
|
43
|
+
#### Humidity:
|
|
44
|
+
- 🍂 **Very dry:** Indicates that moisture is below the recommended minimum range.The plant is at risk of dehydration.
|
|
45
|
+
- 🌿 **Ideal:** Moisture is within the ideal range, which means that the plant is in optimal conditions.
|
|
46
|
+
- 💧 **Slightly wet:** Indicates that moisture is slightly above the ideal range, but it is not yet worrisome.
|
|
47
|
+
- 🌊 **Very humid:** Moisture is above the maximum allowed range, which could lead to saturation and problems such as waterlogging.
|
|
48
|
+
|
|
49
|
+
#### Light:
|
|
50
|
+
- 🌑 **Very little light:** points out that the plant receives less light than necessary, which could affect its growth.
|
|
51
|
+
- 🌥 **Ideal:** The plant receives the amount of light adequate for healthy development.
|
|
52
|
+
- 🌞 **Too much light:** Light exposure is excessive, which can cause burns or stress in the plant.
|
|
53
|
+
|
|
54
|
+
#### Temperature:
|
|
55
|
+
- 🧊 **Very cold:** The temperature is below the minimum range, which can slow down or damage the plant.
|
|
56
|
+
- 🌡️ **Ideal:** The temperature is in the optimal range for the growth and development of the plant.
|
|
57
|
+
- 🔥 **Very hot:** The temperature exceeds the maximum range, which could cause overheating and dehydration.
|
|
74
58
|
|
|
75
|
-
|
|
59
|
+
### Emojis-based happiness system
|
|
76
60
|
|
|
77
|
-
|
|
78
|
-
* es - Spanish
|
|
79
|
-
* fr - French
|
|
80
|
-
* de - German
|
|
81
|
-
* it - Italian
|
|
82
|
-
* pt - Portuguese
|
|
83
|
-
* nl - Dutch
|
|
84
|
-
* ru - Russian
|
|
85
|
-
* zh - Chinese
|
|
86
|
-
* ja - Japanese
|
|
61
|
+
In addition to the specific parameters, we have designed a system of general happiness for the plant, which is represented with caritas emojis.This system provides a global vision of the state of the plant, based on a combination of its levels of humidity, light and temperature.
|
|
87
62
|
|
|
88
|
-
|
|
63
|
+
#### Happiness scale:
|
|
64
|
+
- 🤩 **Very happy:** The plant is in ideal conditions in all key parameters.This is the optimal state.
|
|
65
|
+
- 😊 **Happy:** The plant is in good condition, although there could be slight deviations in some parameters.
|
|
66
|
+
- 😐 **Acceptable:** The plant is in acceptable conditions, but is far from ideal.Small adjustments may be required.
|
|
67
|
+
- 😞 **Bad conditions:** The plant is experiencing unfavorable conditions and needs attention to avoid major damage.
|
|
68
|
+
- 😖 **Critical conditions:** The plant is in a critical state and requires immediate action to prevent its condition will get worse.
|
|
69
|
+
- 🥵 **Extremely critical:** The plant is in an extremely critical state and is in danger of dying if urgent measures are not taken.
|
|
70
|
+
- 😵 **No Data Detected:** Vital signals missing or sensors not transmitting requiring immediate check.
|
|
71
|
+
|
|
72
|
+
## ☕ Donate
|
|
89
73
|
|
|
90
|
-
|
|
74
|
+
Help us to develop more interesting things.
|
|
91
75
|
|
|
92
|
-
|
|
93
|
-
* **External**: Uses an external API; requires an API key.
|
|
76
|
+
[](https://pigeonposse.com/?popup=donate)
|
|
94
77
|
|
|
95
|
-
##
|
|
78
|
+
## 📜 License
|
|
96
79
|
|
|
97
|
-
|
|
80
|
+
This software is licensed with **[GPL-3.0](/LICENSE)**.
|
|
98
81
|
|
|
99
|
-
|
|
100
|
-
* **Low Sunlight**: "I am receiving too little sunlight. I need more sun!"
|
|
101
|
-
* **High Temperature**: "The temperature is too high. Please lower it!"
|
|
102
|
-
* **Happy**: "I am very happy and healthy!"
|
|
82
|
+
[](/LICENSE)
|
|
103
83
|
|
|
104
|
-
##
|
|
84
|
+
## 🐦 About us
|
|
105
85
|
|
|
106
|
-
|
|
86
|
+
*PigeonPosse* is a ✨ **code development collective** ✨ focused on creating practical and interesting tools that help developers and users enjoy a more agile and comfortable experience. Our projects cover various programming sectors and we do not have a thematic limitation in terms of projects.
|
|
107
87
|
|
|
108
|
-
|
|
88
|
+
[](https://github.com/pigeonposse)
|
|
109
89
|
|
|
110
|
-
|
|
90
|
+
### Collaborators
|
|
111
91
|
|
|
112
|
-
|
|
92
|
+
| | Name | Role | GitHub |
|
|
93
|
+
| ---------------------------------------------------------------------------------- | ----------- | ------------ | ---------------------------------------------- |
|
|
94
|
+
| <img src="https://github.com/alejomalia.png?size=72" alt="Angelo" style="border-radius:100%"/> | Alejo | Author & Development | [@alejomalia](https://github.com/alejomalia) |
|
|
95
|
+
| <img src="https://github.com/PigeonPosse.png?size=72" alt="PigeonPosse" style="border-radius:100%"/> | PigeonPosse | Collective | [@PigeonPosse](https://github.com/PigeonPosse) |
|
|
113
96
|
|
|
114
|
-
|
|
97
|
+
<br>
|
|
98
|
+
<p align="center">
|
|
115
99
|
|
|
116
|
-
-
|
|
100
|
+
[](https://pigeonposse.com)
|
|
101
|
+
[](https://pigeonposse.com?popup=about)
|
|
102
|
+
[](https://pigeonposse.com/?popup=donate)
|
|
103
|
+
[](https://github.com/pigeonposse)
|
|
104
|
+
[](https://twitter.com/pigeonposse_)
|
|
105
|
+
[](https://www.instagram.com/pigeon.posse/)
|
|
106
|
+
[](https://medium.com/@pigeonposse)
|
|
117
107
|
|
|
118
|
-
|
|
108
|
+
</p>
|
package/main.js
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
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;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smartplant",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "A library for managing plant care with alerts and multi-language support.",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "main.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@serialport/parser-readline": "^12.0.0",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
]
|
|
111
111
|
},
|
|
112
112
|
"scripts": {
|
|
113
|
-
"start": "node
|
|
113
|
+
"start": "node main.js",
|
|
114
114
|
"update-version": "changeset && changeset version",
|
|
115
115
|
"push": "git add . && cz && git push -f origin $@",
|
|
116
116
|
"push:main": "pnpm push main"
|