various tidyups and esphome adds
This commit is contained in:
@@ -0,0 +1,595 @@
|
||||
##########################################################################################
|
||||
##########################################################################################
|
||||
# DATA CUPBOARD FAN CONTROL
|
||||
# Fan control for Data Cupboard
|
||||
#
|
||||
# Hardware: Yunshan 7-30V 10A Relay Board (HW-622) with ESP8266MOD
|
||||
#
|
||||
# V1.3 2026-02-20 Remove invalid allow_other_uses on pulse_counter pins (ESPHome 2026.2.x)
|
||||
# V1.2 2026-02-20 Use common SNTP package, and publish fan fault/status via interval script
|
||||
# V1.1 2026-02-20 Fix template binary_sensor update method for ESPHome 2026.2.x
|
||||
# V1.0 2026-02-20 Initial Version
|
||||
#
|
||||
# NOTES
|
||||
# - Fan tach inputs are on GPIO1/GPIO3 (UART pins). Logger UART is disabled (baud_rate: 0).
|
||||
# - DHT22 data is on GPIO0 (boot-strap pin). Usually OK, but if you ever get boot issues,
|
||||
# move the DHT22 to a different GPIO if your board allows.
|
||||
# - Using common/sntp_common.yaml for time and diagnostic time text sensors.
|
||||
##########################################################################################
|
||||
##########################################################################################
|
||||
|
||||
##########################################################################################
|
||||
# SPECIFIC DEVICE VARIABLE SUBSTITUTIONS
|
||||
##########################################################################################
|
||||
substitutions:
|
||||
# Device Naming
|
||||
device_name: "esp-datacupboardfans"
|
||||
friendly_name: "Data Cupboard Fans"
|
||||
description_comment: "Fan control for Data Cupboard"
|
||||
device_area: "Data Cupboard"
|
||||
|
||||
# Project Naming
|
||||
project_name: "Yunshan.HW-622 ESP8266MOD Relay Board"
|
||||
project_version: "v1.3"
|
||||
|
||||
# Passwords
|
||||
api_key: !secret esp-api_key
|
||||
ota_pass: !secret esp-ota_pass
|
||||
static_ip_address: !secret esp-datacupboardfans_ip
|
||||
mqtt_command_main_topic: !secret mqtt_command_main_topic
|
||||
|
||||
# MQTT Controls
|
||||
mqtt_device_name: "data-cupboard-fans"
|
||||
mqtt_main_topic: "${mqtt_command_main_topic}/${mqtt_device_name}"
|
||||
|
||||
# Device Settings
|
||||
log_level: "ERROR"
|
||||
update_interval: "60s"
|
||||
|
||||
# Names
|
||||
relay_name: "Data Cupboard Fans Relay"
|
||||
override_name: "Override Fans On"
|
||||
door_name: "Data Cupboard Door Status"
|
||||
temp_name: "Data Cupboard Temperature"
|
||||
hum_name: "Data Cupboard Humidity"
|
||||
fan1_rpm_name: "Data Cupboard Fan1 RPM"
|
||||
fan2_rpm_name: "Data Cupboard Fan2 RPM"
|
||||
fan_status_name: "Data Cupboard Fan Status"
|
||||
|
||||
# Fan tach assumptions (typical PC fan = 2 pulses per revolution)
|
||||
fan_pulses_per_rev: "2"
|
||||
|
||||
# Fault thresholds
|
||||
rpm_min_when_relay_off: "950"
|
||||
rpm_min_when_relay_on: "2300"
|
||||
|
||||
# Default thermostat thresholds
|
||||
default_temp_on: "35"
|
||||
default_temp_off: "28"
|
||||
|
||||
##########################################################################################
|
||||
# PACKAGES: Included Common Packages
|
||||
##########################################################################################
|
||||
packages:
|
||||
#### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode ####
|
||||
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}"
|
||||
|
||||
#### HOME ASSISTANT API ####
|
||||
common_api: !include
|
||||
file: common/api_common.yaml
|
||||
vars:
|
||||
local_api_key: "${api_key}"
|
||||
|
||||
#### MQTT ####
|
||||
common_mqtt: !include
|
||||
file: common/mqtt_common.yaml
|
||||
vars:
|
||||
local_device_name: "${device_name}"
|
||||
|
||||
#### SNTP ####
|
||||
common_sntp: !include
|
||||
file: common/sntp_common.yaml
|
||||
|
||||
#### DIAGNOSTICS Sensors ####
|
||||
diag_basic: !include common/include_basic_diag_sensors.yaml
|
||||
diag_more: !include common/include_more_diag_sensors.yaml
|
||||
|
||||
##########################################################################################
|
||||
# ESPHome
|
||||
##########################################################################################
|
||||
esphome:
|
||||
name: "${device_name}"
|
||||
friendly_name: "${friendly_name}"
|
||||
comment: "${description_comment}"
|
||||
area: "${device_area}"
|
||||
project:
|
||||
name: "${project_name}"
|
||||
version: "${project_version}"
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- lambda: |-
|
||||
id(boot_ms) = millis();
|
||||
id(relay_last_change_ms) = millis();
|
||||
- delay: 2s
|
||||
- script.execute: apply_fan_control
|
||||
- script.execute: evaluate_fan_fault
|
||||
|
||||
##########################################################################################
|
||||
# ESP Platform and Framework
|
||||
##########################################################################################
|
||||
esp8266:
|
||||
board: esp12e
|
||||
restore_from_flash: true
|
||||
|
||||
preferences:
|
||||
flash_write_interval: 5min
|
||||
|
||||
mdns:
|
||||
disabled: false
|
||||
|
||||
##########################################################################################
|
||||
# LOGGING
|
||||
##########################################################################################
|
||||
logger:
|
||||
level: "${log_level}"
|
||||
baud_rate: 0
|
||||
|
||||
##########################################################################################
|
||||
# GLOBALS
|
||||
##########################################################################################
|
||||
globals:
|
||||
# Max temperature tracking
|
||||
- id: g_max_temp_today
|
||||
type: float
|
||||
restore_value: yes
|
||||
initial_value: "-1000.0"
|
||||
- id: g_max_temp_yesterday
|
||||
type: float
|
||||
restore_value: yes
|
||||
initial_value: "-1000.0"
|
||||
- id: g_max_temp_month
|
||||
type: float
|
||||
restore_value: yes
|
||||
initial_value: "-1000.0"
|
||||
- id: g_max_temp_last_month
|
||||
type: float
|
||||
restore_value: yes
|
||||
initial_value: "-1000.0"
|
||||
- id: g_max_temp_year
|
||||
type: float
|
||||
restore_value: yes
|
||||
initial_value: "-1000.0"
|
||||
|
||||
# Last seen date parts (for rollover)
|
||||
- id: g_last_day
|
||||
type: int
|
||||
restore_value: yes
|
||||
initial_value: "0"
|
||||
- id: g_last_month
|
||||
type: int
|
||||
restore_value: yes
|
||||
initial_value: "0"
|
||||
- id: g_last_year
|
||||
type: int
|
||||
restore_value: yes
|
||||
initial_value: "0"
|
||||
|
||||
# Fault evaluation grace timing
|
||||
- id: boot_ms
|
||||
type: uint32_t
|
||||
restore_value: no
|
||||
initial_value: "0"
|
||||
- id: relay_last_change_ms
|
||||
type: uint32_t
|
||||
restore_value: no
|
||||
initial_value: "0"
|
||||
|
||||
##########################################################################################
|
||||
# INTERVAL (periodic evaluations)
|
||||
# NOTE: common_sntp also defines its own interval (60s) for its fallback clock.
|
||||
##########################################################################################
|
||||
interval:
|
||||
- interval: 5s
|
||||
then:
|
||||
- script.execute: evaluate_fan_fault
|
||||
|
||||
##########################################################################################
|
||||
# SWITCHES (Relay + Override)
|
||||
##########################################################################################
|
||||
switch:
|
||||
# Relay output (GPIO4) - powers both fans
|
||||
- platform: gpio
|
||||
name: "${relay_name}"
|
||||
id: fan_relay
|
||||
pin: GPIO4
|
||||
restore_mode: RESTORE_DEFAULT_OFF
|
||||
icon: "mdi:fan"
|
||||
on_turn_on:
|
||||
then:
|
||||
- lambda: |-
|
||||
id(relay_last_change_ms) = millis();
|
||||
- script.execute: apply_fan_control
|
||||
- script.execute: evaluate_fan_fault
|
||||
on_turn_off:
|
||||
then:
|
||||
- lambda: |-
|
||||
id(relay_last_change_ms) = millis();
|
||||
- script.execute: apply_fan_control
|
||||
- script.execute: evaluate_fan_fault
|
||||
|
||||
# Override switch (forces fans ON, bypasses thermostat logic)
|
||||
- platform: template
|
||||
name: "${override_name}"
|
||||
id: override_fans_on
|
||||
restore_mode: RESTORE_DEFAULT_OFF
|
||||
optimistic: true
|
||||
icon: "mdi:fan-plus"
|
||||
entity_category: config
|
||||
turn_on_action:
|
||||
- switch.turn_on: fan_relay
|
||||
- script.execute: apply_fan_control
|
||||
- script.execute: evaluate_fan_fault
|
||||
turn_off_action:
|
||||
- script.execute: apply_fan_control
|
||||
- script.execute: evaluate_fan_fault
|
||||
|
||||
##########################################################################################
|
||||
# NUMBERS (On/Off thresholds with hysteresis)
|
||||
##########################################################################################
|
||||
number:
|
||||
- platform: template
|
||||
name: "Fans On Temperature"
|
||||
id: temp_on_threshold
|
||||
unit_of_measurement: "degC"
|
||||
min_value: 25
|
||||
max_value: 40
|
||||
step: 0.5
|
||||
mode: slider
|
||||
restore_value: true
|
||||
initial_value: ${default_temp_on}
|
||||
optimistic: true
|
||||
entity_category: config
|
||||
icon: "mdi:thermometer-chevron-up"
|
||||
set_action:
|
||||
- script.execute: apply_fan_control
|
||||
|
||||
- platform: template
|
||||
name: "Fans Off Temperature"
|
||||
id: temp_off_threshold
|
||||
unit_of_measurement: "degC"
|
||||
min_value: 25
|
||||
max_value: 40
|
||||
step: 0.5
|
||||
mode: slider
|
||||
restore_value: true
|
||||
initial_value: ${default_temp_off}
|
||||
optimistic: true
|
||||
entity_category: config
|
||||
icon: "mdi:thermometer-chevron-down"
|
||||
set_action:
|
||||
- script.execute: apply_fan_control
|
||||
|
||||
##########################################################################################
|
||||
# BINARY SENSORS (Door + Fault)
|
||||
##########################################################################################
|
||||
binary_sensor:
|
||||
# Door status on GPIO5
|
||||
- platform: gpio
|
||||
name: "${door_name}"
|
||||
id: door_status
|
||||
device_class: door
|
||||
pin:
|
||||
number: GPIO5
|
||||
mode: INPUT_PULLUP
|
||||
inverted: false
|
||||
filters:
|
||||
- delayed_on: 50ms
|
||||
- delayed_off: 50ms
|
||||
|
||||
# Fan fault boolean (published by script)
|
||||
- platform: template
|
||||
name: "Data Cupboard Fan Fault"
|
||||
id: fan_fault
|
||||
device_class: problem
|
||||
|
||||
##########################################################################################
|
||||
# STATUS LED (GPIO2)
|
||||
##########################################################################################
|
||||
status_led:
|
||||
pin:
|
||||
number: GPIO2
|
||||
inverted: true
|
||||
|
||||
##########################################################################################
|
||||
# SENSORS (Temp/Humidity + Fan RPM + Max temp templates)
|
||||
##########################################################################################
|
||||
sensor:
|
||||
# DHT22 on GPIO0
|
||||
- platform: dht
|
||||
pin: GPIO0
|
||||
model: DHT22
|
||||
update_interval: 30s
|
||||
temperature:
|
||||
name: "${temp_name}"
|
||||
id: cupboard_temp
|
||||
unit_of_measurement: "degC"
|
||||
accuracy_decimals: 1
|
||||
on_value:
|
||||
then:
|
||||
- script.execute: update_temp_maxima
|
||||
- script.execute: apply_fan_control
|
||||
humidity:
|
||||
name: "${hum_name}"
|
||||
id: cupboard_humidity
|
||||
unit_of_measurement: "%"
|
||||
accuracy_decimals: 1
|
||||
|
||||
# Fan1 tach pulses on GPIO1 (UART TX pin)
|
||||
- platform: pulse_counter
|
||||
pin:
|
||||
number: GPIO1
|
||||
mode: INPUT_PULLUP
|
||||
inverted: false
|
||||
id: fan1_rpm
|
||||
name: "${fan1_rpm_name}"
|
||||
unit_of_measurement: "rpm"
|
||||
accuracy_decimals: 0
|
||||
update_interval: 10s
|
||||
count_mode:
|
||||
rising_edge: DISABLE
|
||||
falling_edge: INCREMENT
|
||||
filters:
|
||||
# pulse_counter is pulses/minute; typical PC fan tach = 2 pulses per revolution
|
||||
- lambda: |-
|
||||
return x / (float) ${fan_pulses_per_rev};
|
||||
- clamp:
|
||||
min_value: 0
|
||||
max_value: 20000
|
||||
|
||||
# Fan2 tach pulses on GPIO3 (UART RX pin)
|
||||
- platform: pulse_counter
|
||||
pin:
|
||||
number: GPIO3
|
||||
mode: INPUT_PULLUP
|
||||
inverted: false
|
||||
id: fan2_rpm
|
||||
name: "${fan2_rpm_name}"
|
||||
unit_of_measurement: "rpm"
|
||||
accuracy_decimals: 0
|
||||
update_interval: 10s
|
||||
count_mode:
|
||||
rising_edge: DISABLE
|
||||
falling_edge: INCREMENT
|
||||
filters:
|
||||
- lambda: |-
|
||||
return x / (float) ${fan_pulses_per_rev};
|
||||
- clamp:
|
||||
min_value: 0
|
||||
max_value: 20000
|
||||
|
||||
# Max temperature sensors (derived from cupboard_temp)
|
||||
- platform: template
|
||||
name: "Data Cupboard Max Temp Today"
|
||||
id: max_temp_today
|
||||
unit_of_measurement: "degC"
|
||||
accuracy_decimals: 1
|
||||
update_interval: 60s
|
||||
lambda: |-
|
||||
return id(g_max_temp_today);
|
||||
|
||||
- platform: template
|
||||
name: "Data Cupboard Max Temp Yesterday"
|
||||
id: max_temp_yesterday
|
||||
unit_of_measurement: "degC"
|
||||
accuracy_decimals: 1
|
||||
update_interval: 60s
|
||||
lambda: |-
|
||||
return id(g_max_temp_yesterday);
|
||||
|
||||
- platform: template
|
||||
name: "Data Cupboard Max Temp This Month"
|
||||
id: max_temp_month
|
||||
unit_of_measurement: "degC"
|
||||
accuracy_decimals: 1
|
||||
update_interval: 60s
|
||||
lambda: |-
|
||||
return id(g_max_temp_month);
|
||||
|
||||
- platform: template
|
||||
name: "Data Cupboard Max Temp Last Month"
|
||||
id: max_temp_last_month
|
||||
unit_of_measurement: "degC"
|
||||
accuracy_decimals: 1
|
||||
update_interval: 60s
|
||||
lambda: |-
|
||||
return id(g_max_temp_last_month);
|
||||
|
||||
- platform: template
|
||||
name: "Data Cupboard Max Temp This Year"
|
||||
id: max_temp_year
|
||||
unit_of_measurement: "degC"
|
||||
accuracy_decimals: 1
|
||||
update_interval: 60s
|
||||
lambda: |-
|
||||
return id(g_max_temp_year);
|
||||
|
||||
##########################################################################################
|
||||
# TEXT SENSOR (Normal/Fault status) - published by script
|
||||
# NOTE: common_sntp also defines its own text_sensors (time status etc).
|
||||
##########################################################################################
|
||||
text_sensor:
|
||||
- platform: template
|
||||
name: "${fan_status_name}"
|
||||
id: fan_status
|
||||
icon: "mdi:fan-alert"
|
||||
|
||||
##########################################################################################
|
||||
# SCRIPTS
|
||||
##########################################################################################
|
||||
script:
|
||||
# Apply fan control logic (override + hysteresis thresholds)
|
||||
- id: apply_fan_control
|
||||
mode: queued
|
||||
then:
|
||||
- lambda: |-
|
||||
const float t = id(cupboard_temp).state;
|
||||
if (isnan(t)) {
|
||||
return;
|
||||
}
|
||||
|
||||
float on_t = id(temp_on_threshold).state;
|
||||
float off_t = id(temp_off_threshold).state;
|
||||
|
||||
// Guard against bad settings (keep at least 0.5C hysteresis)
|
||||
if (off_t >= on_t) {
|
||||
off_t = on_t - 0.5f;
|
||||
}
|
||||
|
||||
if (id(override_fans_on).state) {
|
||||
if (!id(fan_relay).state) {
|
||||
id(fan_relay).turn_on();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Hysteresis-based control
|
||||
if (!id(fan_relay).state) {
|
||||
if (t >= on_t) {
|
||||
id(fan_relay).turn_on();
|
||||
}
|
||||
} else {
|
||||
if (t <= off_t) {
|
||||
id(fan_relay).turn_off();
|
||||
}
|
||||
}
|
||||
|
||||
# Evaluate and publish fan fault and status
|
||||
- id: evaluate_fan_fault
|
||||
mode: restart
|
||||
then:
|
||||
- lambda: |-
|
||||
const uint32_t now_ms = millis();
|
||||
|
||||
// Grace window after boot / relay change to allow RPM to stabilize
|
||||
const bool in_grace =
|
||||
(now_ms - id(boot_ms) < 20000) ||
|
||||
(now_ms - id(relay_last_change_ms) < 15000);
|
||||
|
||||
if (in_grace) {
|
||||
id(fan_fault).publish_state(false);
|
||||
id(fan_status).publish_state("Normal");
|
||||
return;
|
||||
}
|
||||
|
||||
const float r1 = id(fan1_rpm).state;
|
||||
const float r2 = id(fan2_rpm).state;
|
||||
|
||||
if (isnan(r1) || isnan(r2)) {
|
||||
id(fan_fault).publish_state(true);
|
||||
id(fan_status).publish_state("Fault");
|
||||
return;
|
||||
}
|
||||
|
||||
const float req = id(fan_relay).state ? ${rpm_min_when_relay_on} : ${rpm_min_when_relay_off};
|
||||
|
||||
// Requirement as requested:
|
||||
// - Relay OFF => both fans must be > 1800 RPM
|
||||
// - Relay ON => both fans must be > 3000 RPM
|
||||
const bool fault = (r1 < req) || (r2 < req);
|
||||
|
||||
id(fan_fault).publish_state(fault);
|
||||
id(fan_status).publish_state(fault ? "Fault" : "Normal");
|
||||
|
||||
# Update daily/monthly/yearly maxima based on current temperature and current date
|
||||
- id: update_temp_maxima
|
||||
mode: queued
|
||||
then:
|
||||
- lambda: |-
|
||||
const float t = id(cupboard_temp).state;
|
||||
if (isnan(t)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto now = id(sntp_time).now();
|
||||
if (!now.is_valid()) {
|
||||
// Time not valid yet: still track as rolling maxima
|
||||
if (t > id(g_max_temp_today)) id(g_max_temp_today) = t;
|
||||
if (t > id(g_max_temp_month)) id(g_max_temp_month) = t;
|
||||
if (t > id(g_max_temp_year)) id(g_max_temp_year) = t;
|
||||
} else {
|
||||
// Initialize date tracking on first valid time
|
||||
if (id(g_last_year) == 0) {
|
||||
id(g_last_year) = now.year;
|
||||
id(g_last_month) = now.month;
|
||||
id(g_last_day) = now.day_of_month;
|
||||
|
||||
id(g_max_temp_today) = t;
|
||||
id(g_max_temp_yesterday) = t;
|
||||
id(g_max_temp_month) = t;
|
||||
id(g_max_temp_last_month) = t;
|
||||
id(g_max_temp_year) = t;
|
||||
} else {
|
||||
// Year rollover
|
||||
if (now.year != id(g_last_year)) {
|
||||
id(g_last_year) = now.year;
|
||||
id(g_max_temp_year) = t;
|
||||
}
|
||||
|
||||
// Month rollover
|
||||
if (now.month != id(g_last_month)) {
|
||||
id(g_max_temp_last_month) = id(g_max_temp_month);
|
||||
id(g_max_temp_month) = t;
|
||||
id(g_last_month) = now.month;
|
||||
}
|
||||
|
||||
// Day rollover
|
||||
if (now.day_of_month != id(g_last_day)) {
|
||||
id(g_max_temp_yesterday) = id(g_max_temp_today);
|
||||
id(g_max_temp_today) = t;
|
||||
id(g_last_day) = now.day_of_month;
|
||||
}
|
||||
|
||||
// Update running maxima
|
||||
if (t > id(g_max_temp_today)) id(g_max_temp_today) = t;
|
||||
if (t > id(g_max_temp_month)) id(g_max_temp_month) = t;
|
||||
if (t > id(g_max_temp_year)) id(g_max_temp_year) = t;
|
||||
}
|
||||
}
|
||||
|
||||
// Publish template sensors promptly
|
||||
id(max_temp_today).publish_state(id(g_max_temp_today));
|
||||
id(max_temp_yesterday).publish_state(id(g_max_temp_yesterday));
|
||||
id(max_temp_month).publish_state(id(g_max_temp_month));
|
||||
id(max_temp_last_month).publish_state(id(g_max_temp_last_month));
|
||||
id(max_temp_year).publish_state(id(g_max_temp_year));
|
||||
|
||||
##########################################################################################
|
||||
# MQTT COMMANDS (device-specific)
|
||||
##########################################################################################
|
||||
mqtt:
|
||||
on_message:
|
||||
# Relay control
|
||||
- topic: "${mqtt_main_topic}/relay/set"
|
||||
payload: "ON"
|
||||
then:
|
||||
- switch.turn_on: fan_relay
|
||||
- topic: "${mqtt_main_topic}/relay/set"
|
||||
payload: "OFF"
|
||||
then:
|
||||
- switch.turn_off: fan_relay
|
||||
|
||||
# Override control
|
||||
- topic: "${mqtt_main_topic}/override/set"
|
||||
payload: "ON"
|
||||
then:
|
||||
- switch.turn_on: override_fans_on
|
||||
- topic: "${mqtt_main_topic}/override/set"
|
||||
payload: "OFF"
|
||||
then:
|
||||
- switch.turn_off: override_fans_on
|
||||
Reference in New Issue
Block a user