diff --git a/esphome/esp-astroclockstepper1 copy.old b/esphome/esp-astroclockstepper1 copy.old new file mode 100644 index 0000000..b481505 --- /dev/null +++ b/esphome/esp-astroclockstepper1 copy.old @@ -0,0 +1,1657 @@ +#:########################################################################################:# +# 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.7 2026-06-22 Rebuilt direction model so clockwise always equals forward and position count increase +# V1.6 2026-06-22 Fixed auto-position cycle calculation to use rotation_days / solar-year cycle +# V1.5 2026-06-22 Changed Motor Stop to OFF on boot and added shortest-path auto-positioning +# V1.4 2026-06-22 Added Motor Stop switch, swapped reverse buttons 3/4, and separated auto-position motor direction +# 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 +#:########################################################################################:# +# 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 Reverse / Anti-clockwise: GPIO5 / D1 / Button 1 +# - Slow Reverse / Anti-clockwise: GPIO4 / D2 / Button 2 +# - Slow Forward / Clockwise: GPIO0 / D3 / Button 3 +# - Fast Forward / Clockwise: GPIO16 / D0 / Button 4 +# +# 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 +# - Fast modes are on the two end buttons +# +# Onboard Step LED: +# - GPIO2 / D4 +# - Active-low on most D1 Mini boards +#:########################################################################################:# +# OPERATION NOTES: +# - Normal rotation starts automatically after boot because Motor Stop defaults OFF. +# - Motor Stop is OFF by default after every reboot and is not restored. +# - Motor Stop ON turns off all motor coils and pauses physical position counting. +# - Default rotation is one full revolution clockwise 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. +# - Clockwise physical movement is forward. +# - Forward movement increases Position Step and Position Days. +# - Anti-clockwise physical movement is reverse. +# - Reverse movement decreases Position Step and Position Days. +# - 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. +# - Zero Position Counter sets the current physical pointer location as Jan-zero. +# - Zero Position Counter does not reset the stepper coil phase. +# - Onboard D1 Mini LED flashes briefly every time a motor step is issued. +# - 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. +# - Auto Position Now calculates the target using the configured 365.2422-day rotation cycle. +# - Auto Position Now moves to the current calculated year position by the shortest path. +# - Auto Position can move clockwise or anti-clockwise, whichever is quicker. +# - Auto Position On Boot is off by default, restored on power loss, and runs after boot/IP flash if Motor Stop is OFF. +#:########################################################################################:# +# OFFLINE NOTES: +# a) Home Assistant OFFLINE, but Network ONLINE +# - Device remains controllable via local web portal +# - HA entities unavailable until HA returns +# b) WiFi/Network OFFLINE +# - No HA API or web control available +# - Physical buttons continue to work if Motor Stop is OFF +# - Fallback AP / captive portal should become available from network_common_dhcp.yaml +# c) SNTP NOT SYNCED +# - Normal motor timing continues if Motor Stop is OFF +# - Manual controls continue if Motor Stop is OFF +# - 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 +# - Motor Stop returns to OFF after every reboot +# - Physical position is assumed valid because the motor housing/dial is fixed +#:########################################################################################:# + + +#:########################################################################################:# +# 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.7" + + # Passwords & Secrets + api_key: !secret esp-api_key + ota_pass: !secret esp-ota_pass + + # Device Settings + log_level: "INFO" + 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" + + # Physical Direction Calibration + # This is the motor phase direction that makes the needle move clockwise. + # Your testing showed the old reverse direction moved clockwise, so default is -1. + # If clockwise/anti-clockwise is still backwards, change this to 1. + clockwise_motor_direction_multiplier: "-1" + + # Manual Movement Speeds + manual_fast_rotation_seconds: "30" + manual_slow_rotation_seconds: "300" + + # Auto Position Path + # true = move clockwise or anti-clockwise, whichever is quicker + # false = always move clockwise / forward through the year + auto_position_use_shortest_path: "true" + + # 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" + + # Step LED + onboard_step_led_pin: GPIO2 + step_led_flash_ms: "25" + + # DHCP IP Address LED Flash Settings + ip_flash_enabled: "true" + ip_flash_repeat_count: "5" + ip_flash_zero_digit_blinks: "10" + ip_flash_loop_interval: "25ms" + + # Suggested human-countable timing + ip_flash_on_ms: "200" + ip_flash_between_flash_gap_ms: "450" + 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 + stepper_in2_pin: GPIO12 + stepper_in3_pin: GPIO13 + stepper_in4_pin: GPIO15 + + # Physical Button Input Pins + # Button 1 and 2 physically move anti-clockwise, so they are reverse. + # Button 3 and 4 physically move clockwise, so they are forward. + button_fast_reverse_pin: GPIO5 + button_slow_reverse_pin: GPIO4 + button_slow_forward_pin: GPIO0 + button_fast_forward_pin: GPIO16 + + # Entity Naming + motor_stop_name: "Motor Stop" + + 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" + 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 +# https://esphome.io/components/packages.html +#:########################################################################################:# +packages: + #### WIFI, Network (DHCP/IPV6 etc), Fallback AP, Safemode #### + common_wifi: !include + file: common/network_common_dhcp.yaml + vars: + local_device_name: "${device_name}" + local_ota_pass: "${ota_pass}" + + #### 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 + common_webportal: !include common/webportal_common_nopasswd.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_nomqtt.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 + + # Motor Stop should always be OFF on boot so normal movement can run. + - switch.turn_off: motor_stop_switch + + - logger.log: + level: INFO + format: "Astronomical Clock Stepper started. Normal rotation is %s days per full revolution." + args: + - '"${rotation_days}"' + + # Wait for WiFi/DHCP, then flash the assigned IPv4 address. + # This does not stop the main stepper scheduler from running. + - 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: +# https://esphome.io/components/esp8266.html +#:########################################################################################:# +esp8266: + board: d1_mini + restore_from_flash: true + +preferences: + flash_write_interval: 1min + +mdns: + disabled: false + + +#:########################################################################################:# +# LOGGING: +# https://esphome.io/components/logger.html +#:########################################################################################:# +logger: + level: "${log_level}" + 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, IP flash state, and auto-position state +# https://esphome.io/components/globals.html +#:########################################################################################:# +globals: + - id: current_position_step + type: int + restore_value: yes + initial_value: "0" + + # Electrical stepper phase. + # This should not be reset when setting the calendar zero point. + - id: current_phase_index + type: int + restore_value: yes + 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 + # 5 = Auto Position + # 6 = Motor Stopped + - 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" + + # IP Address Flash State + - id: ip_flash_active + type: bool + restore_value: no + initial_value: "false" + + - id: ip_flash_led_is_on + type: bool + restore_value: no + initial_value: "false" + + - id: ip_flash_next_action_ms + type: uint32_t + restore_value: no + initial_value: "0" + + - id: ip_flash_repeats_completed + type: int + restore_value: no + initial_value: "0" + + - id: ip_flash_octet_index + type: int + restore_value: no + initial_value: "0" + + - id: ip_flash_digit_index + type: int + restore_value: no + initial_value: "0" + + - id: ip_flash_blinks_completed + type: int + restore_value: no + initial_value: "0" + + - id: ip_flash_octet_0 + type: int + restore_value: no + initial_value: "0" + + - id: ip_flash_octet_1 + type: int + restore_value: no + initial_value: "0" + + - id: ip_flash_octet_2 + type: int + restore_value: no + initial_value: "0" + + - id: ip_flash_octet_3 + type: int + 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" + + # 1 = clockwise / forward / increasing position + # -1 = anti-clockwise / reverse / decreasing position + - id: auto_position_logical_direction + type: int + restore_value: no + initial_value: "1" + + - id: auto_position_cycle_seconds + type: double + restore_value: no + initial_value: "0.0" + + - id: auto_position_forward_steps + type: int + restore_value: no + initial_value: "0" + + - id: auto_position_reverse_steps + type: int + restore_value: no + initial_value: "0" + + +#:########################################################################################:# +# 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 GPIO2 / D4 and active-low + - platform: gpio + id: onboard_step_led + pin: + number: ${onboard_step_led_pin} + inverted: true + + +#:########################################################################################:# +# SCRIPT: +# Utility scripts +# https://esphome.io/components/script.html +#:########################################################################################:# +script: + - id: start_ip_flash_script + mode: restart + then: + - lambda: |- + const bool IP_FLASH_ENABLED = ${ip_flash_enabled}; + + if (!IP_FLASH_ENABLED) { + ESP_LOGI("ip_flash", "IP flash is disabled."); + return; + } + + if (WiFi.status() != WL_CONNECTED) { + ESP_LOGW("ip_flash", "WiFi is not connected. Cannot flash IP address."); + return; + } + + IPAddress ip = WiFi.localIP(); + + if (ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0) { + ESP_LOGW("ip_flash", "Invalid IPv4 address 0.0.0.0. Cannot flash IP address."); + return; + } + + id(ip_flash_octet_0) = ip[0]; + id(ip_flash_octet_1) = ip[1]; + id(ip_flash_octet_2) = ip[2]; + id(ip_flash_octet_3) = ip[3]; + + id(ip_flash_repeats_completed) = 0; + id(ip_flash_octet_index) = 0; + id(ip_flash_digit_index) = 0; + id(ip_flash_blinks_completed) = 0; + id(ip_flash_led_is_on) = false; + id(ip_flash_next_action_ms) = millis(); + id(ip_flash_active) = true; + + // Keep the IP flash sequence visually clean. + id(step_led_is_on) = false; + id(onboard_step_led).turn_off(); + + ESP_LOGI( + "ip_flash", + "Flashing IPv4 address %u.%u.%u.%u on onboard LED. Repeats: %d", + ip[0], + ip[1], + ip[2], + ip[3], + ${ip_flash_repeat_count} + ); + + - id: start_auto_position_script + mode: restart + then: + - lambda: |- + const int STEPS_PER_ROTATION = ${steps_per_rotation}; + const bool AUTO_POSITION_USE_SHORTEST_PATH = ${auto_position_use_shortest_path}; + + const double ROTATION_SECONDS = + ${rotation_days} * 24.0 * 60.0 * 60.0; + + auto now = id(sntp_time).now(); + + if (!now.is_valid()) { + ESP_LOGW("auto_position", "SNTP time is not valid yet. Auto position cancelled."); + return; + } + + if (id(motor_stop_switch).state) { + ESP_LOGW("auto_position", "Motor Stop is ON. 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_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_calendar_year = + ((uint32_t) (day_of_year - 1) * 86400UL) + + ((uint32_t) now.hour * 3600UL) + + ((uint32_t) now.minute * 60UL) + + ((uint32_t) now.second); + + // The display zero mark is Jan 1 local midnight. + // The dial cycle is the configured solar-year length, not 365 or 366 days. + double cycle_seconds = (double) seconds_elapsed_this_calendar_year; + + while (cycle_seconds >= ROTATION_SECONDS) { + cycle_seconds -= ROTATION_SECONDS; + } + + while (cycle_seconds < 0.0) { + cycle_seconds += ROTATION_SECONDS; + } + + int target_step = + (int) ((cycle_seconds / ROTATION_SECONDS) * (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 forward_steps = + (target_step - current_step + STEPS_PER_ROTATION) % STEPS_PER_ROTATION; + + const int reverse_steps = + (current_step - target_step + STEPS_PER_ROTATION) % STEPS_PER_ROTATION; + + int chosen_steps = forward_steps; + int chosen_logical_direction = 1; + + if (AUTO_POSITION_USE_SHORTEST_PATH && reverse_steps < forward_steps) { + chosen_steps = reverse_steps; + chosen_logical_direction = -1; + } + + id(auto_position_target_step) = target_step; + id(auto_position_steps_remaining) = chosen_steps; + id(auto_position_logical_direction) = chosen_logical_direction; + id(auto_position_cycle_seconds) = cycle_seconds; + id(auto_position_forward_steps) = forward_steps; + id(auto_position_reverse_steps) = reverse_steps; + + if (chosen_steps == 0) { + id(auto_position_active) = false; + ESP_LOGI( + "auto_position", + "Already at correct solar-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, cycle_seconds: %.2f / %.2f, target step: %d, current step: %d, forward steps: %d, reverse steps: %d, chosen direction: %s, moving %d steps.", + year, + day_of_year, + cycle_seconds, + ROTATION_SECONDS, + target_step, + current_step, + forward_steps, + reverse_steps, + chosen_logical_direction >= 0 ? "clockwise/forward" : "anti-clockwise/reverse", + chosen_steps + ); + + +#:########################################################################################:# +# SWITCH: +# Motor stop, web held-button controls, and restored auto-position boot option +# 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" + + - 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 + + - platform: template + name: "${motor_stop_name}" + id: motor_stop_switch + icon: mdi:motor-off + optimistic: true + + # OFF by default after every boot. State is not restored. + # OFF means the motor is allowed to run. + # ON means motor outputs are disabled. + 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 + - switch.turn_off: web_slow_reverse_switch + - lambda: |- + id(web_fast_forward_active) = false; + id(web_slow_forward_active) = false; + id(web_fast_reverse_active) = false; + id(web_slow_reverse_active) = false; + + 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; + + if (id(auto_position_active)) { + id(auto_position_active) = false; + id(auto_position_steps_remaining) = 0; + ESP_LOGW("motor_stop", "Motor Stop enabled. Auto positioning cancelled."); + } + + id(step_accumulator_ms) = 0.0; + id(current_motion_mode) = 6; + + turn_off_action: + - lambda: |- + id(step_accumulator_ms) = 0.0; + id(current_motion_mode) = 0; + ESP_LOGI("motor_stop", "Motor Stop disabled. Motor outputs enabled."); + + +#:########################################################################################:# +# 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_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" + + - 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_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: 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: +# 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: step_accumulator_ms + value: "0.0" + - globals.set: + id: auto_position_active + value: "false" + - globals.set: + id: auto_position_steps_remaining + value: "0" + - globals.set: + id: auto_position_logical_direction + value: "1" + - globals.set: + id: auto_position_cycle_seconds + value: "0.0" + - globals.set: + id: auto_position_forward_steps + value: "0" + - globals.set: + id: auto_position_reverse_steps + value: "0" + - logger.log: + level: INFO + format: "Position counter zeroed. Current physical pointer location is now Jan-zero." + + - platform: template + name: "${replay_ip_flash_name}" + id: replay_ip_flash_button + icon: mdi:ip-network + 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: +# 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}; + + - 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 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}; + + - 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 Forward Steps" + id: auto_position_forward_steps_sensor + icon: mdi:rotate-right + accuracy_decimals: 0 + update_interval: "${sensor_update_interval}" + lambda: |- + return id(auto_position_forward_steps); + + - platform: template + name: "Auto Position Reverse Steps" + id: auto_position_reverse_steps_sensor + icon: mdi:rotate-left + accuracy_decimals: 0 + update_interval: "${sensor_update_interval}" + lambda: |- + return id(auto_position_reverse_steps); + + - platform: template + name: "Auto Position Cycle Days" + id: auto_position_cycle_days_sensor + icon: mdi:calendar-today + unit_of_measurement: "days" + accuracy_decimals: 4 + update_interval: "${sensor_update_interval}" + lambda: |- + return id(auto_position_cycle_seconds) / 86400.0; + + +#:########################################################################################:# +# 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: |- + if (id(motor_stop_switch).state) { + return {"Motor Stopped"}; + } + + 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 5: + return {"Auto Position"}; + case 6: + return {"Motor Stopped"}; + 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 {"Clockwise / Forward"}; + + - platform: template + name: "Auto Position Direction" + id: auto_position_direction_text_sensor + icon: mdi:rotate-3d-variant + update_interval: "${sensor_update_interval}" + lambda: |- + if (!id(auto_position_active)) { + return {"Idle"}; + } + + if (id(auto_position_logical_direction) < 0) { + return {"Anti-clockwise / Reverse"}; + } + + return {"Clockwise / Forward"}; + + +#:########################################################################################:# +# INTERVAL: +# Stepper scheduler and IP flash scheduler +# https://esphome.io/components/interval.html +#:########################################################################################:# +interval: + - interval: 5ms + then: + - lambda: |- + const int STEPS_PER_ROTATION = ${steps_per_rotation}; + const int CLOCKWISE_MOTOR_DIRECTION_MULTIPLIER = ${clockwise_motor_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 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}; + + 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) { + if (!id(ip_flash_active)) { + 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 logical_direction, uint32_t now_ms) { + // logical_direction >= 0 means clockwise / forward / increase position. + // logical_direction < 0 means anti-clockwise / reverse / decrease position. + int motor_direction = + logical_direction >= 0 + ? CLOCKWISE_MOTOR_DIRECTION_MULTIPLIER + : -CLOCKWISE_MOTOR_DIRECTION_MULTIPLIER; + + if (logical_direction >= 0) { + id(current_position_step) = + (id(current_position_step) + 1) % STEPS_PER_ROTATION; + } else { + id(current_position_step) = + (id(current_position_step) + STEPS_PER_ROTATION - 1) % STEPS_PER_ROTATION; + } + + if (motor_direction >= 0) { + id(current_phase_index) = (id(current_phase_index) + 1) % 8; + } else { + id(current_phase_index) = (id(current_phase_index) + 7) % 8; + } + + 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); + + const bool any_manual_control_active = + fast_forward_active || + slow_forward_active || + fast_reverse_active || + slow_reverse_active; + + const uint32_t now_ms = millis(); + + if (id(last_loop_ms) == 0) { + id(last_loop_ms) = now_ms; + id(step_accumulator_ms) = 0.0; + + if (id(motor_stop_switch).state) { + id(current_motion_mode) = 6; + } else { + id(current_motion_mode) = 0; + } + + return; + } + + const uint32_t elapsed_ms = now_ms - id(last_loop_ms); + id(last_loop_ms) = now_ms; + + if (id(motor_stop_switch).state) { + if (id(auto_position_active)) { + id(auto_position_active) = false; + id(auto_position_steps_remaining) = 0; + ESP_LOGW("motor_stop", "Motor Stop is ON. Auto positioning cancelled."); + } + + all_coils_off(); + id(step_accumulator_ms) = 0.0; + id(current_motion_mode) = 6; + return; + } + + 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_logical_direction = 1; + double selected_step_interval_ms = NORMAL_STEP_INTERVAL_MS; + + if (fast_forward_active) { + selected_mode = 1; + selected_logical_direction = 1; + selected_step_interval_ms = FAST_STEP_INTERVAL_MS; + } else if (slow_forward_active) { + selected_mode = 2; + selected_logical_direction = 1; + selected_step_interval_ms = SLOW_STEP_INTERVAL_MS; + } else if (fast_reverse_active) { + selected_mode = 3; + selected_logical_direction = -1; + selected_step_interval_ms = FAST_STEP_INTERVAL_MS; + } else if (slow_reverse_active) { + selected_mode = 4; + selected_logical_direction = -1; + selected_step_interval_ms = SLOW_STEP_INTERVAL_MS; + } else if (id(auto_position_active)) { + selected_mode = 5; + selected_logical_direction = id(auto_position_logical_direction); + + if (selected_logical_direction == 0) { + selected_logical_direction = 1; + } + + selected_step_interval_ms = AUTO_POSITION_STEP_INTERVAL_MS; + } else { + selected_mode = 0; + selected_logical_direction = 1; + selected_step_interval_ms = NORMAL_STEP_INTERVAL_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_logical_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(auto_position_logical_direction) = 1; + 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 + // 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(ip_flash_active) && + 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; + } + + - interval: ${ip_flash_loop_interval} + then: + - lambda: |- + if (!id(ip_flash_active)) { + return; + } + + const int IP_FLASH_REPEAT_COUNT = ${ip_flash_repeat_count}; + const int IP_FLASH_ZERO_DIGIT_BLINKS = ${ip_flash_zero_digit_blinks}; + + const uint32_t IP_FLASH_ON_MS = ${ip_flash_on_ms}; + const uint32_t IP_FLASH_BETWEEN_FLASH_GAP_MS = ${ip_flash_between_flash_gap_ms}; + const uint32_t IP_FLASH_DIGIT_GAP_MS = ${ip_flash_digit_gap_ms}; + const uint32_t IP_FLASH_OCTET_GAP_MS = ${ip_flash_octet_gap_ms}; + const uint32_t IP_FLASH_REPEAT_GAP_MS = ${ip_flash_repeat_gap_ms}; + + const uint32_t now_ms = millis(); + + if ((int32_t) (now_ms - id(ip_flash_next_action_ms)) < 0) { + return; + } + + auto get_octet = [&](int index) -> int { + switch (index) { + case 0: + return id(ip_flash_octet_0); + case 1: + return id(ip_flash_octet_1); + case 2: + return id(ip_flash_octet_2); + case 3: + default: + return id(ip_flash_octet_3); + } + }; + + auto get_digit_count = [&](int value) -> int { + if (value >= 100) { + return 3; + } + + if (value >= 10) { + return 2; + } + + return 1; + }; + + auto get_digit = [&](int value, int digit_index) -> int { + const int digit_count = get_digit_count(value); + + if (digit_count == 3) { + if (digit_index == 0) { + return value / 100; + } + + if (digit_index == 1) { + return (value / 10) % 10; + } + + return value % 10; + } + + if (digit_count == 2) { + if (digit_index == 0) { + return value / 10; + } + + return value % 10; + } + + return value; + }; + + const int current_octet = get_octet(id(ip_flash_octet_index)); + const int current_digit_count = get_digit_count(current_octet); + const int current_digit = get_digit(current_octet, id(ip_flash_digit_index)); + const int target_blinks = + current_digit == 0 ? IP_FLASH_ZERO_DIGIT_BLINKS : current_digit; + + if (id(ip_flash_led_is_on)) { + id(onboard_step_led).turn_off(); + id(ip_flash_led_is_on) = false; + id(ip_flash_blinks_completed)++; + + if (id(ip_flash_blinks_completed) < target_blinks) { + id(ip_flash_next_action_ms) = now_ms + IP_FLASH_BETWEEN_FLASH_GAP_MS; + return; + } + + id(ip_flash_blinks_completed) = 0; + id(ip_flash_digit_index)++; + + if (id(ip_flash_digit_index) < current_digit_count) { + id(ip_flash_next_action_ms) = now_ms + IP_FLASH_DIGIT_GAP_MS; + return; + } + + id(ip_flash_digit_index) = 0; + id(ip_flash_octet_index)++; + + if (id(ip_flash_octet_index) < 4) { + id(ip_flash_next_action_ms) = now_ms + IP_FLASH_OCTET_GAP_MS; + return; + } + + id(ip_flash_octet_index) = 0; + id(ip_flash_repeats_completed)++; + + if (id(ip_flash_repeats_completed) >= IP_FLASH_REPEAT_COUNT) { + id(ip_flash_active) = false; + id(ip_flash_led_is_on) = false; + id(onboard_step_led).turn_off(); + + ESP_LOGI("ip_flash", "Finished flashing IPv4 address."); + return; + } + + id(ip_flash_next_action_ms) = now_ms + IP_FLASH_REPEAT_GAP_MS; + return; + } + + 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; + + +#:########################################################################################:# +# 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 \ No newline at end of file