diff --git a/esphome/esp-astroclockstepper1.yaml b/esphome/esp-astroclockstepper1.yaml index cf04bab..6d99c4b 100644 --- a/esphome/esp-astroclockstepper1.yaml +++ b/esphome/esp-astroclockstepper1.yaml @@ -6,6 +6,7 @@ # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-astroclockstepper.yaml #:########################################################################################:# # VERSIONS: +# 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 #:########################################################################################:# @@ -48,19 +49,20 @@ # - 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. +# - 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. #:########################################################################################:# # OFFLINE NOTES: -# a) Home Assistant OFFLINE, but Network and MQTT ONLINE -# - Device remains controllable via local web portal and MQTT commands +# a) Home Assistant OFFLINE, but Network ONLINE +# - Device remains controllable via local web portal # - 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 +# b) WiFi/Network OFFLINE +# - No HA API or web control available # - Physical buttons continue to work # - Normal motor timing continues from device uptime -# d) Device power loss / reboot +# - 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 #:########################################################################################:# @@ -79,23 +81,11 @@ substitutions: # Project Naming project_name: "zorruno.Astronomical Clock Stepper" - project_version: "v1.1" + project_version: "v1.2" # 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" @@ -122,6 +112,19 @@ substitutions: 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 @@ -146,6 +149,7 @@ substitutions: physical_slow_reverse_name: "Physical Slow Reverse" zero_position_name: "Zero Position Counter" + replay_ip_flash_name: "Replay IP Address Flash" #:########################################################################################:# @@ -153,15 +157,12 @@ substitutions: # https://esphome.io/components/packages.html #:########################################################################################:# packages: - #### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode #### + #### WIFI, Network (DHCP/IPV6 etc), Fallback AP, Safemode #### common_wifi: !include - #file: common/network_common.yaml file: common/network_common_dhcp.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 @@ -178,7 +179,7 @@ packages: #### WEB PORTAL #### #common_webportal: !include common/webportal_common.yaml - common_webportal: !include common/webportal_common_nopasswd.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 @@ -213,6 +214,14 @@ esphome: 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 + #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK: @@ -240,7 +249,7 @@ logger: #:########################################################################################:# # GLOBALS: -# Internal position, timing, mode, and held-button state +# Internal position, timing, mode, held-button state, and IP flash state # https://esphome.io/components/globals.html #:########################################################################################:# globals: @@ -336,6 +345,62 @@ globals: 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" + #:########################################################################################:# # OUTPUT: @@ -367,6 +432,63 @@ output: 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} + ); + + #:########################################################################################:# # SWITCH: # Web held-button controls @@ -553,6 +675,13 @@ button: level: INFO format: "Position counter zeroed." + - platform: template + name: "${replay_ip_flash_name}" + id: replay_ip_flash_button + icon: mdi:ip-network + on_press: + - script.execute: start_ip_flash_script + #:########################################################################################:# # SENSOR: @@ -647,7 +776,7 @@ text_sensor: #:########################################################################################:# # INTERVAL: -# Stepper scheduler +# Stepper scheduler and IP flash scheduler # https://esphome.io/components/interval.html #:########################################################################################:# interval: @@ -678,9 +807,11 @@ interval: }; 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; + 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) { @@ -811,6 +942,7 @@ interval: } if ( + !id(ip_flash_active) && id(step_led_is_on) && ((uint32_t) (now_ms - id(step_led_started_ms)) >= STEP_LED_FLASH_MS) ) { @@ -818,50 +950,128 @@ interval: id(step_led_is_on) = false; } + - interval: ${ip_flash_loop_interval} + then: + - lambda: |- + if (!id(ip_flash_active)) { + return; + } -#:########################################################################################:# -# 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 + 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 \ No newline at end of file