Files
zorruno-homeassistant/esphome/esp-astroclockstepper1.yaml
T
2026-06-22 15:29:47 +12:00

865 lines
26 KiB
YAML

#:########################################################################################:#
# TITLE: ASTRONOMICAL CLOCK STEPPER
# zorruno.com layout v1.1 2026
#:########################################################################################:#
# REPO:
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-astroclockstepper.yaml
#:########################################################################################:#
# VERSIONS:
# V1.1 2026-06-22 Added onboard D1 Mini LED flash on each motor step
# V1.0 2026-06-22 Initial Version
#:########################################################################################:#
# HARDWARE:
# ESP8266 D1 Mini Clone
# 5V 28BYJ-48 Stepper Motor
# Keyestudio / ULN2003 style 5V stepper driver board
#
# Stepper Driver:
# - IN1: GPIO14 / D5
# - IN2: GPIO12 / D6
# - IN3: GPIO13 / D7
# - IN4: GPIO15 / D8
# - VCC: 5V motor supply
# - GND: Common GND with ESP8266
#
# Physical Buttons:
# - Fast Forward: GPIO5 / D1
# - Slow Forward: GPIO4 / D2
# - Fast Reverse: GPIO0 / D3
# - Slow Reverse: GPIO16 / D0
#
# Button board:
# - Uses built-in 103 pull-up resistors, 10k ohm
# - Buttons should pull the GPIO pin to GND when pressed
# - Pull-ups must go to 3.3V, not 5V
#
# Onboard Step LED:
# - GPIO2 / D4
# - Active-low on most D1 Mini boards
#:########################################################################################:#
# OPERATION NOTES:
# - Normal rotation starts automatically after boot.
# - Default rotation is one full revolution every 365.2422 days.
# - Default step count is 4096 steps per full revolution.
# - Normal movement is extremely slow, about one step every 2.14 hours.
# - Motor coils are powered only while stepping, then released.
# - No holding torque is used.
# - Web template switches mimic held physical buttons.
# - Manual movement only applies while a physical button or web switch is held/on.
# - Position counter wraps back to zero after one full rotation.
# - Onboard D1 Mini LED flashes briefly every time a motor step is issued.
#:########################################################################################:#
# OFFLINE NOTES:
# a) Home Assistant OFFLINE, but Network and MQTT ONLINE
# - Device remains controllable via local web portal and MQTT commands
# - HA entities unavailable until HA returns
# b) MQTT OFFLINE, but WiFi/Network and HA API ONLINE
# - Device remains controllable from Home Assistant via API
# - Local web portal remains available
# c) Entire WiFi/Network OFFLINE
# - No HA API, MQTT, or web control available
# - Physical buttons continue to work
# - Normal motor timing continues from device uptime
# d) Device power loss / reboot
# - V1 does not restore exact astronomical position after reboot
# - Re-align manually using the web or physical button controls
#:########################################################################################:#
#:########################################################################################:#
# SUBSTITUTIONS: 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-astroclockstepper1"
friendly_name: "Astronomical Clock Stepper"
description_comment: "Astronomical Clock Stepper :: ESP8266 D1 Mini with 28BYJ-48 Stepper Motor (Layout V1.1)"
device_area: "Standalone"
# Project Naming
project_name: "zorruno.Astronomical Clock Stepper"
project_version: "v1.1"
# Passwords & Secrets (Unfortunately, you cannot use substitutions inside secret names)
api_key: !secret esp-api_key
ota_pass: !secret esp-ota_pass
static_ip_address: !secret esp-astroclockstepper1_ip
# If we are changing IP addresses, you must update the current IP address here, otherwise it remains
# Don't forget to switch it back when changed.
current_ip_address: ${static_ip_address}
# MQTT Topics
mqtt_command_main_topic: !secret mqtt_command_main_topic
mqtt_status_main_topic: !secret mqtt_status_main_topic
mqtt_device_name: "astro-clock-stepper1"
mqtt_command_topic: "${mqtt_command_main_topic}/${mqtt_device_name}"
mqtt_status_topic: "${mqtt_status_main_topic}/${mqtt_device_name}"
# Device Settings
log_level: "INFO"
sensor_update_interval: "1s"
calculated_sensor_update_interval: "60s"
# Astronomical Clock Settings
rotation_days: "365.2422"
steps_per_rotation: "4096"
normal_direction_name: "Clockwise"
# Use 1 for normal direction.
# Use -1 if the motor rotates the wrong way.
normal_direction_multiplier: "1"
# Manual Movement Speeds
manual_fast_rotation_seconds: "30"
manual_slow_rotation_seconds: "300"
# Coil Release
coil_release_ms: "500"
# Step LED
onboard_step_led_pin: GPIO2
step_led_flash_ms: "25"
# Stepper Output Pins
stepper_in1_pin: GPIO14
stepper_in2_pin: GPIO12
stepper_in3_pin: GPIO13
stepper_in4_pin: GPIO15
# Physical Button Input Pins
button_fast_forward_pin: GPIO5
button_slow_forward_pin: GPIO4
button_fast_reverse_pin: GPIO0
button_slow_reverse_pin: GPIO16
# Entity Naming
web_fast_forward_name: "Web Fast Forward"
web_slow_forward_name: "Web Slow Forward"
web_fast_reverse_name: "Web Fast Reverse"
web_slow_reverse_name: "Web Slow Reverse"
physical_fast_forward_name: "Physical Fast Forward"
physical_slow_forward_name: "Physical Slow Forward"
physical_fast_reverse_name: "Physical Fast Reverse"
physical_slow_reverse_name: "Physical Slow Reverse"
zero_position_name: "Zero Position Counter"
#:########################################################################################:#
# PACKAGES: Included Common Packages
# https://esphome.io/components/packages.html
#:########################################################################################:#
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}"
local_current_ip_address: "${current_ip_address}"
#### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include
file: common/api_common.yaml
#file: common/api_common_noencryption.yaml
vars:
local_api_key: "${api_key}"
#### MQTT ####
common_mqtt: !include
file: common/mqtt_common.yaml
vars:
local_device_name: "${device_name}"
#### WEB PORTAL ####
common_webportal: !include common/webportal_common.yaml
#### SNTP (Only use if you want/need accurate timeclocks) ####
#common_sntp: !include common/sntp_common.yaml
#### DIAGNOSTICS Sensors ####
diag_basic: !include common/include_basic_diag_sensors.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
#:########################################################################################:#
# ESPHOME:
# 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:
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."
args:
- '"${rotation_days}"'
#:########################################################################################:#
# ESP PLATFORM AND FRAMEWORK:
# https://esphome.io/components/esp8266.html
#:########################################################################################:#
esp8266:
board: d1_mini
restore_from_flash: true
preferences:
flash_write_interval: 5min
mdns:
disabled: false
#:########################################################################################:#
# LOGGING:
# https://esphome.io/components/logger.html
#:########################################################################################:#
logger:
level: "${log_level}"
baud_rate: 0
#:########################################################################################:#
# GLOBALS:
# Internal position, timing, mode, and held-button state
# https://esphome.io/components/globals.html
#:########################################################################################:#
globals:
- id: current_position_step
type: int
restore_value: no
initial_value: "0"
- id: current_phase_index
type: int
restore_value: no
initial_value: "0"
- id: last_loop_ms
type: uint32_t
restore_value: no
initial_value: "0"
- id: last_step_ms
type: uint32_t
restore_value: no
initial_value: "0"
- id: step_accumulator_ms
type: double
restore_value: no
initial_value: "0.0"
- id: coils_are_on
type: bool
restore_value: no
initial_value: "false"
- id: step_led_is_on
type: bool
restore_value: no
initial_value: "false"
- id: step_led_started_ms
type: uint32_t
restore_value: no
initial_value: "0"
# 0 = Normal
# 1 = Fast Forward
# 2 = Slow Forward
# 3 = Fast Reverse
# 4 = Slow Reverse
- id: current_motion_mode
type: int
restore_value: no
initial_value: "0"
# Physical Button States
- id: physical_fast_forward_active
type: bool
restore_value: no
initial_value: "false"
- id: physical_slow_forward_active
type: bool
restore_value: no
initial_value: "false"
- id: physical_fast_reverse_active
type: bool
restore_value: no
initial_value: "false"
- id: physical_slow_reverse_active
type: bool
restore_value: no
initial_value: "false"
# Web Button States
- id: web_fast_forward_active
type: bool
restore_value: no
initial_value: "false"
- id: web_slow_forward_active
type: bool
restore_value: no
initial_value: "false"
- id: web_fast_reverse_active
type: bool
restore_value: no
initial_value: "false"
- id: web_slow_reverse_active
type: bool
restore_value: no
initial_value: "false"
#:########################################################################################:#
# OUTPUT:
# Stepper driver GPIO outputs and onboard step LED
# https://esphome.io/components/output/gpio.html
#:########################################################################################:#
output:
- platform: gpio
id: stepper_in1
pin: ${stepper_in1_pin}
- platform: gpio
id: stepper_in2
pin: ${stepper_in2_pin}
- platform: gpio
id: stepper_in3
pin: ${stepper_in3_pin}
- platform: gpio
id: stepper_in4
pin: ${stepper_in4_pin}
# D1 Mini onboard LED is usually GPIO2 / D4 and active-low
- platform: gpio
id: onboard_step_led
pin:
number: ${onboard_step_led_pin}
inverted: true
#:########################################################################################:#
# SWITCH:
# Web held-button controls
# https://esphome.io/components/switch/template.html
#:########################################################################################:#
switch:
- platform: template
name: "${web_fast_forward_name}"
id: web_fast_forward_switch
icon: mdi:fast-forward
optimistic: true
restore_mode: ALWAYS_OFF
turn_on_action:
- switch.turn_off: web_slow_forward_switch
- switch.turn_off: web_fast_reverse_switch
- switch.turn_off: web_slow_reverse_switch
- globals.set:
id: web_fast_forward_active
value: "true"
turn_off_action:
- globals.set:
id: web_fast_forward_active
value: "false"
- platform: template
name: "${web_slow_forward_name}"
id: web_slow_forward_switch
icon: mdi:play-speed
optimistic: true
restore_mode: ALWAYS_OFF
turn_on_action:
- switch.turn_off: web_fast_forward_switch
- switch.turn_off: web_fast_reverse_switch
- switch.turn_off: web_slow_reverse_switch
- globals.set:
id: web_slow_forward_active
value: "true"
turn_off_action:
- globals.set:
id: web_slow_forward_active
value: "false"
- platform: template
name: "${web_fast_reverse_name}"
id: web_fast_reverse_switch
icon: mdi:rewind
optimistic: true
restore_mode: ALWAYS_OFF
turn_on_action:
- switch.turn_off: web_fast_forward_switch
- switch.turn_off: web_slow_forward_switch
- switch.turn_off: web_slow_reverse_switch
- globals.set:
id: web_fast_reverse_active
value: "true"
turn_off_action:
- globals.set:
id: web_fast_reverse_active
value: "false"
- platform: template
name: "${web_slow_reverse_name}"
id: web_slow_reverse_switch
icon: mdi:rewind-outline
optimistic: true
restore_mode: ALWAYS_OFF
turn_on_action:
- switch.turn_off: web_fast_forward_switch
- switch.turn_off: web_slow_forward_switch
- switch.turn_off: web_fast_reverse_switch
- globals.set:
id: web_slow_reverse_active
value: "true"
turn_off_action:
- globals.set:
id: web_slow_reverse_active
value: "false"
#:########################################################################################:#
# BINARY SENSORS:
# Physical button inputs using external 103 / 10k pull-ups
# https://esphome.io/components/binary_sensor/gpio.html
#:########################################################################################:#
binary_sensor:
- platform: gpio
name: "${physical_fast_forward_name}"
id: physical_fast_forward_button
pin:
number: ${button_fast_forward_pin}
mode: INPUT
inverted: true
filters:
- delayed_on: 20ms
- delayed_off: 20ms
on_press:
- globals.set:
id: physical_fast_forward_active
value: "true"
on_release:
- globals.set:
id: physical_fast_forward_active
value: "false"
- platform: gpio
name: "${physical_slow_forward_name}"
id: physical_slow_forward_button
pin:
number: ${button_slow_forward_pin}
mode: INPUT
inverted: true
filters:
- delayed_on: 20ms
- delayed_off: 20ms
on_press:
- globals.set:
id: physical_slow_forward_active
value: "true"
on_release:
- globals.set:
id: physical_slow_forward_active
value: "false"
- platform: gpio
name: "${physical_fast_reverse_name}"
id: physical_fast_reverse_button
pin:
number: ${button_fast_reverse_pin}
mode: INPUT
inverted: true
filters:
- delayed_on: 20ms
- delayed_off: 20ms
on_press:
- globals.set:
id: physical_fast_reverse_active
value: "true"
on_release:
- globals.set:
id: physical_fast_reverse_active
value: "false"
- platform: gpio
name: "${physical_slow_reverse_name}"
id: physical_slow_reverse_button
pin:
number: ${button_slow_reverse_pin}
mode: INPUT
inverted: true
filters:
- delayed_on: 20ms
- delayed_off: 20ms
on_press:
- globals.set:
id: physical_slow_reverse_active
value: "true"
on_release:
- globals.set:
id: physical_slow_reverse_active
value: "false"
#:########################################################################################:#
# BUTTON:
# Utility buttons
# https://esphome.io/components/button/template.html
#:########################################################################################:#
button:
- platform: template
name: "${zero_position_name}"
id: zero_position_counter_button
icon: mdi:counter
on_press:
- globals.set:
id: current_position_step
value: "0"
- globals.set:
id: current_phase_index
value: "0"
- globals.set:
id: step_accumulator_ms
value: "0.0"
- logger.log:
level: INFO
format: "Position counter zeroed."
#:########################################################################################:#
# SENSOR:
# Position and calculated movement sensors
# https://esphome.io/components/sensor/template.html
#:########################################################################################:#
sensor:
- platform: template
name: "Position Days"
id: position_days_sensor
icon: mdi:calendar-clock
unit_of_measurement: "days"
accuracy_decimals: 4
update_interval: "${sensor_update_interval}"
lambda: |-
return ((double) id(current_position_step) * ${rotation_days}) / ${steps_per_rotation};
- platform: template
name: "Position Step"
id: position_step_sensor
icon: mdi:counter
accuracy_decimals: 0
update_interval: "${sensor_update_interval}"
lambda: |-
return id(current_position_step);
- platform: template
name: "Days Per Step"
id: days_per_step_sensor
icon: mdi:calendar-arrow-right
unit_of_measurement: "days/step"
accuracy_decimals: 8
update_interval: "${calculated_sensor_update_interval}"
lambda: |-
return ${rotation_days} / ${steps_per_rotation};
- platform: template
name: "Hours Per Step"
id: hours_per_step_sensor
icon: mdi:timer-outline
unit_of_measurement: "h/step"
accuracy_decimals: 8
update_interval: "${calculated_sensor_update_interval}"
lambda: |-
return (${rotation_days} * 24.0) / ${steps_per_rotation};
- platform: template
name: "Normal Step Interval"
id: normal_step_interval_sensor
icon: mdi:timer-cog-outline
unit_of_measurement: "s/step"
accuracy_decimals: 4
update_interval: "${calculated_sensor_update_interval}"
lambda: |-
return (${rotation_days} * 24.0 * 60.0 * 60.0) / ${steps_per_rotation};
#:########################################################################################:#
# TEXT SENSOR:
# Human-readable status sensors
# https://esphome.io/components/text_sensor/template.html
#:########################################################################################:#
text_sensor:
- platform: template
name: "Motion Mode"
id: motion_mode_text_sensor
icon: mdi:rotate-3d-variant
update_interval: "${sensor_update_interval}"
lambda: |-
switch (id(current_motion_mode)) {
case 1:
return {"Fast Forward"};
case 2:
return {"Slow Forward"};
case 3:
return {"Fast Reverse"};
case 4:
return {"Slow Reverse"};
case 0:
default:
return {"Normal"};
}
- platform: template
name: "Normal Direction"
id: normal_direction_text_sensor
icon: mdi:rotate-right
update_interval: "${calculated_sensor_update_interval}"
lambda: |-
return {"${normal_direction_name}"};
#:########################################################################################:#
# INTERVAL:
# Stepper scheduler
# https://esphome.io/components/interval.html
#:########################################################################################:#
interval:
- interval: 5ms
then:
- lambda: |-
const int STEPS_PER_ROTATION = ${steps_per_rotation};
const int NORMAL_DIRECTION_MULTIPLIER = ${normal_direction_multiplier};
const double NORMAL_STEP_INTERVAL_MS =
(${rotation_days} * 24.0 * 60.0 * 60.0 * 1000.0) / ${steps_per_rotation};
const double FAST_STEP_INTERVAL_MS =
(${manual_fast_rotation_seconds} * 1000.0) / ${steps_per_rotation};
const double SLOW_STEP_INTERVAL_MS =
(${manual_slow_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};
auto all_coils_off = [&]() {
id(stepper_in1).turn_off();
id(stepper_in2).turn_off();
id(stepper_in3).turn_off();
id(stepper_in4).turn_off();
id(coils_are_on) = false;
};
auto flash_step_led = [&](uint32_t now_ms) {
id(onboard_step_led).turn_on();
id(step_led_started_ms) = now_ms;
id(step_led_is_on) = true;
};
auto set_coils_for_phase = [&](int phase) {
const bool sequence[8][4] = {
{true, false, false, false},
{true, true, false, false},
{false, true, false, false},
{false, true, true, false},
{false, false, true, false},
{false, false, true, true },
{false, false, false, true },
{true, false, false, true }
};
phase = ((phase % 8) + 8) % 8;
if (sequence[phase][0]) {
id(stepper_in1).turn_on();
} else {
id(stepper_in1).turn_off();
}
if (sequence[phase][1]) {
id(stepper_in2).turn_on();
} else {
id(stepper_in2).turn_off();
}
if (sequence[phase][2]) {
id(stepper_in3).turn_on();
} else {
id(stepper_in3).turn_off();
}
if (sequence[phase][3]) {
id(stepper_in4).turn_on();
} else {
id(stepper_in4).turn_off();
}
id(coils_are_on) = true;
};
auto do_one_step = [&](int direction, uint32_t now_ms) {
if (direction >= 0) {
id(current_phase_index) = (id(current_phase_index) + 1) % 8;
id(current_position_step) = (id(current_position_step) + 1) % STEPS_PER_ROTATION;
} else {
id(current_phase_index) = (id(current_phase_index) + 7) % 8;
id(current_position_step) = (id(current_position_step) + STEPS_PER_ROTATION - 1) % STEPS_PER_ROTATION;
}
set_coils_for_phase(id(current_phase_index));
id(last_step_ms) = now_ms;
flash_step_led(now_ms);
};
const bool fast_forward_active =
id(physical_fast_forward_active) || id(web_fast_forward_active);
const bool slow_forward_active =
id(physical_slow_forward_active) || id(web_slow_forward_active);
const bool fast_reverse_active =
id(physical_fast_reverse_active) || id(web_fast_reverse_active);
const bool slow_reverse_active =
id(physical_slow_reverse_active) || id(web_slow_reverse_active);
int selected_mode = 0;
int selected_direction = NORMAL_DIRECTION_MULTIPLIER;
double selected_step_interval_ms = NORMAL_STEP_INTERVAL_MS;
if (fast_forward_active) {
selected_mode = 1;
selected_direction = NORMAL_DIRECTION_MULTIPLIER;
selected_step_interval_ms = FAST_STEP_INTERVAL_MS;
} else if (slow_forward_active) {
selected_mode = 2;
selected_direction = NORMAL_DIRECTION_MULTIPLIER;
selected_step_interval_ms = SLOW_STEP_INTERVAL_MS;
} else if (fast_reverse_active) {
selected_mode = 3;
selected_direction = -NORMAL_DIRECTION_MULTIPLIER;
selected_step_interval_ms = FAST_STEP_INTERVAL_MS;
} else if (slow_reverse_active) {
selected_mode = 4;
selected_direction = -NORMAL_DIRECTION_MULTIPLIER;
selected_step_interval_ms = SLOW_STEP_INTERVAL_MS;
}
const uint32_t now_ms = millis();
if (id(last_loop_ms) == 0) {
id(last_loop_ms) = now_ms;
id(current_motion_mode) = selected_mode;
id(step_accumulator_ms) = 0.0;
return;
}
const uint32_t elapsed_ms = now_ms - id(last_loop_ms);
id(last_loop_ms) = now_ms;
if (selected_mode != id(current_motion_mode)) {
id(current_motion_mode) = selected_mode;
id(step_accumulator_ms) = 0.0;
}
id(step_accumulator_ms) += (double) elapsed_ms;
if (id(step_accumulator_ms) >= selected_step_interval_ms) {
do_one_step(selected_direction, now_ms);
id(step_accumulator_ms) -= selected_step_interval_ms;
// If the loop was badly delayed, avoid trying to catch up with a burst
// of very fast steps that the motor may not physically follow.
if (id(step_accumulator_ms) > selected_step_interval_ms) {
id(step_accumulator_ms) = 0.0;
}
}
if (
id(coils_are_on) &&
selected_mode == 0 &&
((uint32_t) (now_ms - id(last_step_ms)) >= COIL_RELEASE_MS)
) {
all_coils_off();
}
if (
id(step_led_is_on) &&
((uint32_t) (now_ms - id(step_led_started_ms)) >= STEP_LED_FLASH_MS)
) {
id(onboard_step_led).turn_off();
id(step_led_is_on) = false;
}
#:########################################################################################:#
# 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