diff --git a/examples/multi-device/my-lemon-tree.yaml b/examples/multi-device/my-lemon-tree.yaml new file mode 100644 index 0000000..dd02acf --- /dev/null +++ b/examples/multi-device/my-lemon-tree.yaml @@ -0,0 +1,40 @@ +# ============================================================================= +# My Lemon Tree — device-specific config +# ============================================================================= +# Only define what's specific to this device. Everything else comes from the +# base package (sensors, display, deep sleep, MQTT, fonts...). +# ============================================================================= + +substitutions: + device_name: "lemon-tree" + friendly_name: "Lemon Tree" + device_comment: "Citrus limon / Lemon Tree" + + # MQTT topic prefix — set once and never change (renaming orphans HA entities). + # Format: -, visible in ESPHome logs on first boot. + mqtt_topic_prefix: "lemon-tree-abcdef" + + # OTA password for this device + ota_password: "replace-with-your-ota-password" + + # Plant label image (place PNG in esphome/plant_labels/) + label_image: "plant_labels/Lemon_tree_label_page_1.png" + + timezone: "Europe/Paris" + + # Soil calibration — measure your specific sensor in dry air and submerged + soil_v_wet: "1.25" + soil_v_dry: "2.8" + + # Lemon trees tolerate more light than average + light_max: "8000" + +packages: + base: github://flowcool/Smart_Plant/examples/multi-device/packages/smart_plant_base.yaml@V2R1 + +# Static IP — merges on top of base wifi block. Remove this block for DHCP. +wifi: + manual_ip: + static_ip: 192.168.1.100 + gateway: 192.168.1.1 + subnet: 255.255.255.0 diff --git a/examples/multi-device/packages/smart_plant_base.yaml b/examples/multi-device/packages/smart_plant_base.yaml new file mode 100644 index 0000000..1527010 --- /dev/null +++ b/examples/multi-device/packages/smart_plant_base.yaml @@ -0,0 +1,318 @@ +# ============================================================================= +# Smart Plant — shared base package +# ============================================================================= +# Usage: create a device file (see ../my-lemon-tree.yaml) that defines only +# the substitutions specific to that plant, then reference this package via: +# +# packages: +# base: github://flowcool/Smart_Plant/examples/multi-device/packages/smart_plant_base.yaml@V2R1 +# +# Every change pushed here propagates to all devices on the next OTA flash. +# ============================================================================= + +# Default values — override any of these in your device file. +substitutions: + device_name: "smart-plant" + friendly_name: "Smart Plant" + device_comment: "${friendly_name}" + project_name: "smart.plant" + project_version: "2.2" + ap_ssid: "Smart-Plant" + ap_pwd: "smartplant" + timezone: "Europe/Paris" + + # MQTT topic prefix — must match retained prefix already in Home Assistant. + # Format: -. Set once, never change + # (renaming orphans all entities in HA). + mqtt_topic_prefix: "${device_name}" + + # OTA password (leave empty string for no password) + ota_password: "" + + # Address used by ESPHome CLI/dashboard for OTA and log connections. + # Default: device hostname via mDNS. Set to static IP for reliability. + use_address: "${device_name}" + + # Background image for the e-paper display — local path or URL + label_image: "https://smart-plant.readthedocs.io/en/v2r1/_images/Lemon_tree_label_page_1.png" + + # Soil moisture calibration (ADC voltage → %) + soil_v_wet: "1.25" # voltage when sensor is fully submerged + soil_v_dry: "2.8" # voltage in dry air + + # Gauge display ranges + light_max: "3775" + temp_min: "-10" + temp_max: "50" + hum_min: "20" + hum_max: "80" + +esphome: + name: "${device_name}" + name_add_mac_suffix: true + comment: "${device_comment}" + project: + name: "${project_name}" + version: "${project_version}" + on_boot: + priority: 600 + then: + - wait_until: + condition: + mqtt.connected: + timeout: 30s + - script.execute: consider_deep_sleep + +esp32: + board: esp32-s2-saola-1 + framework: + type: arduino + cpu_frequency: 160MHz + +logger: + level: WARN + +# MQTT — required for deep-sleep compatibility: retained values survive sleep +# so Home Assistant sees fresh data on wake without "unavailable" state. +# To use native API instead, remove this block and uncomment `# api:` below +# (deep sleep will still work but HA may briefly show entities as unavailable). +mqtt: + broker: !secret mqtt_broker + username: !secret mqtt_username + password: !secret mqtt_password + discovery: true + discovery_retain: true + birth_message: # disabled — prevents HA marking device offline during sleep + will_message: + log_topic: # disabled — saves bandwidth on wake + topic_prefix: "${mqtt_topic_prefix}" + +# api: + +ota: + - platform: esphome + password: "${ota_password}" + +captive_portal: +improv_serial: + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + fast_connect: true + power_save_mode: LIGHT + use_address: "${use_address}" + ap: + ssid: "${ap_ssid}" + password: "${ap_pwd}" + +i2c: + scl: GPIO34 + sda: GPIO33 + scan: false + id: bus_a + +spi: + clk_pin: GPIO12 + mosi_pin: GPIO11 + +image: + - file: "${label_image}" + id: page_1_background + type: BINARY + invert_alpha: true + +font: + - file: "gfonts://Audiowide" + id: font_title + size: 20 + - file: "gfonts://Audiowide" + id: font_subtitle + size: 15 + - file: "gfonts://Audiowide" + id: font_parameters + size: 15 + - file: 'gfonts://Material+Symbols+Outlined' + id: font_icon + size: 20 + glyphs: + - "\U0000ebdc" + - "\U0000ebd9" + - "\U0000ebe0" + - "\U0000ebdd" + - "\U0000ebe2" + - "\U0000ebd4" + - "\U0000e1a4" + - "\U0000e627" + +time: + - platform: sntp + id: esptime + timezone: "${timezone}" + servers: + - "162.159.200.1" # Cloudflare NTP (anycast, IP stable) + - "216.239.35.0" # Google NTP + - "pool.ntp.org" + on_time_sync: + then: + - component.update: my_display + +switch: + - platform: gpio + pin: GPIO4 + id: exc + icon: "mdi:power" + restore_mode: ALWAYS_ON + internal: true + +sensor: + - platform: max17043 + id: max17043_id + battery_voltage: + id: batvolt + name: "${friendly_name} Battery voltage" + internal: true + battery_level: + id: batpercent + device_class: "battery" + unit_of_measurement: "%" + name: "${friendly_name} Battery" + force_update: true + + - platform: aht10 + variant: AHT20 + i2c_id: bus_a + temperature: + name: "${friendly_name} Temperature" + id: temp + icon: "mdi:thermometer" + device_class: temperature + force_update: true + humidity: + name: "${friendly_name} Air Humidity" + id: hum + icon: "mdi:water-percent" + device_class: humidity + force_update: true + update_interval: 3s + + - platform: veml7700 + address: 0x10 + update_interval: 3s + ambient_light: + name: "${friendly_name} Ambient light" + id: light + icon: "mdi:white-balance-sunny" + device_class: illuminance + force_update: true + actual_gain: + name: "${friendly_name} Actual gain" + internal: true + + - platform: adc + pin: GPIO1 + name: "${friendly_name} Soil Moisture" + id: soil + icon: "mdi:cup-water" + device_class: moisture + update_interval: 3s + unit_of_measurement: "%" + attenuation: 12db + force_update: true + filters: + - median: + window_size: 5 + send_every: 5 + - calibrate_linear: + - ${soil_v_wet} -> 100.00 + - ${soil_v_dry} -> 0.00 + - lambda: if (x < 1) return 0; else if (x > 100) return 100; return (x); + accuracy_decimals: 0 + +display: + - platform: waveshare_epaper + cs_pin: GPIO10 + dc_pin: GPIO13 + busy_pin: GPIO14 + reset_pin: GPIO15 + rotation: 270 + model: 2.90inv2 + id: my_display + update_interval: never + full_update_every: 1 + pages: + - id: page1 + lambda: |- + it.image(0, 0, id(page_1_background)); + + float battery_perc = id(batpercent).state; + int battery_range = battery_perc / 16; + battery_range = (battery_range > 6) ? 6 : battery_range; + battery_range = (battery_range < 0) ? 0 : battery_range; + + const char* battery_icon_map[] = { + "\U0000ebdc", "\U0000ebd9", "\U0000ebe0", "\U0000ebdd", + "\U0000ebe2", "\U0000ebd4", "\U0000e1a4" + }; + it.printf(278, 1, id(font_icon), TextAlign::TOP_LEFT, battery_icon_map[battery_range]); + it.printf(278, 1, id(font_subtitle), TextAlign::TOP_RIGHT, "%3.0f%%", battery_perc); + + auto now = id(esptime).now(); + if (now.is_valid()) { + it.strftime(278, 18, id(font_subtitle), TextAlign::TOP_RIGHT, "%H:%M %d/%m", now); + } else { + it.printf(278, 18, id(font_subtitle), TextAlign::TOP_RIGHT, "--:-- --/--"); + } + it.printf(278, 18, id(font_icon), TextAlign::TOP_LEFT, "\U0000e627"); + + float pi = 3.141592653589793; + float alpha = 4.71238898038469; + float beta = 2*pi - alpha; + int radius = 22; + int thick = 7; + + auto draw_gauge = [&](float value, float vmin, float vmax, int xc, int yc, const char* fmt) { + if (value < vmin) value = vmin; + if (value > vmax) value = vmax; + float val = (value - vmin) / abs(vmax - vmin) * alpha; + int x0 = static_cast(xc + radius + radius * cos(pi / 2 + beta / 2 + val)); + int y0 = static_cast(yc + radius + radius * sin(pi / 2 + beta / 2 + val)); + int x1 = static_cast(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val + 0.1)); + int y1 = static_cast(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val + 0.1)); + int x2 = static_cast(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val - 0.1)); + int y2 = static_cast(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val - 0.1)); + it.line(x0, y0, x1, y1); + it.line(x1, y1, x2, y2); + it.line(x2, y2, x0, y0); + it.printf(xc + radius, yc + 1.7*radius, id(font_parameters), TextAlign::TOP_CENTER, fmt, value); + }; + + draw_gauge(id(soil).state, 0, 100, 80, 50, "%.0f%%"); + draw_gauge(id(light).state, 0, ${light_max}, 134, 70, "%.0flx"); + draw_gauge(id(temp).state, ${temp_min}, ${temp_max}, 188, 50, "%.0f°C"); + draw_gauge(id(hum).state, ${hum_min}, ${hum_max}, 242, 70, "%.0f%%"); + +deep_sleep: + id: deep_sleep_control + sleep_duration: 1h + +script: + - id: consider_deep_sleep + mode: queued + then: + - delay: 5s + - component.update: my_display + - delay: 5s + - if: + condition: + sensor.in_range: + id: batpercent + above: 90 + then: + - deep_sleep.prevent: deep_sleep_control + else: + - switch.turn_off: exc + - max17043.sleep_mode: max17043_id + - deep_sleep.enter: deep_sleep_control + - delay: 25s + - script.execute: consider_deep_sleep