Programming
- There are two main programming methods supported and tested with the Smart Plant V1:
ESPHome
Arduino
In both scenarios, you will first need to enter the board into flashing mode. For that, press and hold the Flash pushbutton while you reset the board (pressing once the Reset pushbutton).
Important
For flashing new firmwares, if the OTA support is not available, you will need an external UST-to-TTL module (like this) connected to the Serial port (3.3, GND, Tx, Rx).
Caution
When flashing the board, make sure its only powered from one power source: through the Serial port (by removing the battery connector) or through the battery (but then do not connect the 3.3V pin on the Serial port).
ESPHome
ESPHome is a well known platform for programming ESP-based devices with a very little effort. It is configured via YAML files and supports a wide range of functionalities and sensors.
Important
For using ESPHome, and all its funcionalities, you need to have a Home Assistant instance running in the same network as your Smart Plant V1.
The Smart Plant V1 already comes with an embeded version of ESPHome, that would only require an OTA update to get it ready to work in your network:
Power the board, and let it run for 1-2 minutes. When the board cannot connect to a WiFi network, it will create a fallback hotspot.
Use a smartphone or tablet and go to the WiFi settings, connect to the recently created Smart-Plant hotspot with the password smartplant.
Access to the captive portal and open the browser if doesn’t pop up automatically.
Enter your network setttings and press Save.
Now, your ESPHome device is ready to be found by Home Assistant in your network. Add it from the ESPHome section to add and edit a customized configuration file.
As an example of such configuration setup (and the one flashed on the factory settings of the Smart Plant V1) with all the dependencies:
In the folder structure above:
Audiowide.ttf
is just a fonts style, you can download any of your choice and paste it therematerialdesignicons-webfont_5.9.55.ttf
is a file containing a set of MDI that you can download from hereicon-map.h
is a mapping file that is used to associate a variable name with the icon ID from the previous file. It contains the following code:
1 #include <map>
2 std::map<int, std::string> battery_icon_map
3 {
4 {0, "\U000F10CD"},
5 {1, "\U000F007A"},
6 {2, "\U000F007B"},
7 {3, "\U000F007C"},
8 {4, "\U000F007D"},
9 {5, "\U000F007E"},
10 {6, "\U000F007F"},
11 {7, "\U000F0080"},
12 {8, "\U000F0081"},
13 {9, "\U000F0082"},
14 {10, "\U000F0079"},
15 };
Lemon_tree_label_page_1.png
is the background image that will be displayed on the e-paper. It has a resolution of 296x128 pixels.
smart-plant.yaml
is the YAML configuration file:
1esphome:
2 name: smart-plant
3 # Initialize the IIC bus immediatelly after the powering the sensors
4 on_boot:
5 priority: 600
6 then:
7 - lambda: |-
8 Wire.begin();
9 delay(100);
10
11external_components:
12 - source:
13 type: git
14 url: https://github.com/velaar/esphome
15 ref: dev
16 components: [ waveshare_epaper, display]
17
18esp32:
19 board: esp32dev
20 framework:
21 type: arduino
22
23# Enable logging
24logger:
25
26# Enable Home Assistant API
27api:
28
29ota:
30 password: ***********
31
32wifi:
33 ssid: !secret wifi_ssid
34 password: !secret wifi_password
35
36 # Enable fallback hotspot (captive portal) in case wifi connection fails
37 ap:
38 ssid: "Smart-Plant"
39 password: "smartplant"
40
41captive_portal:
42
43
44i2c:
45 scl: GPIO22
46 sda: GPIO21
47 scan: true
48 id: bus_a
49
50spi:
51 clk_pin: GPIO13
52 mosi_pin: GPIO14
53
54image:
55 - file: "Lemon_tree_label_page_1.png"
56 id: page_1_background
57
58font:
59 - file: "fonts/Audiowide.ttf"
60 id: font_title
61 size: 20
62 - file: "fonts/Audiowide.ttf"
63 id: font_subtitle
64 size: 15
65 - file: "fonts/Audiowide.ttf"
66 id: font_parameters
67 size: 15
68
69time:
70 - platform: homeassistant
71 id: esptime
72
73switch:
74 - platform: gpio
75 pin: GPIO16
76 id: exc
77 name: "Excitation switch"
78 icon: "mdi:power"
79 restore_mode: ALWAYS_ON
80
81
82sensor:
83 # Temperature and humidity sensor
84 - platform: aht10
85 temperature:
86 name: "Temperature"
87 id: temp
88 icon: "mdi:thermometer"
89 humidity:
90 name: "Air Humidity"
91 id: hum
92 icon: "mdi:water-percent"
93 update_interval: 1s
94
95 # Light sensor
96 - platform: adc
97 pin: GPIO33
98 id: illum
99 name: "Light"
100 icon: "mdi:white-balance-sunny"
101 attenuation: 11db
102 unit_of_measurement: lux
103 update_interval: 1s
104 filters:
105 - lambda: |-
106 return (x / 10000.0) * 2000000.0;
107
108 # Capacitive soil moisture sensor
109 - platform: adc
110 pin: GPIO32
111 name: "Soil Moisture"
112 id : soil
113 icon: "mdi:cup-water"
114 update_interval: 1s
115 unit_of_measurement: "%"
116 attenuation: 11db
117 filters:
118 - median:
119 window_size: 7
120 send_every: 3
121
122 - calibrate_linear:
123 - 1.25 -> 100.00
124 - 2.8 -> 0.00
125 - lambda: if (x < 1) return 0; else if (x > 100) return 100; return (x);
126 accuracy_decimals: 0
127 on_value:
128 then:
129 - component.update: my_display
130
131display:
132 - platform: waveshare_epaper
133 dc_pin: GPIO27
134 cs_pin: GPIO15
135 busy_pin: GPIO25
136 reset_pin: GPIO26
137 rotation: 270
138 model: 2.90inv2
139 update_interval: never
140 id: my_display
141 pages:
142 - id: page1
143 lambda: |-
144 #define H_LEFT_MARGIN 4
145 #define H_RIGHT_MARGIN 280
146 #define H_CENTER 128
147 #define V_WEATHER 0
148 #define V_CLOCK 1
149 #define V_WIFI 30
150 #define V_VOLTAGE 60
151 #define V_BATTERY 90
152
153 it.image(0, 0, id(page_1_background));
154
155 // Battery
156 float battery_perc = id(battery).state;
157 int battery_range = battery_perc / 10 ;
158 battery_range = (battery_range > 10) ? 10 : battery_range;
159 battery_range = (battery_range < 0) ? 0 : battery_range;
160
161 it.printf(278, 1, id(font_icon_battery), TextAlign::TOP_LEFT, battery_icon_map[battery_range].c_str()
162 );
163 it.printf(278, 1, id(font_subtitle), TextAlign::TOP_RIGHT,
164 "%3.0f%%", battery_perc);
165
166
167 // Clock
168 it.strftime(278, 18, id(font_subtitle), TextAlign::TOP_RIGHT,
169 "%d/%m/%y", id(esptime).now());
170
171 // Parameters
172 // Drawing the marker over the gauge
173 float pi = 3.141592653589793;
174 float alpha = 4.71238898038469; // Defined as the gauge angle in radians (270deg)
175 float beta = 2*pi - alpha;
176 int radius = 22; // Radius of the gauge in pixels
177 int thick = 7; // Size of the marker
178
179 // *** Moisture ***
180 int min_range = 0;
181 int max_range = 100;
182 int xc = 80;
183 int yc = 50;
184
185 float measured = id(soil).state;
186
187 if (measured < min_range) {
188 measured = min_range;
189 }
190 if (measured > max_range) {
191 measured = max_range;
192 }
193
194 float val = (measured - min_range) / abs(max_range - min_range) * alpha;
195
196 int x0 = static_cast<int>(xc + radius + radius * cos(pi / 2 + beta / 2 + val));
197 int y0 = static_cast<int>(yc + radius + radius * sin(pi / 2 + beta / 2 + val));
198 int x1 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val + 0.1));
199 int y1 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val + 0.1));
200 int x2 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val - 0.1));
201 int y2 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val - 0.1));
202 it.line(x0, y0, x1, y1);
203 it.line(x1, y1, x2, y2);
204 it.line(x2, y2, x0, y0);
205
206 it.printf(xc + radius, yc + 1.7*radius, id(font_parameters), TextAlign::TOP_CENTER,
207 "%.0f%%", id(soil).state);
208
209 // *** Light ***
210 min_range = 0;
211 max_range = 10000;
212 xc = 134;
213 yc = 70;
214
215 measured = id(illum).state;
216
217 if (measured < min_range) {
218 measured = min_range;
219 }
220 if (measured > max_range) {
221 measured = max_range;
222 }
223
224 val = (measured - min_range) / abs(max_range - min_range) * alpha;
225 x0 = static_cast<int>(xc + radius + radius * cos(pi / 2 + beta / 2 + val));
226 y0 = static_cast<int>(yc + radius + radius * sin(pi / 2 + beta / 2 + val));
227 x1 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val + 0.1));
228 y1 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val + 0.1));
229 x2 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val - 0.1));
230 y2 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val - 0.1));
231 it.line(x0, y0, x1, y1);
232 it.line(x1, y1, x2, y2);
233 it.line(x2, y2, x0, y0);
234
235 it.printf(xc + radius, yc + 1.7*radius, id(font_parameters), TextAlign::TOP_CENTER,
236 "%.0flx", id(illum).state);
237
238
239 // *** Temperature ***
240 min_range = -10;
241 max_range = 50;
242 xc = 188;
243 yc = 50;
244
245 measured = id(temp).state;
246
247 if (measured < min_range) {
248 measured = min_range;
249 }
250 if (measured > max_range) {
251 measured = max_range;
252 }
253
254 val = (measured - min_range) / abs(max_range - min_range) * alpha;
255 x0 = static_cast<int>(xc + radius + radius * cos(pi / 2 + beta / 2 + val));
256 y0 = static_cast<int>(yc + radius + radius * sin(pi / 2 + beta / 2 + val));
257 x1 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val + 0.1));
258 y1 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val + 0.1));
259 x2 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val - 0.1));
260 y2 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val - 0.1));
261 it.line(x0, y0, x1, y1);
262 it.line(x1, y1, x2, y2);
263 it.line(x2, y2, x0, y0);
264
265 it.printf(xc + radius, yc + 1.7*radius, id(font_parameters), TextAlign::TOP_CENTER,
266 "%.0f°C", id(temp).state);
267
268
269 // *** Humidity ***
270 min_range = 20;
271 max_range = 80;
272 xc = 242;
273 yc = 70;
274
275 measured = id(hum).state;
276
277 if (measured < min_range) {
278 measured = min_range;
279 }
280 if (measured > max_range) {
281 measured = max_range;
282 }
283
284 val = (measured - min_range) / abs(max_range - min_range) * alpha;
285 x0 = static_cast<int>(xc + radius + radius * cos(pi / 2 + beta / 2 + val));
286 y0 = static_cast<int>(yc + radius + radius * sin(pi / 2 + beta / 2 + val));
287 x1 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val + 0.1));
288 y1 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val + 0.1));
289 x2 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val - 0.1));
290 y2 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val - 0.1));
291 it.line(x0, y0, x1, y1);
292 it.line(x1, y1, x2, y2);
293 it.line(x2, y2, x0, y0);
294
295 it.printf(xc + radius, yc + 1.7*radius, id(font_parameters), TextAlign::TOP_CENTER,
296 "%.0f%%", id(hum).state);
297
298
299deep_sleep:
300 run_duration: 10s
301 sleep_duration: 3600s
Arduino
If you are still interested in programming directly with the Arduino IDE, the procedure is no different than with any other ESP32 devices:
Open the Arduino IDE and go to File -> Preferences option.
Add to the Additional Boards Manager URSLs the url:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
Close the preferences and open in the menu Tools -> Board -> Boards Manager.
Search for esp32 and install it. This might take some time.
Now you can select the board ESP32 Dev Module as the target board. Leave the rest of parameters by default.
Select the correct port and remember to enter the board into flashing mode before uploading the sketch.