Files
zorruno-homeassistant/esphome/esp-flysprayer1.yaml
T
2025-09-10 21:07:31 +12:00

697 lines
28 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
##########################################################################################
##########################################################################################
# FLY SPRAYER Sinilink XY-WFUSB (ESP8266/ESP8285) ESPHome
#
# V1.0 2025-05-30 Initial Version
#
# Device: Sinilink XY-WFUSB module https://www.aliexpress.com/item/1005005815472269.html
# GPIO: Tasmota template: Button=GPIO4, Relay=GPIO5, Led_i=GPIO14, LedLink=GPIO16
#
# Purpose:
# - Pulse a MOSFET/relay with 5 V to trigger a fly spray can at selectable intervals.
# - Sprayer has its own timers, but it fires whenever it is started up, which we utilise here.
# - Enforce a cooldown so it never double-fires too quickly (Sprayer has some memory)
# - Operation window select: Night only / Day only / 24 Hours / On / Off.
# - Randomize each interval by ± a percentage to avoid a perfectly regular pattern.
# - Red LED (GPIO14, inverted) pulses slowly when idle; goes solid ON during spray.
# - Green LED (GPIO16) as standard ESPHome status LED.
# - Has a resettable total spray count, and a daily spray count
# - has a countdown until next spray
#
##########################################################################################
##########################################################################################
##########################################################################################
# SPECIFIC DEVICE VARIABLE SUBSTITUTIONS
# If NOT using a secrets file, just replace these with the passwords etc (in quotes)
##########################################################################################
substitutions:
# Device Naming
device_name: "esp-flysprayer1"
friendly_name: "Fly Sprayer"
description_comment: "Control of generic automatic fly sprayer on a configurable interval"
device_area: "Lounge" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.
# Project Naming
project_name: "Generic ESP8266.XY-WFUSB" # Project Details
project_version: "v1.0" # Project V denotes release of yaml file, allowing checking of deployed vs latest version
# Passwords & Secrets
api_key: !secret esp-api_key # unfortunately you can't use substitutions inside secrets names
ota_pass: !secret esp-ota_pass # unfortunately you can't use substitutions inside secrets names
static_ip_address: !secret esp-flysprayer1_ip
#mqtt_local_command_main_topic: !secret mqtt_local_command_main_topic
#mqtt_local_status_main_topic: !secret mqtt_local_status_main_topic
# Device Settings
log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
update_interval: "60s" # update time for for general sensors etc
# MQTT Controls
#mqtt_device_name: "lounge-flyspray"
#mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_device_name}" # Topic we will use to command this locally without HA
#mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_device_name}" # Topic we will use to view status locally without HA
# Hardware pins (XY-WFUSB per your Tasmota screen)
pin_button: 4 # GPIO4 -> momentary button (optional local trigger)
pin_relay: 5 # GPIO5 -> MOSFET/relay output
pin_led_red: 14 # GPIO14 -> Red LED (inverted)
pin_led_green: 16 # GPIO16 -> Green status LED (WiFi/status)
relay_inverted: false # Set to "true" if your MOSFET stage is active LOW
# Timing configuration
relay_activation_ms: 5000 # Relay Activation Time (ms). Nominally 5 seconds.
relay_cooldown_ms: 30000 # Relay Cool Down Period (ms). Minimum gap between sprays.
# Operation windows (local 24h). Night is from night_start_h to night_end_h across midnight.
night_start_h: 21 # 21:00 (9pm)
night_end_h: 7 # 07:00 (7am)
# Interval jitter (± percent of selected interval)
interval_jitter_pct: 10 # Default ±10%
#########################################################################################
# PACKAGES: Included Common Packages
# https://esphome.io/components/packages.html
##########################################################################################
packages:
common_wifi: !include
file: common/network_common.yaml
vars:
local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}"
common_api: !include
file: common/api_common.yaml
vars:
local_api_key: "${api_key}"
#common_webportal: !include
# file: common/webportal_common.yaml
common_mqtt: !include
file: common/mqtt_common.yaml
vars:
local_device_name: "${device_name}"
common_sntp: !include
file: common/sntp_common.yaml
common_general_sensors: !include
file: common/sensors_common.yaml
vars:
local_friendly_name: "${friendly_name}"
local_update_interval: "${update_interval}"
##########################################################################################
# ESPHome CORE CONFIGURATION
# https://esphome.io/components/esphome.html
##########################################################################################
esphome:
name: "${device_name}"
friendly_name: "${friendly_name}"
comment: "${description_comment}"
area: "${device_area}"
project:
name: "${project_name}"
version: "${project_version}"
on_boot:
# Early: make hardware safe/pretty
- priority: -10
then:
- switch.turn_off: relay_out
- light.turn_on:
id: red_led
brightness: 50%
effect: red_idle_pulse
# Late: after components & restores are ready, publish real values and start loop
- priority: 800
then:
- delay: 200ms
- lambda: |-
// Publish restored counts
id(spray_count_sensor).publish_state((float) id(spray_count));
id(spray_count_today_sensor).publish_state(id(spray_count_today));
// Reset countdown display
id(next_spray_seconds).publish_state(0);
id(next_spray_in_text).publish_state("Ready");
- script.execute: spray_loop
##########################################################################################
# ESP PLATFORM AND FRAMEWORK
# https://esphome.io/components/esp8266.html
# https://esphome.io/components/esp32.html
##########################################################################################
esp8266:
board: esp8285
early_pin_init: false # Initialise pins early to known values. Recommended false where switches are involved. Defaults to True.
board_flash_mode: dout # Default is dout
preferences:
flash_write_interval: 20s
##########################################################################################
# GLOBAL VARIABLES
# https://esphome.io/components/globals.html
##########################################################################################
globals:
# millisecond tick when we last fired successfully
- id: last_fire_ms
type: uint32_t
restore_value: no
initial_value: '0'
- id: last_wait_ms
type: uint32_t
restore_value: no
initial_value: '60000'
- id: window_allowed
type: bool
restore_value: no
initial_value: 'false'
# absolute millis() when the next spray countdown ends
- id: next_target_ms
type: uint32_t
restore_value: no
initial_value: '0'
# total successful sprays (persists across reboots)
- id: spray_count
type: uint32_t
restore_value: yes
initial_value: '0'
# total sprays since local midnight
- id: spray_count_today
type: uint32_t
restore_value: yes
initial_value: '0'
# day-of-month snapshot to detect midnight rollover
- id: last_day
type: int
restore_value: yes
initial_value: '-1'
##########################################################################################
# LOGGER COMPONENT
# https://esphome.io/components/logger.html
# Logs all log messages through the serial port and through MQTT topics.
##########################################################################################
logger:
level: "${log_level}" #INFO Level suggested, or DEBUG for testing
#baud_rate: 0 #set to 0 for no logging via UART, needed if you are using it for other serial things (eg PZEM)
#esp8266_store_log_strings_in_flash: false
#tx_buffer_size: 64
##########################################################################################
# MQTT COMMANDS
# This adds device-specific MQTT command triggers to the common MQTT configuration.
##########################################################################################
#mqtt:
# on_message:
# # Light control to ramp up
# - topic: "${mqtt_local_command_topic}/light/set"
# payload: "${mqtt_local_device_command_ON}"
# then:
# - switch.turn_on: mosfet_ramp_switch
# # Light control to ramp down
# - topic: "${mqtt_local_command_topic}/light/set"
# payload: "${mqtt_local_device_command_OFF}"
# then:
# - switch.turn_off: mosfet_ramp_switch
#########################################################################################
# STATUS LED
# https://esphome.io/components/status_led.html
#########################################################################################
status_led:
pin:
number: ${pin_led_green}
inverted: false
##########################################################################################
# SWITCH COMPONENT
# https://esphome.io/components/switch/
##########################################################################################
switch:
- platform: gpio
id: relay_out
name: "Spray Relay"
icon: mdi:relay
pin:
number: ${pin_relay}
inverted: ${relay_inverted}
restore_mode: RESTORE_DEFAULT_OFF
internal: true
on_turn_on:
# Ensure red LED goes solid during activation (script also enforces)
- light.turn_on:
id: red_led
brightness: 100%
effect: none
on_turn_off:
# Return to idle pulsing when relay is off
- light.turn_on:
id: red_led
brightness: 50%
effect: red_idle_pulse
#################################################################################################
# BUTTON COMPONENT
# https://esphome.io/components/button/index.html
#################################################################################################
button:
- platform: template
id: spray_now
name: "Spray Now"
icon: mdi:spray
on_press:
then:
- script.execute: try_spray_now
- platform: template
id: reset_spray_count
name: "Reset Spray Count"
icon: mdi:backup-restore
on_press:
then:
- lambda: |-
id(spray_count) = 0;
id(spray_count_sensor).publish_state(0);
#########################################################################################
# SELECT COMPONENT
# https://esphome.io/components/select/index.html
#########################################################################################
select:
- platform: template
id: spray_interval
name: "${friendly_name} Spray Interval"
icon: mdi:timer
optimistic: true
restore_value: true
initial_option: "20 minutes"
options:
- "5 minutes"
- "10 minutes"
- "20 minutes"
- "30 minutes"
- "1 hour"
- "2 hours"
- "4 hours"
on_value:
then:
- logger.log:
format: "Spray Interval changed to %s — rescheduling"
args: [ 'id(spray_interval).state.c_str()' ]
- script.execute: spray_loop
- platform: template
id: spray_operation
name: "${friendly_name} Operation"
icon: mdi:toggle-switch
optimistic: true
restore_value: true
initial_option: "On"
options:
- "Off"
- "Night Only"
- "Day Only"
- "24 Hours"
- "On"
on_value:
then:
- logger.log:
format: "Operation changed to %s — rescheduling"
args: [ 'id(spray_operation).state.c_str()' ]
- script.execute: spray_loop
##########################################################################################
# BINARY SENSORS
# https://esphome.io/components/binary_sensor/
##########################################################################################
binary_sensor:
# Front panel/local button (GPIO4) single press triggers a Spray Now (honors cooldown)
- platform: gpio
id: btn_local
pin:
number: ${pin_button}
mode: INPUT_PULLUP
inverted: true
name: "Local Button Press"
icon: mdi:spray
internal: true
on_press:
then:
- button.press: spray_now
##########################################################################################
# SENSOR COMPONENT
# https://esphome.io/components/sensor/
##########################################################################################
sensor:
# Next spray remaining time in seconds (updated by the scheduler)
- platform: template
id: next_spray_seconds
name: "Next Spray Seconds to go"
icon: mdi:timer-sand
unit_of_measurement: "s"
accuracy_decimals: 0
#entity_category: diagnostic
update_interval: never
# Total sprays counter (persisted; updated on each spray and when reset)
- platform: template
id: spray_count_sensor
name: "Spray Count Total"
icon: mdi:counter
unit_of_measurement: ""
accuracy_decimals: 0
state_class: total_increasing
update_interval: never
filters:
- round: 0
# Sprays since local midnight (auto-resets at midnight)
- platform: template
id: spray_count_today_sensor
name: "${friendly_name} Spray Count Today"
icon: mdi:counter
unit_of_measurement: ""
accuracy_decimals: 0
state_class: measurement
update_interval: never
filters:
- round: 0
##########################################################################################
# TEXT SENSOR COMPONENT
# https://esphome.io/components/text_sensor/
##########################################################################################
text_sensor:
- platform: template
id: next_spray_in_text
name: "Next Spray In"
icon: mdi:clock-outline
#entity_category: diagnostic
update_interval: never
##########################################################################################
# OUTPUT COMPONENT
# https://esphome.io/components/light/index.html
##########################################################################################
# An OUTPUT can be binary (0,1) or float, which is any value between 0 and 1.
# PWM Outputs such as "ledc" are float. https://esphome.io/components/output/ledc.html
##########################################################################################
output:
- platform: esp8266_pwm
id: red_led_pwm
pin:
number: ${pin_led_red}
inverted: true # Tasmota showed Led_i on GPIO14
##########################################################################################
# LIGHT COMPONENT
# https://esphome.io/components/light/
##########################################################################################
light:
- platform: monochromatic
id: red_led
name: "${friendly_name} Red LED"
icon: mdi:led-on
output: red_led_pwm
default_transition_length: 300ms
internal: true
effects:
- pulse:
name: red_idle_pulse
transition_length: 1500ms
update_interval: 1500ms
##########################################################################################
# NUMBER COMPONENT
# https://esphome.io/components/number/
##########################################################################################
##########################################################################################
# SCRIPT COMPONENT
# https://esphome.io/components/script.html
# Scripts can be executed nearly anywhere in your device configuration with a single call.
##########################################################################################
# SCRIPTS main loop, cooldown-safe trigger, and actual spray action
##########################################################################################
##########################################################################################
# SCRIPT COMPONENT
# https://esphome.io/components/script.html
# Scripts can be executed nearly anywhere in your device configuration with a single call.
##########################################################################################
# SCRIPTS main loop, cooldown-safe trigger, and actual spray action
##########################################################################################
script:
# Cooldown-checked "Spray Now"
- id: try_spray_now
mode: restart
then:
- if:
condition:
lambda: |-
const uint32_t now = millis();
return (now - id(last_fire_ms)) >= (uint32_t) ${relay_cooldown_ms};
then:
- logger.log: "Spray: activating relay (within allowed period)."
- lambda: |-
id(last_fire_ms) = millis();
- script.execute: do_spray
else:
- logger.log: "Spray request ignored: Fired during cool down period."
# Actual relay action for the configured activation time
- id: do_spray
mode: restart
then:
- lambda: |-
id(spray_count)++;
id(spray_count_sensor).publish_state((float) id(spray_count));
id(spray_count_today)++;
id(spray_count_today_sensor).publish_state(id(spray_count_today)); // publish as int
- switch.turn_on: relay_out
- light.turn_on:
id: red_led
brightness: 100%
effect: none
- delay: !lambda "return (uint32_t) ${relay_activation_ms};"
- switch.turn_off: relay_out
- light.turn_on:
id: red_led
brightness: 50%
effect: red_idle_pulse
# Scheduler loop waits a randomized interval, updates countdown text, checks window, then tries to spray
- id: spray_loop
mode: restart
then:
- while:
condition:
lambda: "return true;"
then:
# 1) Compute the base interval and randomized wait
- lambda: |-
auto s = id(spray_interval).state;
uint32_t base_sec = 3600; // default 1 hour
if (s == "5 minutes") base_sec = 5 * 60;
else if (s == "10 minutes") base_sec = 10 * 60;
else if (s == "20 minutes") base_sec = 20 * 60;
else if (s == "30 minutes") base_sec = 30 * 60;
else if (s == "1 hour") base_sec = 60 * 60;
else if (s == "2 hours") base_sec = 2 * 60 * 60;
else if (s == "4 hours") base_sec = 4 * 60 * 60;
// Jitter ±pct
const uint32_t pct = ${interval_jitter_pct};
const uint32_t jitter = (base_sec * pct) / 100;
const uint32_t span = (jitter * 2u) + 1u;
const uint32_t rnd = random_uint32() % span; // 0..(2*jitter)
const int32_t delta = (int32_t)rnd - (int32_t)jitter; // -jitter..+jitter
int32_t next_sec = (int32_t)base_sec + delta;
if (next_sec < 1) next_sec = 1;
ESP_LOGI("spray", "Next interval: base=%u s, jitter=%u%%, wait=%ld s",
base_sec, pct, (long) next_sec);
id(last_wait_ms) = (uint32_t) next_sec * 1000u;
id(next_target_ms) = millis() + id(last_wait_ms);
// Publish an initial countdown snapshot (minutes only for text; seconds kept for numeric sensor)
uint32_t rem_ms = id(next_target_ms) > millis() ? (id(next_target_ms) - millis()) : 0;
uint32_t sec = (rem_ms + 999u) / 1000u; // seconds, rounded up
id(next_spray_seconds).publish_state(sec);
uint32_t min = (sec + 59u) / 60u; // minutes, ceiling
char buf[16];
snprintf(buf, sizeof(buf), "%u min", (unsigned)min);
std::string countdown_txt(buf);
// Override text for specific modes
auto op = id(spray_operation).state;
char await_buf[16];
if (op == "Off") {
id(next_spray_in_text).publish_state("Disabled");
} else if (op == "Night Only") {
bool in_window = false;
if (id(sntp_time).now().is_valid()) {
auto n = id(sntp_time).now();
int hour = n.hour;
int nh = ${night_start_h};
int eh = ${night_end_h};
if (nh > eh) in_window = (hour >= nh) || (hour < eh);
else in_window = (hour >= nh) && (hour < eh);
}
if (!in_window) {
snprintf(await_buf, sizeof(await_buf), "%02d:00", ${night_start_h});
id(next_spray_in_text).publish_state(std::string("Awaiting ") + await_buf);
} else {
id(next_spray_in_text).publish_state(countdown_txt);
}
} else if (op == "Day Only") {
bool in_window = false;
if (id(sntp_time).now().is_valid()) {
auto n = id(sntp_time).now();
int hour = n.hour;
int nh = ${night_start_h};
int eh = ${night_end_h};
bool is_night;
if (nh > eh) is_night = (hour >= nh) || (hour < eh);
else is_night = (hour >= nh) && (hour < eh);
in_window = !is_night; // day is the opposite of night window
}
if (!in_window) {
snprintf(await_buf, sizeof(await_buf), "%02d:00", ${night_end_h});
id(next_spray_in_text).publish_state(std::string("Awaiting ") + await_buf);
} else {
id(next_spray_in_text).publish_state(countdown_txt);
}
} else {
id(next_spray_in_text).publish_state(countdown_txt);
}
# 2) Tick the countdown once per second until the target time
- while:
condition:
lambda: "return millis() < id(next_target_ms);"
then:
- lambda: |-
uint32_t rem_ms = id(next_target_ms) > millis() ? (id(next_target_ms) - millis()) : 0;
uint32_t sec = (rem_ms + 999u) / 1000u; // seconds, rounded up
id(next_spray_seconds).publish_state(sec);
uint32_t min = (sec + 59u) / 60u; // minutes, ceiling
char buf[16];
snprintf(buf, sizeof(buf), "%u min", (unsigned)min);
std::string countdown_txt(buf);
// Override text for specific modes
auto op = id(spray_operation).state;
char await_buf[16];
if (op == "Off") {
id(next_spray_in_text).publish_state("Disabled");
} else if (op == "Night Only") {
bool in_window = false;
if (id(sntp_time).now().is_valid()) {
auto n = id(sntp_time).now();
int hour = n.hour;
int nh = ${night_start_h};
int eh = ${night_end_h};
if (nh > eh) in_window = (hour >= nh) || (hour < eh);
else in_window = (hour >= nh) && (hour < eh);
}
if (!in_window) {
snprintf(await_buf, sizeof(await_buf), "%02d:00", ${night_start_h});
id(next_spray_in_text).publish_state(std::string("Awaiting ") + await_buf);
} else {
id(next_spray_in_text).publish_state(countdown_txt);
}
} else if (op == "Day Only") {
bool in_window = false;
if (id(sntp_time).now().is_valid()) {
auto n = id(sntp_time).now();
int hour = n.hour;
int nh = ${night_start_h};
int eh = ${night_end_h};
bool is_night;
if (nh > eh) is_night = (hour >= nh) || (hour < eh);
else is_night = (hour >= nh) && (hour < eh);
in_window = !is_night;
}
if (!in_window) {
snprintf(await_buf, sizeof(await_buf), "%02d:00", ${night_end_h});
id(next_spray_in_text).publish_state(std::string("Awaiting ") + await_buf);
} else {
id(next_spray_in_text).publish_state(countdown_txt);
}
} else {
id(next_spray_in_text).publish_state(countdown_txt);
}
// Midnight rollover check (via sntp_time)
if (id(sntp_time).now().is_valid()) {
auto n = id(sntp_time).now();
int d = n.day_of_month;
if (id(last_day) == -1) {
// First valid time after boot: initialise without reset
id(last_day) = d;
} else if (id(last_day) != d) {
// New day -> reset today's counter
id(last_day) = d;
id(spray_count_today) = 0;
id(spray_count_today_sensor).publish_state(0);
}
}
- delay: 1s
# 3) Check operation window; if allowed, attempt spray (cooldown enforced inside)
- lambda: |-
auto op = id(spray_operation).state;
bool allowed = false;
if (op == "Off") {
allowed = false;
} else if (op == "24 Hours" || op == "On") {
allowed = true;
} else {
if (!id(sntp_time).now().is_valid()) {
allowed = false; // no time yet; be conservative
} else {
auto n = id(sntp_time).now();
int hour = n.hour; // 0..23
int nh = ${night_start_h};
int eh = ${night_end_h};
bool is_night;
if (nh > eh) {
// window crosses midnight (e.g., 21:00..07:00)
is_night = (hour >= nh) || (hour < eh);
} else {
is_night = (hour >= nh) && (hour < eh);
}
if (op == "Night Only") allowed = is_night;
else if (op == "Day Only") allowed = !is_night;
}
}
if (allowed) ESP_LOGI("spray", "Window OK: attempting spray.");
else ESP_LOGI("spray", "Window blocked: skipping this cycle.");
id(window_allowed) = allowed;
- if:
condition:
lambda: "return id(window_allowed);"
then:
- script.execute: try_spray_now