Edit esp-astroclockstepper1.yaml

This commit is contained in:
ESPHome Device Builder
2026-06-28 17:53:35 +12:00
parent 7665dc5242
commit 76e9d7c68e
+362 -18
View File
@@ -6,6 +6,7 @@
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-astroclockstepper.yaml
#:########################################################################################:#
# VERSIONS:
# V1.3 2026-06-22 Added SNTP year auto-positioning button and restored boot auto-position switch
# V1.2 2026-06-22 Added DHCP IPv4 address flash sequence on onboard LED after WiFi connects
# V1.1 2026-06-22 Added onboard D1 Mini LED flash on each motor step
# V1.0 2026-06-22 Initial Version
@@ -52,6 +53,10 @@
# - After WiFi connects, the onboard LED flashes the DHCP IPv4 address.
# - IPv4 flash sequence is decimal digit based.
# - A digit of 0 is represented as 10 flashes.
# - SNTP is used for current year-position calculation.
# - User should manually align the dial to year-zero, then press Zero Position Counter.
# - Auto Position Now moves forward only to the current calculated year position.
# - Auto Position On Boot is off by default, restored on power loss, and runs after boot/IP flash.
#:########################################################################################:#
# OFFLINE NOTES:
# a) Home Assistant OFFLINE, but Network ONLINE
@@ -62,9 +67,14 @@
# - Physical buttons continue to work
# - Normal motor timing continues from device uptime
# - Fallback AP / captive portal should become available from network_common_dhcp.yaml
# c) Device power loss / reboot
# - V1 does not restore exact astronomical position after reboot
# - Re-align manually using the web or physical button controls
# c) SNTP NOT SYNCED
# - Normal motor timing continues
# - Manual controls continue
# - Auto Position Now will not run until SNTP time is valid
# d) Device power loss / reboot
# - Current position counter is restored from flash
# - Auto Position On Boot setting is restored from flash
# - Physical position is assumed valid because the motor housing/dial is fixed
#:########################################################################################:#
@@ -81,7 +91,7 @@ substitutions:
# Project Naming
project_name: "zorruno.Astronomical Clock Stepper"
project_version: "v1.2"
project_version: "v1.3"
# Passwords & Secrets (Unfortunately, you cannot use substitutions inside secret names)
api_key: !secret esp-api_key
@@ -92,6 +102,9 @@ substitutions:
sensor_update_interval: "1s"
calculated_sensor_update_interval: "60s"
# Time Settings
timezone_name: "Pacific/Auckland"
# Astronomical Clock Settings
rotation_days: "365.2422"
steps_per_rotation: "4096"
@@ -105,6 +118,10 @@ substitutions:
manual_fast_rotation_seconds: "30"
manual_slow_rotation_seconds: "300"
# Auto Position Speed
# Default is the same as Slow Forward: one full rotation every 300 seconds.
auto_position_rotation_seconds: "300"
# Coil Release
coil_release_ms: "500"
@@ -121,9 +138,9 @@ substitutions:
# Suggested human-countable timing
ip_flash_on_ms: "200"
ip_flash_between_flash_gap_ms: "450"
ip_flash_digit_gap_ms: "1000"
ip_flash_octet_gap_ms: "4500"
ip_flash_repeat_gap_ms: "10000"
ip_flash_digit_gap_ms: "1500"
ip_flash_octet_gap_ms: "3500"
ip_flash_repeat_gap_ms: "8000"
# Stepper Output Pins
stepper_in1_pin: GPIO14
@@ -151,6 +168,11 @@ substitutions:
zero_position_name: "Zero Position Counter"
replay_ip_flash_name: "Replay IP Address Flash"
auto_position_now_name: "Auto Position Now"
auto_position_on_boot_name: "Auto Position On Boot"
auto_position_active_name: "Auto Position Active"
sntp_time_valid_name: "SNTP Time Valid"
#:########################################################################################:#
# PACKAGES: Included Common Packages
@@ -181,13 +203,8 @@ packages:
#common_webportal: !include common/webportal_common.yaml
common_webportal: !include common/webportal_common_nopasswd.yaml
#### SNTP (Only use if you want/need accurate timeclocks) ####
#common_sntp: !include common/sntp_common.yaml
common_sntp: !include common/sntp_common_public.yaml
#### DIAGNOSTICS Sensors ####
diag_basic: !include common/include_basic_diag_sensors.yaml
diag_more: !include common/include_more_diag_sensors_nomqtt.yaml
#diag_more: !include common/include_more_diag_sensors.yaml
#diag_debug: !include common/include_debug_diag_sensors.yaml
#diag_resetcount: !include common/include_resetcount_diag_sensors.yaml
@@ -210,6 +227,7 @@ esphome:
priority: -100
then:
- output.turn_off: onboard_step_led
- logger.log:
level: INFO
format: "Astronomical Clock Stepper started. Normal rotation is %s days per full revolution."
@@ -221,9 +239,38 @@ esphome:
- wait_until:
condition:
wifi.connected:
- delay: 3s
- script.execute: start_ip_flash_script
# Give the IP flash state machine time to start.
- delay: 100ms
# Wait for the boot IP flash sequence to finish before optional auto-positioning.
- wait_until:
condition:
lambda: |-
return !id(ip_flash_active);
- if:
condition:
switch.is_on: auto_position_on_boot_switch
then:
- globals.set:
id: auto_position_on_boot_pending
value: "true"
- if:
condition:
lambda: |-
return id(sntp_time).now().is_valid();
then:
- globals.set:
id: auto_position_on_boot_pending
value: "false"
- script.execute: start_auto_position_script
#:########################################################################################:#
# ESP PLATFORM AND FRAMEWORK:
@@ -234,7 +281,7 @@ esp8266:
restore_from_flash: true
preferences:
flash_write_interval: 5min
flash_write_interval: 1min
mdns:
disabled: false
@@ -249,20 +296,42 @@ logger:
baud_rate: 0
#:########################################################################################:#
# TIME:
# SNTP time source for year-position calculation
# https://esphome.io/components/time/sntp.html
#:########################################################################################:#
time:
- platform: sntp
id: sntp_time
timezone: "${timezone_name}"
on_time_sync:
then:
- if:
condition:
lambda: |-
return id(auto_position_on_boot_pending) && id(auto_position_on_boot_switch).state;
then:
- globals.set:
id: auto_position_on_boot_pending
value: "false"
- script.execute: start_auto_position_script
#:########################################################################################:#
# GLOBALS:
# Internal position, timing, mode, held-button state, and IP flash state
# Internal position, timing, mode, held-button state, IP flash state, and auto-position state
# https://esphome.io/components/globals.html
#:########################################################################################:#
globals:
- id: current_position_step
type: int
restore_value: no
restore_value: yes
initial_value: "0"
- id: current_phase_index
type: int
restore_value: no
restore_value: yes
initial_value: "0"
- id: last_loop_ms
@@ -300,6 +369,7 @@ globals:
# 2 = Slow Forward
# 3 = Fast Reverse
# 4 = Slow Reverse
# 5 = Auto Position
- id: current_motion_mode
type: int
restore_value: no
@@ -403,6 +473,27 @@ globals:
restore_value: no
initial_value: "0"
# Auto Position State
- id: auto_position_active
type: bool
restore_value: no
initial_value: "false"
- id: auto_position_on_boot_pending
type: bool
restore_value: no
initial_value: "false"
- id: auto_position_target_step
type: int
restore_value: no
initial_value: "0"
- id: auto_position_steps_remaining
type: int
restore_value: no
initial_value: "0"
#:########################################################################################:#
# OUTPUT:
@@ -490,10 +581,115 @@ script:
${ip_flash_repeat_count}
);
- id: start_auto_position_script
mode: restart
then:
- lambda: |-
const int STEPS_PER_ROTATION = ${steps_per_rotation};
auto now = id(sntp_time).now();
if (!now.is_valid()) {
ESP_LOGW("auto_position", "SNTP time is not valid yet. Auto position cancelled.");
return;
}
const int year = now.year;
const int month = now.month;
const int day_of_month = now.day_of_month;
const bool is_leap_year =
((year % 4 == 0) && (year % 100 != 0)) ||
(year % 400 == 0);
const int days_in_year = is_leap_year ? 366 : 365;
const int days_before_month_common[12] = {
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
};
const int days_before_month_leap[12] = {
0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335
};
int safe_month = month;
if (safe_month < 1) {
safe_month = 1;
}
if (safe_month > 12) {
safe_month = 12;
}
int safe_day_of_month = day_of_month;
if (safe_day_of_month < 1) {
safe_day_of_month = 1;
}
if (safe_day_of_month > 31) {
safe_day_of_month = 31;
}
const int day_of_year =
(is_leap_year ? days_before_month_leap[safe_month - 1] : days_before_month_common[safe_month - 1]) +
safe_day_of_month;
const uint32_t seconds_elapsed_this_year =
((uint32_t) (day_of_year - 1) * 86400UL) +
((uint32_t) now.hour * 3600UL) +
((uint32_t) now.minute * 60UL) +
((uint32_t) now.second);
const double seconds_in_year = (double) days_in_year * 86400.0;
int target_step =
(int) (((double) seconds_elapsed_this_year / seconds_in_year) *
(double) STEPS_PER_ROTATION);
target_step = target_step % STEPS_PER_ROTATION;
int current_step = id(current_position_step) % STEPS_PER_ROTATION;
if (current_step < 0) {
current_step += STEPS_PER_ROTATION;
}
const int steps_to_move =
(target_step - current_step + STEPS_PER_ROTATION) % STEPS_PER_ROTATION;
id(auto_position_target_step) = target_step;
id(auto_position_steps_remaining) = steps_to_move;
if (steps_to_move == 0) {
id(auto_position_active) = false;
ESP_LOGI(
"auto_position",
"Already at correct year position. Year: %d, target step: %d.",
year,
target_step
);
return;
}
id(auto_position_active) = true;
ESP_LOGI(
"auto_position",
"Auto positioning started. Year: %d, day_of_year: %d/%d, target step: %d, current step: %d, moving forward %d steps.",
year,
day_of_year,
days_in_year,
target_step,
current_step,
steps_to_move
);
#:########################################################################################:#
# SWITCH:
# Web held-button controls
# Web held-button controls and restored auto-position boot option
# https://esphome.io/components/switch/template.html
#:########################################################################################:#
switch:
@@ -569,6 +765,13 @@ switch:
id: web_slow_reverse_active
value: "false"
- platform: template
name: "${auto_position_on_boot_name}"
id: auto_position_on_boot_switch
icon: mdi:calendar-sync
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
#:########################################################################################:#
# BINARY SENSORS:
@@ -652,6 +855,20 @@ binary_sensor:
id: physical_slow_reverse_active
value: "false"
- platform: template
name: "${auto_position_active_name}"
id: auto_position_active_sensor
icon: mdi:calendar-sync
lambda: |-
return id(auto_position_active);
- platform: template
name: "${sntp_time_valid_name}"
id: sntp_time_valid_sensor
icon: mdi:clock-check-outline
lambda: |-
return id(sntp_time).now().is_valid();
#:########################################################################################:#
# BUTTON:
@@ -673,6 +890,12 @@ button:
- globals.set:
id: step_accumulator_ms
value: "0.0"
- globals.set:
id: auto_position_active
value: "false"
- globals.set:
id: auto_position_steps_remaining
value: "0"
- logger.log:
level: INFO
format: "Position counter zeroed."
@@ -684,6 +907,13 @@ button:
on_press:
- script.execute: start_ip_flash_script
- platform: template
name: "${auto_position_now_name}"
id: auto_position_now_button
icon: mdi:calendar-clock
on_press:
- script.execute: start_auto_position_script
#:########################################################################################:#
# SENSOR:
@@ -740,6 +970,34 @@ sensor:
lambda: |-
return (${rotation_days} * 24.0 * 60.0 * 60.0) / ${steps_per_rotation};
- platform: template
name: "Auto Position Target Step"
id: auto_position_target_step_sensor
icon: mdi:target
accuracy_decimals: 0
update_interval: "${sensor_update_interval}"
lambda: |-
return id(auto_position_target_step);
- platform: template
name: "Auto Position Steps Remaining"
id: auto_position_steps_remaining_sensor
icon: mdi:counter
accuracy_decimals: 0
update_interval: "${sensor_update_interval}"
lambda: |-
return id(auto_position_steps_remaining);
- platform: template
name: "Auto Position Target Days"
id: auto_position_target_days_sensor
icon: mdi:calendar-clock
unit_of_measurement: "days"
accuracy_decimals: 4
update_interval: "${sensor_update_interval}"
lambda: |-
return ((double) id(auto_position_target_step) * ${rotation_days}) / ${steps_per_rotation};
#:########################################################################################:#
# TEXT SENSOR:
@@ -762,6 +1020,8 @@ text_sensor:
return {"Fast Reverse"};
case 4:
return {"Slow Reverse"};
case 5:
return {"Auto Position"};
case 0:
default:
return {"Normal"};
@@ -797,6 +1057,9 @@ interval:
const double SLOW_STEP_INTERVAL_MS =
(${manual_slow_rotation_seconds} * 1000.0) / ${steps_per_rotation};
const double AUTO_POSITION_STEP_INTERVAL_MS =
(${auto_position_rotation_seconds} * 1000.0) / ${steps_per_rotation};
const uint32_t COIL_RELEASE_MS = ${coil_release_ms};
const uint32_t STEP_LED_FLASH_MS = ${step_led_flash_ms};
@@ -883,6 +1146,18 @@ interval:
const bool slow_reverse_active =
id(physical_slow_reverse_active) || id(web_slow_reverse_active);
const bool any_manual_control_active =
fast_forward_active ||
slow_forward_active ||
fast_reverse_active ||
slow_reverse_active;
if (any_manual_control_active && id(auto_position_active)) {
id(auto_position_active) = false;
id(auto_position_steps_remaining) = 0;
ESP_LOGW("auto_position", "Auto positioning cancelled by manual control.");
}
int selected_mode = 0;
int selected_direction = NORMAL_DIRECTION_MULTIPLIER;
double selected_step_interval_ms = NORMAL_STEP_INTERVAL_MS;
@@ -903,6 +1178,10 @@ interval:
selected_mode = 4;
selected_direction = -NORMAL_DIRECTION_MULTIPLIER;
selected_step_interval_ms = SLOW_STEP_INTERVAL_MS;
} else if (id(auto_position_active)) {
selected_mode = 5;
selected_direction = NORMAL_DIRECTION_MULTIPLIER;
selected_step_interval_ms = AUTO_POSITION_STEP_INTERVAL_MS;
}
const uint32_t now_ms = millis();
@@ -926,6 +1205,23 @@ interval:
if (id(step_accumulator_ms) >= selected_step_interval_ms) {
do_one_step(selected_direction, now_ms);
if (selected_mode == 5 && id(auto_position_active)) {
id(auto_position_steps_remaining)--;
if (id(auto_position_steps_remaining) <= 0) {
id(auto_position_steps_remaining) = 0;
id(auto_position_active) = false;
id(current_motion_mode) = 0;
ESP_LOGI(
"auto_position",
"Auto positioning complete. Current step: %d.",
id(current_position_step)
);
}
}
id(step_accumulator_ms) -= selected_step_interval_ms;
// If the loop was badly delayed, avoid trying to catch up with a burst
@@ -1076,4 +1372,52 @@ interval:
id(onboard_step_led).turn_on();
id(ip_flash_led_is_on) = true;
id(ip_flash_next_action_ms) = now_ms + IP_FLASH_ON_MS;
id(ip_flash_next_action_ms) = now_ms + IP_FLASH_ON_MS;
#:########################################################################################:#
# MQTT:
# Device-specific MQTT command triggers, in addition to common MQTT config
# https://esphome.io/components/mqtt.html
#:########################################################################################:#
#mqtt:
# on_message:
# - topic: "${mqtt_command_topic}/fast_forward/set"
# payload: "ON"
# then:
# - switch.turn_on: web_fast_forward_switch
#
# - topic: "${mqtt_command_topic}/fast_forward/set"
# payload: "OFF"
# then:
# - switch.turn_off: web_fast_forward_switch
#
# - topic: "${mqtt_command_topic}/slow_forward/set"
# payload: "ON"
# then:
# - switch.turn_on: web_slow_forward_switch
#
# - topic: "${mqtt_command_topic}/slow_forward/set"
# payload: "OFF"
# then:
# - switch.turn_off: web_slow_forward_switch
#
# - topic: "${mqtt_command_topic}/fast_reverse/set"
# payload: "ON"
# then:
# - switch.turn_on: web_fast_reverse_switch
#
# - topic: "${mqtt_command_topic}/fast_reverse/set"
# payload: "OFF"
# then:
# - switch.turn_off: web_fast_reverse_switch
#
# - topic: "${mqtt_command_topic}/slow_reverse/set"
# payload: "ON"
# then:
# - switch.turn_on: web_slow_reverse_switch
#
# - topic: "${mqtt_command_topic}/slow_reverse/set"
# payload: "OFF"
# then:
# - switch.turn_off: web_slow_reverse_switch