checkpoint commit
All checks were successful
Podman DDNS Image / build-and-push-ddns (push) Successful in 1m3s
BIN
active/device_esphome/beep.wav
Normal file
@@ -8,6 +8,7 @@
|
||||
- [Adding a New Device](#adding-a-new-device)
|
||||
- [Controlling Home Assistant](#controlling-home-assistant)
|
||||
- [Configuration Sections](#configuration-sections)
|
||||
- [Symbols](#symbols)
|
||||
- [esphome](#esphome)
|
||||
- [esp32](#esp32-1)
|
||||
- [logger](#logger)
|
||||
@@ -63,7 +64,8 @@ uv venv
|
||||
uv pip install esphome
|
||||
source .venv/bin/activate
|
||||
|
||||
esphome run m5stack-atom-echo.yaml
|
||||
# grep for debug logs only (helpful for filtering noise)
|
||||
esphome run tab1.yaml | grep -E '.*[\[D\]]'
|
||||
```
|
||||
|
||||
## Adding a New Device
|
||||
@@ -78,6 +80,13 @@ esphome run m5stack-atom-echo.yaml
|
||||
|
||||
<https://esphome.io/components/>
|
||||
|
||||
## Symbols
|
||||
|
||||
You can display the embedded symbols among the text by their codepoint address
|
||||
preceded by \u. For example: \uF00C :
|
||||
|
||||

|
||||
|
||||
### esphome
|
||||
|
||||
### esp32
|
||||
@@ -145,6 +154,22 @@ data:
|
||||
media_content_id: "media-source://media_source/local/wake_word_triggered.wav"
|
||||
```
|
||||
|
||||
Playing arbitrary sound:
|
||||
|
||||
```yaml
|
||||
audio_file:
|
||||
- id: beep_sound
|
||||
file: "beep.wav"
|
||||
|
||||
media_source:
|
||||
- platform: audio_file
|
||||
id: file_source
|
||||
|
||||
- media_player.speaker.play_on_device_media_file:
|
||||
media_file: beep_sound
|
||||
announcement: true
|
||||
```
|
||||
|
||||
### voice assistant
|
||||
|
||||
<https://esphome.io/components/voice_assistant/>
|
||||
|
||||
@@ -54,24 +54,24 @@ i2c:
|
||||
id: bus_1
|
||||
|
||||
sensor:
|
||||
- platform: sonic_i2c
|
||||
i2c_id: bus_1
|
||||
address: 0x57
|
||||
name: "Ultrasonic Sensor 1"
|
||||
id: ultrasonic1
|
||||
unit_of_measurement: mm
|
||||
update_interval: 5s
|
||||
filters:
|
||||
- filter_out: nan
|
||||
- lambda: |-
|
||||
if (x == 0) {
|
||||
return {}; // This filters out the reading
|
||||
} else {
|
||||
return x; // This passes the reading through
|
||||
}
|
||||
- sliding_window_moving_average:
|
||||
window_size: 10
|
||||
send_every: 20
|
||||
- platform: sonic_i2c
|
||||
i2c_id: bus_1
|
||||
address: 0x57
|
||||
name: "Ultrasonic Sensor 1"
|
||||
id: ultrasonic1
|
||||
unit_of_measurement: mm
|
||||
update_interval: 5s
|
||||
filters:
|
||||
- filter_out: nan
|
||||
- lambda: |-
|
||||
if (x == 0) {
|
||||
return {}; // This filters out the reading
|
||||
} else {
|
||||
return x; // This passes the reading through
|
||||
}
|
||||
- sliding_window_moving_average:
|
||||
window_size: 10
|
||||
send_every: 20
|
||||
|
||||
button:
|
||||
- platform: factory_reset
|
||||
@@ -99,7 +99,7 @@ speaker:
|
||||
dac_type: external
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
channel: stereo # The Echo has poor playback audio quality when using mon audio
|
||||
channel: stereo # The Echo has poor playback audio quality when using mon audio
|
||||
buffer_duration: 60ms
|
||||
|
||||
media_player:
|
||||
@@ -197,7 +197,7 @@ voice_assistant:
|
||||
- delay: 2s
|
||||
- script.execute: reset_led
|
||||
on_client_connected:
|
||||
- delay: 2s # Give the api server time to settle
|
||||
- delay: 2s # Give the api server time to settle
|
||||
- script.execute: start_wake_word
|
||||
on_client_disconnected:
|
||||
- script.execute: stop_wake_word
|
||||
@@ -355,24 +355,24 @@ switch:
|
||||
on_turn_off:
|
||||
# Turn off the repeat mode and disable the pause between playlist items
|
||||
- lambda: |-
|
||||
id(echo_media_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0);
|
||||
id(echo_media_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0);
|
||||
# Stop playing the alarm
|
||||
- media_player.stop:
|
||||
announcement: true
|
||||
on_turn_on:
|
||||
# Turn on the repeat mode and pause for 1000 ms between playlist items/repeats
|
||||
- lambda: |-
|
||||
id(echo_media_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000);
|
||||
id(echo_media_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000);
|
||||
- media_player.speaker.play_on_device_media_file:
|
||||
media_file: timer_finished_wave_file
|
||||
announcement: true
|
||||
|
||||
BIN
active/device_esphome/image.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
active/device_esphome/images/charging.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
active/device_esphome/images/charging.xcf
Normal file
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 431 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 430 KiB After Width: | Height: | Size: 109 KiB |
997
active/device_esphome/pyramid1.yaml
Normal file
@@ -0,0 +1,997 @@
|
||||
---
|
||||
substitutions:
|
||||
name: pyramid1
|
||||
friendly_name: Pyramid 1
|
||||
|
||||
# Casita images
|
||||
loading_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/loading_320_240.png
|
||||
idle_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/idle_320_240.png
|
||||
listening_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/listening_320_240.png
|
||||
thinking_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/thinking_320_240.png
|
||||
replying_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/replying_320_240.png
|
||||
error_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/error_320_240.png
|
||||
error_no_wifi_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-wifi.png
|
||||
error_no_ha_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-ha.png
|
||||
|
||||
# Fonts
|
||||
mdi_webfont_file: https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf
|
||||
|
||||
# Audio files
|
||||
wake_word_trigger_sound_file: wake_word_triggered.wav
|
||||
# timer_finished_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac
|
||||
# error_cloud_expired_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/error_cloud_expired.mp3
|
||||
|
||||
# Micro wake word models
|
||||
pick_pig: https://raw.githubusercontent.com/esphome/micro-wake-word-models/refs/heads/main/models/v2/experiments/hey_peppa_pig.json
|
||||
stop_model_file: https://github.com/kahrendt/microWakeWord/releases/download/stop/stop.json
|
||||
# Background colors
|
||||
loading_illustration_background_color: "000000"
|
||||
idle_illustration_background_color: "000000"
|
||||
listening_illustration_background_color: "FFFFFF"
|
||||
thinking_illustration_background_color: "FFFFFF"
|
||||
replying_illustration_background_color: "FFFFFF"
|
||||
error_illustration_background_color: "000000"
|
||||
|
||||
# Phases of the Voice Assistant
|
||||
# The voice assistant is ready to be triggered by a wake word
|
||||
voice_assist_idle_phase_id: "1"
|
||||
# The voice assistant is listening for a voice command
|
||||
voice_assist_listening_phase_id: "2"
|
||||
# The voice assistant is currently processing the command
|
||||
voice_assist_thinking_phase_id: "3"
|
||||
# The voice assistant is replying to the command
|
||||
voice_assist_replying_phase_id: "4"
|
||||
# The voice assistant is not ready
|
||||
voice_assist_not_ready_phase_id: "10"
|
||||
# The voice assistant encountered an error
|
||||
voice_assist_error_phase_id: "11"
|
||||
# Muted phase
|
||||
voice_assist_muted_phase_id: "12"
|
||||
# Finished timer phase
|
||||
voice_assist_timer_finished_phase_id: "20"
|
||||
|
||||
esphome:
|
||||
name: pyramid1
|
||||
friendly_name: Pyramid 1
|
||||
min_version: 2025.11.3
|
||||
on_boot:
|
||||
priority: 600
|
||||
then:
|
||||
- delay: 30s
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(init_in_progress);
|
||||
then:
|
||||
- lambda: id(init_in_progress) = false;
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
flash_size: 8MB
|
||||
cpu_frequency: 240MHz
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: "innoIL7I6ZfRekL58F65REjeYNLW1Hp/Q/Kv9SEjnNA="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "22de00dcf5c2701a25d2fe719d596123"
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
ap:
|
||||
ssid: "Echo-Pyramid Fallback Hotspot"
|
||||
password: "uSTvJjVzweZp"
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
level: INFO
|
||||
logs:
|
||||
sensor: WARN
|
||||
|
||||
captive_portal:
|
||||
|
||||
button:
|
||||
- platform: factory_reset
|
||||
id: factory_reset_btn
|
||||
internal: true
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: GPIO41
|
||||
mode: INPUT_PULLUP
|
||||
inverted: true
|
||||
id: user_button
|
||||
internal: true
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at least 50ms
|
||||
- OFF for at least 50ms
|
||||
then:
|
||||
- switch.turn_off: timer_ringing
|
||||
- timing:
|
||||
- ON for at least 10s
|
||||
then:
|
||||
- button.press: factory_reset_btn
|
||||
|
||||
external_components:
|
||||
- source: github://m5stack/esphome-yaml/components
|
||||
components: [aw87559, si5351, lp5562, pyramidrgb, pyramidtouch]
|
||||
refresh: 0s
|
||||
|
||||
# I2C Bus Configuration
|
||||
i2c:
|
||||
- id: bsp_bus
|
||||
sda: GPIO45
|
||||
scl: GPIO0
|
||||
scan: true
|
||||
- id: ext_bus # used on atomic echo base
|
||||
sda: GPIO38
|
||||
scl: GPIO39
|
||||
|
||||
# Ehco Base GPIO Expander
|
||||
pi4ioe5v6408:
|
||||
- id: pi4ioe5v6408_hub
|
||||
i2c_id: ext_bus
|
||||
address: 0x43
|
||||
|
||||
aw87559:
|
||||
id: audio_amp
|
||||
i2c_id: ext_bus
|
||||
address: 0x5B
|
||||
|
||||
si5351:
|
||||
id: clock_gen
|
||||
i2c_id: ext_bus
|
||||
address: 0x60
|
||||
|
||||
# I2S Bus Configuration
|
||||
i2s_audio:
|
||||
- id: i2s_audio_bus
|
||||
i2s_lrclk_pin: GPIO8
|
||||
i2s_bclk_pin: GPIO6
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO15
|
||||
mosi_pin: GPIO21
|
||||
# miso_pin is not used
|
||||
|
||||
audio_dac:
|
||||
- platform: es8311
|
||||
id: es8311_dac
|
||||
i2c_id: ext_bus
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
|
||||
audio_adc:
|
||||
- platform: es7210
|
||||
id: es7210_adc
|
||||
i2c_id: ext_bus
|
||||
address: 0x40
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: i2s_mic
|
||||
sample_rate: 16000
|
||||
i2s_din_pin: GPIO5
|
||||
bits_per_sample: 16bit
|
||||
adc_type: external
|
||||
channel: stereo
|
||||
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: i2s_speaker
|
||||
i2s_dout_pin: GPIO7
|
||||
dac_type: external
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
channel: mono
|
||||
audio_dac: es8311_dac
|
||||
|
||||
media_player:
|
||||
- platform: speaker
|
||||
name: "Echo Pyramid Player"
|
||||
id: echo_pyramid_player
|
||||
volume_min: 0.0
|
||||
volume_max: 1.0
|
||||
volume_initial: 0.10
|
||||
buffer_size: 6000
|
||||
announcement_pipeline:
|
||||
speaker: i2s_speaker
|
||||
format: WAV
|
||||
# sample_rate: 48000
|
||||
# num_channels: 1
|
||||
codec_support_enabled: false
|
||||
files:
|
||||
- id: wake_word_triggered_sound
|
||||
file: ${wake_word_trigger_sound_file}
|
||||
# - id: timer_finished_sound
|
||||
# file: ${timer_finished_sound_file}
|
||||
# - id: error_cloud_expired
|
||||
# file: ${error_cloud_expired_file}
|
||||
on_state:
|
||||
- logger.log: "State updated!"
|
||||
on_play:
|
||||
- logger.log: "Playback started!"
|
||||
|
||||
on_announcement:
|
||||
- logger.log: "Announcing!"
|
||||
# Stop the wake word (mWW or VA) if the mic is capturing
|
||||
- if:
|
||||
condition:
|
||||
- microphone.is_capturing:
|
||||
then:
|
||||
- script.execute: stop_wake_word
|
||||
# Ensure VA stops before moving on
|
||||
- if:
|
||||
condition:
|
||||
- lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "In Home Assistant";
|
||||
then:
|
||||
- wait_until:
|
||||
- not:
|
||||
voice_assistant.is_running:
|
||||
# Since VA isn't running, this is user-intiated media playback. Draw the mute display
|
||||
- if:
|
||||
condition:
|
||||
not:
|
||||
voice_assistant.is_running:
|
||||
then:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
|
||||
- script.execute: draw_display
|
||||
on_idle:
|
||||
# Since VA isn't running, this is the end of user-intiated media playback. Restart the wake word.
|
||||
- if:
|
||||
condition:
|
||||
not:
|
||||
voice_assistant.is_running:
|
||||
then:
|
||||
- script.execute: start_wake_word
|
||||
- script.execute: set_idle_or_mute_phase
|
||||
- script.execute: draw_display
|
||||
|
||||
switch:
|
||||
# NS4150B
|
||||
- platform: gpio
|
||||
name: Speaker Enable
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe5v6408_hub
|
||||
number: 0
|
||||
mode:
|
||||
output: true
|
||||
icon: "mdi:volume-high"
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
- platform: template
|
||||
name: Mute Microphone
|
||||
id: mute
|
||||
icon: "mdi:microphone-off"
|
||||
optimistic: true
|
||||
restore_mode: RESTORE_DEFAULT_OFF
|
||||
entity_category: config
|
||||
on_turn_off:
|
||||
- microphone.unmute:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
|
||||
|
||||
on_turn_on:
|
||||
- microphone.mute:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
|
||||
|
||||
- platform: template
|
||||
id: timer_ringing
|
||||
optimistic: true
|
||||
internal: true
|
||||
restore_mode: ALWAYS_OFF
|
||||
on_turn_off:
|
||||
# Turn off the repeat mode and disable the pause between playlist items
|
||||
- lambda: |-
|
||||
id(echo_pyramid_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_pyramid_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0);
|
||||
# Stop playing the alarm
|
||||
- media_player.stop:
|
||||
announcement: true
|
||||
- script.execute: start_wake_word
|
||||
on_turn_on:
|
||||
- script.execute: stop_wake_word
|
||||
# Turn on the repeat mode and pause for 1000 ms between playlist items/repeats
|
||||
- lambda: |-
|
||||
id(echo_pyramid_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_pyramid_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000);
|
||||
# - media_player.speaker.play_on_device_media_file:
|
||||
# media_file: timer_finished_sound
|
||||
# announcement: true
|
||||
- delay: 15min
|
||||
- switch.turn_off: timer_ringing
|
||||
|
||||
select:
|
||||
- platform: template
|
||||
entity_category: config
|
||||
name: Wake word engine location
|
||||
id: wake_word_engine_location
|
||||
icon: "mdi:account-voice"
|
||||
optimistic: true
|
||||
restore_value: true
|
||||
options:
|
||||
- In Home Assistant
|
||||
- On device
|
||||
initial_option: On device
|
||||
on_value:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(init_in_progress);
|
||||
then:
|
||||
- wait_until:
|
||||
lambda: return id(voice_assistant_phase) == ${voice_assist_muted_phase_id} || id(voice_assistant_phase) == ${voice_assist_idle_phase_id};
|
||||
- if:
|
||||
condition:
|
||||
lambda: return x == "In Home Assistant";
|
||||
then:
|
||||
- micro_wake_word.stop
|
||||
- delay: 500ms
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(true);
|
||||
- voice_assistant.start_continuous:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return x == "On device";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(false);
|
||||
- voice_assistant.stop
|
||||
- delay: 500ms
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- micro_wake_word.start
|
||||
|
||||
- platform: template
|
||||
name: "Wake word sensitivity"
|
||||
optimistic: true
|
||||
initial_option: Slightly sensitive
|
||||
restore_value: true
|
||||
entity_category: config
|
||||
options:
|
||||
- Slightly sensitive
|
||||
- Moderately sensitive
|
||||
- Very sensitive
|
||||
on_value:
|
||||
# Sets specific wake word probabilities computed for each particular model
|
||||
# Note probability cutoffs are set as a quantized uint8 value, each comment has the corresponding floating point cutoff
|
||||
# False Accepts per Hour values are tested against all units and channels from the Dinner Party Corpus.
|
||||
# These cutoffs apply only to the specific models included in the firmware: okay_nabu@20241226.3, hey_jarvis@v2, hey_mycroft@v2
|
||||
lambda: |-
|
||||
if (x == "Slightly sensitive") {
|
||||
id(okay_nabu).set_probability_cutoff(217); // 0.85 -> 0.000 FAPH on DipCo (Manifest's default)
|
||||
id(hey_jarvis).set_probability_cutoff(247); // 0.97 -> 0.563 FAPH on DipCo (Manifest's default)
|
||||
id(hey_mycroft).set_probability_cutoff(253); // 0.99 -> 0.567 FAPH on DipCo
|
||||
} else if (x == "Moderately sensitive") {
|
||||
id(okay_nabu).set_probability_cutoff(176); // 0.69 -> 0.376 FAPH on DipCo
|
||||
id(hey_jarvis).set_probability_cutoff(235); // 0.92 -> 0.939 FAPH on DipCo
|
||||
id(hey_mycroft).set_probability_cutoff(242); // 0.95 -> 1.502 FAPH on DipCo (Manifest's default)
|
||||
} else if (x == "Very sensitive") {
|
||||
id(okay_nabu).set_probability_cutoff(143); // 0.56 -> 0.751 FAPH on DipCo
|
||||
id(hey_jarvis).set_probability_cutoff(212); // 0.83 -> 1.502 FAPH on DipCo
|
||||
id(hey_mycroft).set_probability_cutoff(237); // 0.93 -> 1.878 FAPH on DipCo
|
||||
}
|
||||
|
||||
micro_wake_word:
|
||||
id: mww
|
||||
microphone: i2s_mic
|
||||
models:
|
||||
- model: okay_nabu
|
||||
id: okay_nabu
|
||||
- model: hey_jarvis
|
||||
id: hey_jarvis
|
||||
- model: hey_mycroft
|
||||
id: hey_mycroft
|
||||
- model: https://raw.githubusercontent.com/esphome/micro-wake-word-models/refs/heads/main/models/v2/experiments/hey_peppa_pig.json
|
||||
id: hey_peppa_pig
|
||||
- model: ${stop_model_file}
|
||||
id: stop
|
||||
internal: true
|
||||
vad:
|
||||
on_wake_word_detected:
|
||||
- script.execute:
|
||||
id: play_sound
|
||||
priority: true
|
||||
sound_file: !lambda return id(wake_word_triggered_sound);
|
||||
|
||||
- wait_until:
|
||||
condition:
|
||||
- media_player.is_announcing:
|
||||
timeout: 0.5s
|
||||
# Announcement is finished and the I2S bus is free
|
||||
- wait_until:
|
||||
- and:
|
||||
- not:
|
||||
media_player.is_announcing:
|
||||
- not:
|
||||
speaker.is_playing:
|
||||
|
||||
- voice_assistant.start:
|
||||
wake_word: !lambda return wake_word;
|
||||
|
||||
voice_assistant:
|
||||
id: va
|
||||
microphone: i2s_mic
|
||||
media_player: echo_pyramid_player
|
||||
micro_wake_word: mww
|
||||
noise_suppression_level: 2
|
||||
auto_gain: 31dBFS
|
||||
volume_multiplier: 2.0
|
||||
on_listening:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id};
|
||||
- script.execute: draw_display
|
||||
on_stt_vad_end:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id};
|
||||
- script.execute: draw_display
|
||||
on_tts_start:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id};
|
||||
- script.execute: draw_display
|
||||
on_end:
|
||||
# Wait a short amount of time to see if an announcement starts
|
||||
- wait_until:
|
||||
condition:
|
||||
- media_player.is_announcing:
|
||||
timeout: 0.5s
|
||||
# Announcement is finished and the I2S bus is free
|
||||
- wait_until:
|
||||
- and:
|
||||
- not:
|
||||
media_player.is_announcing:
|
||||
- not:
|
||||
speaker.is_playing:
|
||||
# Restart only mWW if enabled; streaming wake words automatically restart
|
||||
- if:
|
||||
condition:
|
||||
- lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "On device";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(false);
|
||||
- micro_wake_word.start:
|
||||
- script.execute: set_idle_or_mute_phase
|
||||
- script.execute: draw_display
|
||||
|
||||
on_error:
|
||||
# Only set the error phase if the error code is different than duplicate_wake_up_detected or stt-no-text-recognized
|
||||
# These two are ignored for a better user experience
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- lambda: return !id(init_in_progress);
|
||||
- lambda: return code != "duplicate_wake_up_detected";
|
||||
- lambda: return code != "stt-no-text-recognized";
|
||||
then:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id};
|
||||
- script.execute: draw_display
|
||||
- delay: 1s
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
|
||||
else:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
|
||||
# If the error code is cloud-auth-failed, serve a local audio file guiding the user.
|
||||
- if:
|
||||
condition:
|
||||
- lambda: return code == "cloud-auth-failed";
|
||||
then:
|
||||
# - script.execute:
|
||||
# id: play_sound
|
||||
# priority: true
|
||||
# sound_file: !lambda return id(error_cloud_expired);
|
||||
- script.execute: draw_display
|
||||
|
||||
on_client_connected:
|
||||
- lambda: id(init_in_progress) = false;
|
||||
- script.execute: start_wake_word
|
||||
- script.execute: set_idle_or_mute_phase
|
||||
- script.execute: draw_display
|
||||
|
||||
on_client_disconnected:
|
||||
- script.execute: stop_wake_word
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id};
|
||||
- script.execute: draw_display
|
||||
|
||||
on_timer_finished:
|
||||
- switch.turn_on: timer_ringing
|
||||
- wait_until:
|
||||
media_player.is_announcing:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_timer_finished_phase_id};
|
||||
|
||||
globals:
|
||||
- id: init_in_progress
|
||||
type: bool
|
||||
restore_value: false
|
||||
initial_value: "true"
|
||||
- id: voice_assistant_phase
|
||||
type: int
|
||||
restore_value: false
|
||||
initial_value: ${voice_assist_not_ready_phase_id}
|
||||
- id: current_volume
|
||||
type: float
|
||||
restore_value: true
|
||||
initial_value: "0.3"
|
||||
|
||||
sensor:
|
||||
- platform: pyramidtouch
|
||||
address: 0x1A
|
||||
i2c_id: ext_bus
|
||||
update_interval: 50ms
|
||||
publish_swipe_event: true
|
||||
swipe_timeout_ms: 500
|
||||
touch1:
|
||||
name: "Touch 1"
|
||||
touch2:
|
||||
name: "Touch 2"
|
||||
touch3:
|
||||
name: "Touch 3"
|
||||
touch4:
|
||||
name: "Touch 4"
|
||||
swipe_event:
|
||||
name: "Touch Swipe Event"
|
||||
entity_category: diagnostic
|
||||
on_value:
|
||||
then:
|
||||
- lambda: |-
|
||||
// Swipe codes:
|
||||
// 1 = Left Up (volume up)
|
||||
// 2 = Left Down (volume down)
|
||||
// 3 = Right Up (brightness up)
|
||||
// 4 = Right Down (brightness down)
|
||||
const float volume_step = 0.05f; // 5% volume per gesture
|
||||
const float brightness_step = 5.0f; // 5% brightness per gesture
|
||||
|
||||
const int ev = (int) x;
|
||||
|
||||
if (ev == 1 || ev == 2) {
|
||||
// Left side: control volume (0.0 - 1.0)
|
||||
float v = id(current_volume);
|
||||
if (ev == 1) {
|
||||
v = std::min(1.0f, v + volume_step);
|
||||
} else {
|
||||
v = std::max(0.0f, v - volume_step);
|
||||
}
|
||||
|
||||
auto call = id(echo_pyramid_player).make_call();
|
||||
call.set_volume(v);
|
||||
call.perform();
|
||||
|
||||
id(current_volume) = v;
|
||||
} else if (ev == 3 || ev == 4) {
|
||||
// Right side: control RGB brightness (0 - 100)
|
||||
float b = id(rgb_master_brightness).state;
|
||||
if (ev == 3) {
|
||||
b = std::min(100.0f, b + brightness_step);
|
||||
} else {
|
||||
b = std::max(0.0f, b - brightness_step);
|
||||
}
|
||||
|
||||
uint8_t b8 = (uint8_t) b;
|
||||
id(pyramid_rgb1).set_strip_brightness(1, b8);
|
||||
id(pyramid_rgb2).set_strip_brightness(2, b8);
|
||||
id(rgb_master_brightness).publish_state(b);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
lp5562:
|
||||
id: lp5562_led
|
||||
i2c_id: bsp_bus
|
||||
use_internal_clk: true
|
||||
# power_save_mode: true
|
||||
# high_pwm_freq: true
|
||||
# logarithmic_dimming: true
|
||||
white_current: 17.5
|
||||
|
||||
pyramidrgb:
|
||||
- id: pyramid_rgb1
|
||||
i2c_id: ext_bus
|
||||
address: 0x1A
|
||||
strip: 1
|
||||
brightness: 80
|
||||
- id: pyramid_rgb2
|
||||
i2c_id: ext_bus
|
||||
address: 0x1A
|
||||
strip: 2
|
||||
brightness: 80
|
||||
|
||||
number:
|
||||
# Master media player volume (0.0–1.0)
|
||||
- platform: template
|
||||
name: "Master Volume"
|
||||
id: master_volume
|
||||
icon: "mdi:volume-high"
|
||||
min_value: 0.0
|
||||
max_value: 0.4
|
||||
step: 0.01
|
||||
restore_value: true
|
||||
initial_value: 0.3
|
||||
optimistic: true
|
||||
set_action:
|
||||
- lambda: |-
|
||||
float v = x;
|
||||
auto call = id(echo_pyramid_player).make_call();
|
||||
call.set_volume(v);
|
||||
call.perform();
|
||||
id(current_volume) = v;
|
||||
|
||||
# Master RGB brightness (applies to both strips, 0–100%)
|
||||
- platform: template
|
||||
name: "RGB Master Brightness"
|
||||
id: rgb_master_brightness
|
||||
icon: "mdi:brightness-6"
|
||||
min_value: 0
|
||||
max_value: 100
|
||||
step: 1
|
||||
restore_value: true
|
||||
initial_value: 100
|
||||
optimistic: true
|
||||
set_action:
|
||||
- lambda: |-
|
||||
uint8_t b = (uint8_t) x;
|
||||
id(pyramid_rgb1).set_strip_brightness(1, b);
|
||||
id(pyramid_rgb2).set_strip_brightness(2, b);
|
||||
|
||||
output:
|
||||
- platform: lp5562
|
||||
id: lp5562_white_channel
|
||||
lp5562_id: lp5562_led
|
||||
channel: white
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch0_red
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 0
|
||||
color: red
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch0_green
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 0
|
||||
color: green
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch0_blue
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 0
|
||||
color: blue
|
||||
# Strip 1, Channel 1 (Group 2)
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch1_red
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 1
|
||||
color: red
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch1_green
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 1
|
||||
color: green
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch1_blue
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 1
|
||||
color: blue
|
||||
|
||||
# Strip 2, Channel 2 (Group 1)
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch2_red
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 2
|
||||
color: red
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch2_green
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 2
|
||||
color: green
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch2_blue
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 2
|
||||
color: blue
|
||||
|
||||
# Strip 2, Channel 3 (Group 2)
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch3_red
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 3
|
||||
color: red
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch3_green
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 3
|
||||
color: green
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch3_blue
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 3
|
||||
color: blue
|
||||
|
||||
light:
|
||||
- platform: monochromatic
|
||||
name: "LCD Backlight"
|
||||
output: lp5562_white_channel
|
||||
icon: "mdi:television"
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
- platform: rgb
|
||||
name: "Strip1 Group1"
|
||||
red: rgb1_ch0_red
|
||||
green: rgb1_ch0_green
|
||||
blue: rgb1_ch0_blue
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
- platform: rgb
|
||||
name: "Strip1 Group2"
|
||||
red: rgb1_ch1_red
|
||||
green: rgb1_ch1_green
|
||||
blue: rgb1_ch1_blue
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
- platform: rgb
|
||||
name: "Strip2 Group1"
|
||||
red: rgb2_ch2_red
|
||||
green: rgb2_ch2_green
|
||||
blue: rgb2_ch2_blue
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
- platform: rgb
|
||||
name: "Strip2 Group2"
|
||||
red: rgb2_ch3_red
|
||||
green: rgb2_ch3_green
|
||||
blue: rgb2_ch3_blue
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
id: atoms3r_lcd
|
||||
model: ST7789V
|
||||
dc_pin: GPIO42
|
||||
reset_pin: GPIO48
|
||||
cs_pin: GPIO14
|
||||
data_rate: 40MHz
|
||||
dimensions:
|
||||
height: 128
|
||||
width: 128
|
||||
offset_width: 2
|
||||
offset_height: 1
|
||||
|
||||
invert_colors: true
|
||||
rotation: 180°
|
||||
pages:
|
||||
- id: idle_page
|
||||
lambda: |-
|
||||
it.fill(id(idle_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_idle), ImageAlign::CENTER);
|
||||
- id: listening_page
|
||||
lambda: |-
|
||||
it.fill(id(listening_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_listening), ImageAlign::CENTER);
|
||||
- id: thinking_page
|
||||
lambda: |-
|
||||
it.fill(id(thinking_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_thinking), ImageAlign::CENTER);
|
||||
- id: replying_page
|
||||
lambda: |-
|
||||
it.fill(id(replying_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_replying), ImageAlign::CENTER);
|
||||
- id: error_page
|
||||
lambda: |-
|
||||
it.fill(id(error_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_error), ImageAlign::CENTER);
|
||||
- id: no_ha_page
|
||||
lambda: |-
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_ha), ImageAlign::CENTER);
|
||||
- id: no_wifi_page
|
||||
lambda: |-
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_wifi), ImageAlign::CENTER);
|
||||
- id: initializing_page
|
||||
lambda: |-
|
||||
it.fill(id(loading_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_initializing), ImageAlign::CENTER);
|
||||
- id: muted_page
|
||||
lambda: |-
|
||||
it.fill(Color::BLACK);
|
||||
it.printf(0, 0, id(mdi_icon_128), Color::WHITE, "%s", "\U000F036D");
|
||||
|
||||
script:
|
||||
# Starts either mWW or the streaming wake word, depending on the configured location
|
||||
- id: start_wake_word
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- not:
|
||||
- voice_assistant.is_running:
|
||||
- lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "On device";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(false);
|
||||
- micro_wake_word.start:
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- not:
|
||||
- voice_assistant.is_running:
|
||||
- lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "In Home Assistant";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(true);
|
||||
- voice_assistant.start_continuous:
|
||||
# Stops either mWW or the streaming wake word, depending on the configured location
|
||||
- id: stop_wake_word
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "In Home Assistant";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(false);
|
||||
- voice_assistant.stop:
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "On device";
|
||||
then:
|
||||
- micro_wake_word.stop:
|
||||
# Set the voice assistant phase to idle or muted, depending on if the software mute switch is activated
|
||||
- id: set_idle_or_mute_phase
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
|
||||
else:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
|
||||
|
||||
- id: play_sound
|
||||
parameters:
|
||||
priority: bool
|
||||
sound_file: "audio::AudioFile*"
|
||||
then:
|
||||
- lambda: |-
|
||||
if (priority) {
|
||||
id(echo_pyramid_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
}
|
||||
if ( (id(echo_pyramid_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING ) || priority) {
|
||||
id(echo_pyramid_player)
|
||||
->play_file(sound_file, true, false);
|
||||
}
|
||||
|
||||
- id: draw_display
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(init_in_progress);
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
wifi.connected:
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
api.connected:
|
||||
then:
|
||||
- lambda: |
|
||||
switch(id(voice_assistant_phase)) {
|
||||
case ${voice_assist_listening_phase_id}:
|
||||
id(atoms3r_lcd).show_page(listening_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_thinking_phase_id}:
|
||||
id(atoms3r_lcd).show_page(thinking_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_replying_phase_id}:
|
||||
id(atoms3r_lcd).show_page(replying_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_error_phase_id}:
|
||||
id(atoms3r_lcd).show_page(error_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_muted_phase_id}:
|
||||
id(atoms3r_lcd).show_page(muted_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_not_ready_phase_id}:
|
||||
id(atoms3r_lcd).show_page(no_ha_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
default:
|
||||
id(atoms3r_lcd).show_page(idle_page);
|
||||
id(atoms3r_lcd).update();
|
||||
}
|
||||
else:
|
||||
- display.page.show: no_ha_page
|
||||
- component.update: atoms3r_lcd
|
||||
else:
|
||||
- display.page.show: no_wifi_page
|
||||
- component.update: atoms3r_lcd
|
||||
else:
|
||||
- display.page.show: initializing_page
|
||||
- component.update: atoms3r_lcd
|
||||
|
||||
image:
|
||||
- file: ${error_illustration_file}
|
||||
id: casita_error
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${idle_illustration_file}
|
||||
id: casita_idle
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${listening_illustration_file}
|
||||
id: casita_listening
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${thinking_illustration_file}
|
||||
id: casita_thinking
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${replying_illustration_file}
|
||||
id: casita_replying
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${loading_illustration_file}
|
||||
id: casita_initializing
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${error_no_wifi_illustration_file}
|
||||
id: error_no_wifi
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${error_no_ha_illustration_file}
|
||||
id: error_no_ha
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
|
||||
font:
|
||||
- file: ${mdi_webfont_file}
|
||||
id: mdi_icon_128
|
||||
size: 128
|
||||
bpp: 4
|
||||
glyphs:
|
||||
- "\U000F036D" # mdi:mic-mute
|
||||
|
||||
color:
|
||||
- id: idle_color
|
||||
hex: ${idle_illustration_background_color}
|
||||
- id: listening_color
|
||||
hex: ${listening_illustration_background_color}
|
||||
- id: thinking_color
|
||||
hex: ${thinking_illustration_background_color}
|
||||
- id: replying_color
|
||||
hex: ${replying_illustration_background_color}
|
||||
- id: loading_color
|
||||
hex: ${loading_illustration_background_color}
|
||||
- id: error_color
|
||||
hex: ${error_illustration_background_color}
|
||||
599
active/device_esphome/tab1.yaml
Normal file
@@ -0,0 +1,599 @@
|
||||
esphome:
|
||||
name: tab1
|
||||
friendly_name: M5Stack Tab5 1
|
||||
on_boot:
|
||||
# Set the charging icon to the correct state on boot
|
||||
- then:
|
||||
- logger.log: "Delaying backlight initialization"
|
||||
- delay: 2s
|
||||
- logger.log: "End delay"
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(charging).state;
|
||||
then:
|
||||
- lvgl.widget.show:
|
||||
id: charging_icon_widget
|
||||
else:
|
||||
- lvgl.widget.hide:
|
||||
id: charging_icon_widget
|
||||
|
||||
esp32:
|
||||
board: esp32-p4-evboard
|
||||
flash_size: 16MB
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
enable_idf_experimental_features: true
|
||||
|
||||
esp32_hosted:
|
||||
variant: esp32c6
|
||||
active_high: true
|
||||
clk_pin: GPIO12
|
||||
cmd_pin: GPIO13
|
||||
d0_pin: GPIO11
|
||||
d1_pin: GPIO10
|
||||
d2_pin: GPIO9
|
||||
d3_pin: GPIO8
|
||||
reset_pin: GPIO15
|
||||
slot: 1
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
level: DEBUG
|
||||
|
||||
psram:
|
||||
mode: hex
|
||||
speed: 200MHz
|
||||
|
||||
api:
|
||||
|
||||
# Touchscreen support
|
||||
external_components:
|
||||
- source: github://pr#12075
|
||||
components: [st7123]
|
||||
refresh: 1h
|
||||
|
||||
ota:
|
||||
platform: esphome
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
fast_connect: true
|
||||
on_connect:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "IDLE"
|
||||
- select.set:
|
||||
id: dac_output
|
||||
option: "LINE1"
|
||||
- lvgl.label.update:
|
||||
id: lbl_ip
|
||||
text: !lambda return id(ip_addr).state;
|
||||
- lvgl.label.update:
|
||||
id: lbl_ap
|
||||
text: !lambda return id(ssid).state;
|
||||
on_disconnect:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "DISCONNECTED"
|
||||
|
||||
text_sensor:
|
||||
- platform: wifi_info
|
||||
ip_address:
|
||||
id: ip_addr
|
||||
name: Device IP Address
|
||||
address_0:
|
||||
name: Device IP Address 0
|
||||
address_1:
|
||||
name: Device IP Address 1
|
||||
address_2:
|
||||
name: Device IP Address 2
|
||||
address_3:
|
||||
name: Device IP Address 3
|
||||
address_4:
|
||||
name: Device IP Address 4
|
||||
ssid:
|
||||
id: ssid
|
||||
name: Device Connected SSID
|
||||
bssid:
|
||||
name: Device Connected BSSID
|
||||
mac_address:
|
||||
name: Device Mac Wifi Address
|
||||
scan_results:
|
||||
name: Device Latest Scan Results
|
||||
dns_address:
|
||||
name: Device DNS Address
|
||||
power_save_mode:
|
||||
name: Device Wifi Power Save Mode
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
timezone: America/New_York
|
||||
servers:
|
||||
- 0.pool.ntp.org
|
||||
- 1.pool.ntp.org
|
||||
- 2.pool.ntp.org
|
||||
|
||||
# wireguard:
|
||||
# address: !secret tab1_wg_ip
|
||||
# private_key: !secret tab1_wg_pk
|
||||
# peer_endpoint: !secret wg_host
|
||||
# peer_public_key: !secret wg_pubkey
|
||||
|
||||
# # Optional keepalive (disabled by default)
|
||||
# peer_persistent_keepalive: 25s
|
||||
|
||||
i2c:
|
||||
- id: bsp_bus
|
||||
sda: GPIO31
|
||||
scl: GPIO32
|
||||
frequency: 400kHz
|
||||
|
||||
pi4ioe5v6408:
|
||||
- id: pi4ioe1
|
||||
address: 0x43
|
||||
# 0: O - wifi_antenna_int_ext
|
||||
# 1: O - speaker_enable
|
||||
# 2: O - external_5v_power
|
||||
# 3: NC
|
||||
# 4: O - lcd reset
|
||||
# 5: O - touch panel reset
|
||||
# 6: O - camera reset
|
||||
# 7: I - headphone detect
|
||||
- id: pi4ioe2
|
||||
address: 0x44
|
||||
# 0: O - wifi_power
|
||||
# 1: NC
|
||||
# 2: NC
|
||||
# 3: O - usb_5v_power
|
||||
# 4: O - poweroff pulse
|
||||
# 5: O - quick charge enable (inverted)
|
||||
# 6: I - charging status
|
||||
# 7: O - charge enable
|
||||
|
||||
button:
|
||||
- platform: restart
|
||||
name: "Restart Tablet"
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: wifi_power
|
||||
name: "WiFi Power"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 0
|
||||
restore_mode: ALWAYS_ON
|
||||
- platform: gpio
|
||||
id: usb_5v_power
|
||||
name: "USB Power"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 3
|
||||
- platform: gpio
|
||||
id: quick_charge
|
||||
name: "Quick Charge"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 5
|
||||
inverted: true
|
||||
- platform: gpio
|
||||
id: charge_enable
|
||||
name: "Charge Enable"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 7
|
||||
restore_mode: ALWAYS_ON
|
||||
- platform: gpio
|
||||
id: wifi_antenna_int_ext
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 0
|
||||
- platform: gpio
|
||||
id: speaker_enable
|
||||
name: "Speaker Enable"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 1
|
||||
restore_mode: ALWAYS_ON
|
||||
- platform: gpio
|
||||
id: external_5v_power
|
||||
name: "External 5V Power"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 2
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
id: charging
|
||||
name: "Charging Status"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 6
|
||||
mode: INPUT_PULLDOWN
|
||||
on_state:
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(charging).state;
|
||||
then:
|
||||
- lvgl.widget.show:
|
||||
id: charging_icon_widget
|
||||
else:
|
||||
- lvgl.widget.hide:
|
||||
id: charging_icon_widget
|
||||
|
||||
- platform: gpio
|
||||
id: headphone_detect
|
||||
name: "Headphone Detect"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 7
|
||||
- platform: lvgl
|
||||
widget: volume_up_widget
|
||||
name: Volume Up Button
|
||||
on_press:
|
||||
then:
|
||||
- logger.log: "Button pressed"
|
||||
- media_player.volume_up:
|
||||
id: tab5_media_player
|
||||
- delay: 100ms
|
||||
- lvgl.label.update:
|
||||
id: lbl_volume
|
||||
text: !lambda return to_string(int(id(tab5_media_player).volume * 100));
|
||||
- light.turn_on:
|
||||
id: backlight
|
||||
brightness: !lambda |-
|
||||
float current_value = id(backlight).current_values.get_brightness();
|
||||
return current_value < 0.6 ? 0.6 : current_value + 0.2;
|
||||
- platform: lvgl
|
||||
widget: volume_down_widget
|
||||
name: Volume Down Button
|
||||
on_press:
|
||||
then:
|
||||
- logger.log: "Button pressed"
|
||||
- media_player.volume_down:
|
||||
id: tab5_media_player
|
||||
- lvgl.label.update:
|
||||
id: lbl_volume
|
||||
text: !lambda return to_string(int(id(tab5_media_player).volume * 100));
|
||||
- light.turn_on:
|
||||
id: backlight
|
||||
brightness: !lambda |-
|
||||
float current_value = id(backlight).current_values.get_brightness();
|
||||
return current_value < 0.6 ? 0.6 : current_value - 0.2;
|
||||
|
||||
sensor:
|
||||
- platform: ina226
|
||||
address: 0x41
|
||||
adc_averaging: 16
|
||||
max_current: 8.192A
|
||||
shunt_resistance: 0.005ohm
|
||||
bus_voltage:
|
||||
id: battery_voltage
|
||||
name: "Battery Voltage"
|
||||
current:
|
||||
id: battery_current
|
||||
name: "Battery Current"
|
||||
# Positive means discharging
|
||||
# Negative means charging
|
||||
|
||||
# Tab5 built-in battery discharges from full (8.23 V) to shutdown threshold (6.0 V)
|
||||
- platform: template
|
||||
name: "Battery Percentage"
|
||||
lambda: |-
|
||||
float voltage = id(battery_voltage).state;
|
||||
// Adjust these values based on your battery's actual min/max voltage
|
||||
float min_voltage = 6.75; // Discharged voltage
|
||||
float max_voltage = 8.2; // Fully charged voltage
|
||||
float percentage = (voltage - min_voltage) / (max_voltage - min_voltage) * 100.0;
|
||||
if (percentage > 100.0) return 100.0;
|
||||
if (percentage < 0.0) return 0.0;
|
||||
return percentage;
|
||||
update_interval: 60s
|
||||
unit_of_measurement: "%"
|
||||
accuracy_decimals: 1
|
||||
id: battery_percent
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.label.update:
|
||||
id: lbl_battery
|
||||
text:
|
||||
format: "Battery: %.1f%%"
|
||||
args: ["id(battery_percent).state"]
|
||||
|
||||
touchscreen:
|
||||
- platform: st7123
|
||||
i2c_id: bsp_bus
|
||||
interrupt_pin: GPIO23
|
||||
display: lcd
|
||||
update_interval: never
|
||||
reset_pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 5
|
||||
calibration:
|
||||
x_min: 0
|
||||
x_max: 720
|
||||
y_min: 0
|
||||
y_max: 1280
|
||||
id: touch
|
||||
on_touch:
|
||||
- logger.log: "LVGL resuming"
|
||||
- lvgl.resume:
|
||||
- light.turn_on: backlight
|
||||
# on_release:
|
||||
# - media_player.stop:
|
||||
|
||||
esp_ldo:
|
||||
- voltage: 2.5V
|
||||
channel: 3
|
||||
|
||||
display:
|
||||
- platform: mipi_dsi
|
||||
id: lcd
|
||||
dimensions:
|
||||
height: 1280
|
||||
width: 720
|
||||
model: M5STACK-TAB5-V2
|
||||
reset_pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 4
|
||||
|
||||
output:
|
||||
- platform: ledc
|
||||
pin: GPIO22
|
||||
id: backlight_pwm
|
||||
frequency: 1000Hz
|
||||
|
||||
light:
|
||||
- platform: monochromatic
|
||||
output: backlight_pwm
|
||||
name: "Display Backlight"
|
||||
id: backlight
|
||||
restore_mode: ALWAYS_ON
|
||||
default_transition_length: 250ms
|
||||
initial_state:
|
||||
brightness: "50%"
|
||||
|
||||
image:
|
||||
defaults:
|
||||
type: rgb565
|
||||
transparency: alpha_channel
|
||||
resize: 512x512
|
||||
byte_order: little_endian
|
||||
images:
|
||||
- file: "images/va_idle.png"
|
||||
id: va_idle
|
||||
- file: "images/va_listen.png"
|
||||
id: va_listen
|
||||
- file: "images/va_speak.png"
|
||||
id: va_speak
|
||||
- file: "images/charging.png"
|
||||
id: charging_icon
|
||||
resize: 64x64
|
||||
|
||||
lvgl:
|
||||
byte_order: little_endian
|
||||
|
||||
on_idle:
|
||||
timeout: 120s
|
||||
then:
|
||||
- logger.log: "LVGL is idle"
|
||||
- light.turn_off:
|
||||
id: backlight
|
||||
transition_length: 15s
|
||||
- lvgl.pause:
|
||||
widgets:
|
||||
- obj:
|
||||
align: TOP_MID
|
||||
width: 100%
|
||||
height: 100%
|
||||
layout:
|
||||
type: flex
|
||||
flex_flow: column
|
||||
flex_align_main: START
|
||||
flex_align_track: center
|
||||
flex_align_cross: center
|
||||
widgets:
|
||||
- label:
|
||||
align: TOP_MID
|
||||
id: lbl_status
|
||||
text_font: montserrat_48
|
||||
text: "CONNECTING..."
|
||||
- label:
|
||||
align: TOP_MID
|
||||
id: lbl_ap
|
||||
text_font: montserrat_22
|
||||
text: "CONNECTING..."
|
||||
- label:
|
||||
align: TOP_MID
|
||||
id: lbl_ip
|
||||
text_font: montserrat_22
|
||||
text: "CONNECTING..."
|
||||
- image:
|
||||
id: listen_icon_widget
|
||||
src: va_idle
|
||||
align: CENTER
|
||||
- label:
|
||||
align: BOTTOM_LEFT
|
||||
id: lbl_version
|
||||
text_font: montserrat_12
|
||||
text: "v0.6"
|
||||
- label:
|
||||
align: BOTTOM_RIGHT
|
||||
id: lbl_battery
|
||||
text_font: montserrat_28
|
||||
text: Loading...
|
||||
- image:
|
||||
id: charging_icon_widget
|
||||
src: charging_icon
|
||||
align: TOP_RIGHT
|
||||
- button:
|
||||
id: volume_up_widget
|
||||
widgets:
|
||||
- label:
|
||||
text: "\uF028"
|
||||
text_font: montserrat_48
|
||||
text_align: CENTER
|
||||
align: CENTER
|
||||
x: 20
|
||||
y: 20
|
||||
width: 100
|
||||
height: 100
|
||||
pad_all: 8
|
||||
- button:
|
||||
id: volume_down_widget
|
||||
widgets:
|
||||
- label:
|
||||
text: "\uF027"
|
||||
text_font: montserrat_48
|
||||
text_align: CENTER
|
||||
align: CENTER
|
||||
x: 20
|
||||
y: 140
|
||||
width: 100
|
||||
height: 100
|
||||
pad_all: 8
|
||||
- label:
|
||||
x: 20
|
||||
y: 260
|
||||
id: lbl_volume
|
||||
text_font: montserrat_28
|
||||
text: !lambda return "%.1f",to_string(id(tab5_media_player).volume);
|
||||
|
||||
# The DAC Output select needs to be manually (or with an automation) changed to `LINE1` for the onboard speaker
|
||||
select:
|
||||
- platform: es8388
|
||||
dac_output:
|
||||
name: DAC Output
|
||||
id: dac_output
|
||||
adc_input_mic:
|
||||
name: ADC Input Mic
|
||||
id: adc_input
|
||||
|
||||
- platform: template
|
||||
id: wifi_antenna_select
|
||||
name: "WiFi Antenna"
|
||||
options:
|
||||
- "Internal"
|
||||
- "External"
|
||||
optimistic: true
|
||||
on_value:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return i == 0;
|
||||
then:
|
||||
- switch.turn_off: wifi_antenna_int_ext
|
||||
else:
|
||||
- switch.turn_on: wifi_antenna_int_ext
|
||||
|
||||
i2s_audio:
|
||||
- id: mic_bus
|
||||
i2s_lrclk_pin: GPIO29
|
||||
i2s_bclk_pin: GPIO27
|
||||
i2s_mclk_pin: GPIO30
|
||||
|
||||
audio_adc:
|
||||
- platform: es7210
|
||||
id: es7210_adc
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: tab5_microphone
|
||||
i2s_din_pin: GPIO28
|
||||
sample_rate: 16000
|
||||
bits_per_sample: 16bit
|
||||
adc_type: external
|
||||
|
||||
audio_dac:
|
||||
- platform: es8388
|
||||
id: es8388_dac
|
||||
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: tab5_speaker
|
||||
i2s_dout_pin: GPIO26
|
||||
audio_dac: es8388_dac
|
||||
dac_type: external
|
||||
channel: mono
|
||||
buffer_duration: 100ms
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 48000
|
||||
|
||||
media_player:
|
||||
- platform: speaker
|
||||
name: None
|
||||
id: tab5_media_player
|
||||
announcement_pipeline:
|
||||
speaker: tab5_speaker
|
||||
format: WAV
|
||||
|
||||
micro_wake_word:
|
||||
id: mww
|
||||
models:
|
||||
- okay_nabu
|
||||
- hey_mycroft
|
||||
- hey_jarvis
|
||||
on_wake_word_detected:
|
||||
- voice_assistant.start:
|
||||
wake_word: !lambda return wake_word;
|
||||
|
||||
voice_assistant:
|
||||
id: va
|
||||
microphone: tab5_microphone
|
||||
media_player: tab5_media_player
|
||||
micro_wake_word: mww
|
||||
on_listening:
|
||||
- logger.log: "LVGL resuming"
|
||||
- lvgl.resume:
|
||||
- light.turn_on: backlight
|
||||
- lvgl.image.update:
|
||||
id: listen_icon_widget
|
||||
src: va_listen
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "LISTENING"
|
||||
on_stt_vad_end:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "PROCESSING"
|
||||
- lvgl.image.update:
|
||||
id: listen_icon_widget
|
||||
src: va_idle
|
||||
on_tts_start:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "RESPONDING"
|
||||
- lvgl.image.update:
|
||||
id: listen_icon_widget
|
||||
src: va_speak
|
||||
on_end:
|
||||
# Wait a short amount of time to see if an announcement starts
|
||||
- wait_until:
|
||||
condition:
|
||||
- media_player.is_announcing:
|
||||
timeout: 0.5s
|
||||
# Announcement is finished and the I2S bus is free
|
||||
- wait_until:
|
||||
- and:
|
||||
- not:
|
||||
media_player.is_announcing:
|
||||
- not:
|
||||
speaker.is_playing:
|
||||
- micro_wake_word.start:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "IDLE"
|
||||
- lvgl.image.update:
|
||||
id: listen_icon_widget
|
||||
src: va_idle
|
||||
- light.turn_off:
|
||||
id: backlight
|
||||
transition_length: 15s
|
||||
on_client_connected:
|
||||
- micro_wake_word.start:
|
||||
on_client_disconnected:
|
||||
- micro_wake_word.stop:
|
||||
@@ -1,6 +1,18 @@
|
||||
esphome:
|
||||
name: tab1
|
||||
friendly_name: M5Stack Tab5 1
|
||||
name: tab2
|
||||
friendly_name: M5Stack Tab5 2
|
||||
on_boot:
|
||||
# Set the charing icon to the correct state on boot
|
||||
- then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(charging).state;
|
||||
then:
|
||||
- lvgl.widget.show:
|
||||
id: charging_icon_widget
|
||||
else:
|
||||
- lvgl.widget.hide:
|
||||
id: charging_icon_widget
|
||||
|
||||
esp32:
|
||||
board: esp32-p4-evboard
|
||||
@@ -43,6 +55,7 @@ ota:
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
fast_connect: true
|
||||
on_connect:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
@@ -54,6 +67,29 @@ wifi:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "DISCONNECTED"
|
||||
# ap:
|
||||
# password: !secret hotspot_password
|
||||
# ap_timeout: 90s
|
||||
|
||||
# captive_portal:
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
timezone: America/New_York
|
||||
servers:
|
||||
- 0.pool.ntp.org
|
||||
- 1.pool.ntp.org
|
||||
- 2.pool.ntp.org
|
||||
|
||||
# wireguard:
|
||||
# address: !secret tab1_wg_ip
|
||||
# private_key: !secret tab1_wg_pk
|
||||
# peer_endpoint: !secret wg_host
|
||||
# peer_public_key: !secret wg_pubkey
|
||||
|
||||
# # Optional keepalive (disabled by default)
|
||||
# peer_persistent_keepalive: 25s
|
||||
|
||||
i2c:
|
||||
- id: bsp_bus
|
||||
@@ -142,6 +178,17 @@ binary_sensor:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 6
|
||||
mode: INPUT_PULLDOWN
|
||||
on_state:
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(charging).state;
|
||||
then:
|
||||
- lvgl.widget.show:
|
||||
id: charging_icon_widget
|
||||
else:
|
||||
- lvgl.widget.hide:
|
||||
id: charging_icon_widget
|
||||
|
||||
- platform: gpio
|
||||
id: headphone_detect
|
||||
@@ -186,7 +233,7 @@ sensor:
|
||||
- lvgl.label.update:
|
||||
id: lbl_battery
|
||||
text:
|
||||
format: "Battery: %.1f%"
|
||||
format: "Battery: %.1f%%"
|
||||
args: ["id(battery_percent).state"]
|
||||
|
||||
touchscreen:
|
||||
@@ -255,6 +302,9 @@ image:
|
||||
id: va_listen
|
||||
- file: "images/va_speak.png"
|
||||
id: va_speak
|
||||
- file: "images/charging.png"
|
||||
id: charging_icon
|
||||
resize: 64x64
|
||||
|
||||
lvgl:
|
||||
byte_order: little_endian
|
||||
@@ -287,6 +337,25 @@ lvgl:
|
||||
id: lbl_battery
|
||||
text_font: montserrat_28
|
||||
text: Loading...
|
||||
- image:
|
||||
id: charging_icon_widget
|
||||
src: charging_icon
|
||||
align: TOP_RIGHT
|
||||
- slider:
|
||||
id: backlight_slider
|
||||
x: 20
|
||||
y: 50
|
||||
width: 30
|
||||
height: 220
|
||||
pad_all: 8
|
||||
min_value: 0
|
||||
max_value: 255
|
||||
on_release:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
data:
|
||||
entity_id: light.backlight
|
||||
brightness: !lambda return int(x);
|
||||
|
||||
# The DAC Output select needs to be manually (or with an automation) changed to `LINE1` for the onboard speaker
|
||||
select:
|
||||