-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathNewSensor.yaml
More file actions
463 lines (426 loc) · 15.7 KB
/
Copy pathNewSensor.yaml
File metadata and controls
463 lines (426 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
# Code Parts Used From https://github.com/hanspeda/esphome_homeassistant_display.git
# Code Assistance Provided by OpenAI codex
# Code Parts Used From https://community.home-assistant.io/t/delayed-initialization-i2c-bus/659101/7
# -----------------------------------------------------------------------------
# Global substitution values used throughout the configuration. Adjusting these
# settings allows the device runtime and sleep intervals to be tuned without
# touching the rest of the file. A dedicated low-battery daytime interval keeps
# Wi-Fi performance high while still stretching runtimes. The night-specific
# window is shortened so the unit keeps checking in while avoiding a full code
# rewrite.
# -----------------------------------------------------------------------------
external_components:
# Pull in local custom components. Using a relative path keeps this firmware
# self-contained and allows quick iterations without publishing a library.
- source:
type: local
path: my_components
substitutions:
# Active time after a wake cycle. Keep short to minimize battery drain.
run_time: 20s
# Standard deep-sleep duration between measurements while on battery.
sleep_time: 30min
# Extended sleep when the battery is depleted to preserve charge.
low_battery_sleep_time: 120min
# Shortened night interval; leaves the device mostly awake at night for
# easier debugging without major logic changes (note the 11/15/25 tweak).
night_sleep_time: 1h ##reduction to almost no sleep to avoid having to rewrite the code. 11/15/25
# Core ESPHome configuration and boot sequence. The on_boot logic ensures the
# device gathers sensor data before entering deep sleep when running on
# battery-only power. The split priorities let us power the sensor rails before
# performing any reads, then pause deep sleep while the boot script finishes
# networking and publishing work.
esphome:
name: planter
friendly_name: Planter
on_boot:
# High priority: power the sensors as early as possible.
- priority: 700
then:
- switch.turn_on: sensor_vdd
- delay: 100ms # wait for sensor to power up
# Lower priority: block deep sleep and perform first measurements.
- priority: 200
then:
- deep_sleep.prevent: deep_sleep_ctrl
# Sample the 5V rail immediately so voltage telemetry remains available
# even though the device now always follows the low-power workflow.
- component.update: adc_5v_raw
- logger.log: "Running unified low-power workflow."
# The deep-sleep timer is already paused; hand off to the battery boot
# script so it can connect, publish, and then re-enable sleep once the
# data has landed in Home Assistant.
- script.execute: handle_battery_boot
# Logging is kept at DEBUG with UART disabled to reduce active-time power
# draw while still surfacing important events. Disable UART so no pins are held
# by the serial console during deep sleep.
logger:
level: DEBUG
baud_rate: 0
# ESP32 hardware configuration. The CPU frequency is reduced to save power
# during battery operation and ESP-IDF is used to expose deep-sleep controls.
esp32:
board: esp32dev
framework:
type: esp-idf
cpu_frequency: 80MHz
api:
# Secure encrypted API connection to Home Assistant.
id: homeassistant_api
encryption:
key: !secret planter20api
ota:
# Enable over-the-air updates via the ESPHome native method.
- platform: esphome
password: !secret ota_code
# Primary Wi-Fi connection along with a fallback access point to assist with
# provisioning should the device be unable to join the main network. Power save
# is disabled and the output power is driven high to maintain a distant link.
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.7.107
gateway: 192.168.7.1
subnet: 255.255.255.0
power_save_mode: NONE
output_power: 20dB
fast_connect: true
ap:
ssid: "Soil Fallback Hotspot"
password: "BDVDN7O0KT7E"
captive_portal:
# I2C bus that hosts the SHT31 sensor. Scanning is disabled to shorten boot
# time and because the addresses are hard-coded.
i2c:
sda: 21
scl: 22
scan: false
id: bus_a
switch:
# Power control for the sensors. Defaulting to off ensures the bus is only
# energized when required, extending battery life and avoiding phantom reads.
- platform: gpio
pin: GPIO17
id: sensor_vdd
name: "Sensor Power"
restore_mode: ALWAYS_OFF
sensor:
# SHT31 temperature and humidity readings. Automatic polling is disabled so
# that manual updates only occur when required, trimming awake time.
- platform: sht3xd
id: sht31_sensor
address: 0x44
update_interval: never
temperature:
name: "Planter Probe Temperature"
id: sht31_temperature
force_update: true
humidity:
name: "Planter Probe Humidity"
id: sht31_humidity
force_update: true
# Second SHT31 mounted in the enclosure to monitor ambient box conditions.
- platform: sht3xd
id: box_sht31_sensor
address: 0x45
update_interval: never
temperature:
name: "Enclosure Temperature"
id: box_temperature
force_update: true
humidity:
name: "Enclosure Humidity"
id: box_humidity
force_update: true
# Raw ADC reading of the 5V input, kept internal for derived sensors and only
# sampled on-demand. Multiplication accounts for the voltage-divider ratio.
- platform: adc
id: adc_5v_raw
name: "ADC Pin Voltage (GPIO36)"
pin: GPIO36
attenuation: 12db
update_interval: never
accuracy_decimals: 3
internal: true
# Scaled 5V reading that also toggles a binary sensor to note external power
# presence, incorporating hysteresis to avoid flapping states.
- platform: copy
source_id: adc_5v_raw
id: source_voltage
name: "5V"
unit_of_measurement: "V"
accuracy_decimals: 2
filters:
- multiply: 1.8
on_value:
then:
- lambda: |-
static bool present = false;
if (!present && x > 4.0) present = true;
else if (present && x < 3.0) present = false;
id(voltage_present).publish_state(present);
# Raw ADC reading of the Li-Ion battery which is scaled below to volts and
# percentage. Like the 5V channel, this is sampled only when necessary to
# avoid wasted conversions.
- platform: adc
id: adc_batt_raw
name: "ADC Pin Voltage (GPIO32)"
pin: GPIO32
attenuation: 12db
update_interval: never
accuracy_decimals: 3
internal: true
# Capacitive soil moisture probe read via ADC; left raw for reuse in filters.
- platform: adc
id: soil_moisture_raw
pin: GPIO33
name: "Soil Moisture Raw"
attenuation: 11db
update_interval: never
accuracy_decimals: 3
internal: true
# Scaled soil moisture percentage assuming 3.0V corresponds to saturation.
- platform: copy
source_id: soil_moisture_raw
name: "Capacitive Soil Moisture"
id: soil_moisture_percent
unit_of_measurement: "%"
device_class: moisture
state_class: measurement
accuracy_decimals: 1
filters:
- lambda: |-
float percent = (x / 3.0f) * 100.0f;
if (percent < 0.0f) percent = 0.0f;
if (percent > 100.0f) percent = 100.0f;
return percent;
# Battery voltage derived from the raw ADC reading with divider compensation.
- platform: copy
source_id: adc_batt_raw
id: battery_voltage
name: "Battery Voltage"
unit_of_measurement: "V"
device_class: voltage
state_class: measurement
accuracy_decimals: 2
force_update: true
filters:
- multiply: 1.36
# Battery percentage using empirically tuned discharge curve and clamping to
# ensure Home Assistant sees valid 0-100 values.
- platform: copy
source_id: battery_voltage
id: battery_percent
name: "Battery %"
unit_of_measurement: "%"
device_class: battery
state_class: measurement
accuracy_decimals: 0
force_update: true
filters:
- calibrate_linear:
- 4.2 -> 100
- 4.00 -> 85
- 3.85 -> 60
- 3.80 -> 50
- 3.70 -> 25
- 3.50 -> 5
- 3.30 -> 0
- lambda: |-
if (x < 0) x = 0;
if (x > 100) x = 100;
return x;
# Raw Wi-Fi RSSI, throttled to emit only meaningful changes for diagnostics.
- platform: wifi_signal
id: wifi_signal_dbm
name: "WiFi Signal"
update_interval: 60s
entity_category: diagnostic
filters:
- delta: 1
# Derived Wi-Fi signal percentage to give a user-friendly representation in
# dashboards.
- platform: copy
source_id: wifi_signal_dbm
name: "WiFi Signal (%)"
unit_of_measurement: "%"
device_class: signal_strength
entity_category: diagnostic
accuracy_decimals: 0
filters:
- lambda: |-
const float RSSI_MIN = -90.0f;
const float RSSI_MAX = -30.0f;
float percent = (x - RSSI_MIN) / (RSSI_MAX - RSSI_MIN) * 100.0f;
if (percent < 0.0f) percent = 0.0f;
if (percent > 100.0f) percent = 100.0f;
return percent;
- sliding_window_moving_average:
window_size: 4
send_every: 1
binary_sensor:
# Indicates whether external 5V power is present; used to decide between
# normal updates and the low-power workflow.
- platform: template
id: voltage_present
name: "Voltage Present"
internal: true
device_class: power
filters:
- delayed_on: 100ms
- delayed_off: 300ms
globals:
# Track the last sleep/publish reason so it survives reboots and can be
# displayed in Home Assistant.
- id: last_event_text
type: std::string
restore_value: yes
initial_value: '"none"'
text_sensor:
# Human-readable summary of the most recent sleep reason and timestamp.
- platform: template
name: "Planter Last Event"
id: last_event
update_interval: never
lambda: |-
return std::string(id(last_event_text).c_str());
on_value:
then:
- lambda: |-
id(last_event_text) = x;
ESP_LOGI("main", "Event stored: %s", x.c_str());
time:
# Pull time from Home Assistant so sleep decisions can use real hours.
- platform: homeassistant
id: home_time
script:
# Sleep management script differentiating between day and night to conserve
# battery life. Runs automatically on battery boots to collect and publish
# readings before re-entering deep sleep.
- id: handle_battery_boot
mode: restart
then:
- logger.log: "On battery, waiting for Wi-Fi and sensors."
- wait_until:
condition:
wifi.connected:
timeout: 30s
- logger.log: "Wi-Fi connected or timeout reached."
- wait_until:
condition:
api.connected:
timeout: 15s
- logger.log: "Network ready, reading sensors."
# Manual polling of every sensor to avoid background wakeups.
- component.update: sht31_sensor
- component.update: box_sht31_sensor
- component.update: wifi_signal_dbm
- component.update: adc_batt_raw
- component.update: adc_5v_raw
- component.update: soil_moisture_raw
# Ensure all readings are available before publishing.
- wait_until:
condition:
lambda: |-
return id(sht31_temperature).has_state() &&
id(sht31_humidity).has_state() &&
id(box_temperature).has_state() &&
id(battery_voltage).has_state();
- delay: 750ms
- logger.log: "Publishing complete, entering sleep."
- script.execute: enter_sleep
# Decide the appropriate sleep duration and persist the reason for Home
# Assistant. Nighttime and low-battery states extend sleep for longevity.
- id: enter_sleep
then:
- lambda: |-
auto now = id(home_time).now();
std::string reason;
if (!now.is_valid()) reason = "invalid_time";
else if (now.hour >= 22) reason = "night";
else if (id(battery_percent).has_state() && id(battery_percent).state <= 30.0f)
reason = "day_low_battery";
else
reason = "day_normal";
char buffer[128];
snprintf(buffer, sizeof(buffer),
"Sleep reason: %s at %s, battery: %.0f%%",
reason.c_str(),
now.strftime("%Y-%m-%d %H:%M:%S").c_str(),
id(battery_percent).state);
id(last_event).publish_state(buffer);
id(last_event_text) = buffer;
// DEBUG: TO PREVENT DUPLICATE LOG ENTRIES, COMMENT OUT THE BELOW LINE ///////////////
// ESP_LOGI("main", "Event stored: %s", buffer);
- if:
condition: # time after 21:00 (9pm) ##reduction to almost no sleep 11/15/25. changed from 21 to 23
lambda: |-
auto t = id(home_time).now();
return (t.is_valid() && t.hour >= 23);
then:
- homeassistant.event:
event: esphome.planter_sleep
data:
device: battery
reason: "night"
duration: "${night_sleep_time}"
battery_percent: !lambda 'return id(battery_percent).state;'
now: !lambda 'return id(home_time).now().strftime("%Y-%m-%d %H:%M:%S");'
- deep_sleep.allow: deep_sleep_ctrl
- delay: 250ms
- deep_sleep.enter:
id: deep_sleep_ctrl
sleep_duration: ${night_sleep_time}
else: # battery out of charge, extend sleep time to save battery
- if:
condition:
lambda: |-
return id(battery_percent).has_state() &&
id(battery_percent).state <= 30.0f;
then:
- homeassistant.event:
event: esphome.planter_sleep
data:
device: battery
reason: "day_low_battery"
duration: "${low_battery_sleep_time}"
battery_percent: !lambda 'return id(battery_percent).state;'
now: !lambda 'return id(home_time).now().strftime("%Y-%m-%d %H:%M:%S");'
- deep_sleep.allow: deep_sleep_ctrl
- delay: 250ms
- deep_sleep.enter:
id: deep_sleep_ctrl
sleep_duration: ${low_battery_sleep_time}
else: # normal battery sleep
- homeassistant.event:
event: esphome.planter_sleep
data:
device: battery
reason: "day_normal"
duration: "${sleep_time}"
battery_percent: !lambda 'return id(battery_percent).state;'
now: !lambda 'return id(home_time).now().strftime("%Y-%m-%d %H:%M:%S");'
- deep_sleep.allow: deep_sleep_ctrl
- delay: 250ms
- deep_sleep.enter:
id: deep_sleep_ctrl
sleep_duration: ${sleep_time}
interval:
# Failsafe refresh in case the node wakes via GPIO instead of deep sleep.
- interval: 30s
then:
- component.update: sht31_sensor
- component.update: box_sht31_sensor
- component.update: adc_5v_raw
- component.update: adc_batt_raw
- component.update: soil_moisture_raw
# Deep sleep controller that governs wake durations and responds to the
# external wake pin. Keeping the pin as KEEP_AWAKE prevents unexpected sleep
# when the pin is held high.
deep_sleep:
id: deep_sleep_ctrl
run_duration: ${run_time}
sleep_duration: ${sleep_time}
wakeup_pin: GPIO39
wakeup_pin_mode: KEEP_AWAKE