├── components └── es8311 │ ├── __init__.py │ ├── audio_dac.py │ ├── es8311.h │ ├── es8311_const.h │ └── es8311.cpp ├── README.md ├── waveshare_camera_pkg.yaml └── waveshare-s3-audio.yaml /components/es8311/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/es8311/audio_dac.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | from esphome.components import i2c 3 | from esphome.components.audio_dac import AudioDac 4 | import esphome.config_validation as cv 5 | from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_MIC_GAIN, CONF_SAMPLE_RATE 6 | 7 | CODEOWNERS = ["@kroimon", "@kahrendt"] 8 | DEPENDENCIES = ["i2c"] 9 | 10 | es8311_ns = cg.esphome_ns.namespace("es8311") 11 | ES8311 = es8311_ns.class_("ES8311", AudioDac, cg.Component, i2c.I2CDevice) 12 | 13 | CONF_USE_MCLK = "use_mclk" 14 | CONF_USE_MICROPHONE = "use_microphone" 15 | CONF_FORCE_MASTER = "force_master" 16 | CONF_MCLK_MULTIPLE = "mclk_multiple" 17 | 18 | es8311_resolution = es8311_ns.enum("ES8311Resolution") 19 | ES8311_BITS_PER_SAMPLE_ENUM = { 20 | 16: es8311_resolution.ES8311_RESOLUTION_16, 21 | 24: es8311_resolution.ES8311_RESOLUTION_24, 22 | 32: es8311_resolution.ES8311_RESOLUTION_32, 23 | } 24 | 25 | es8311_mic_gain = es8311_ns.enum("ES8311MicGain") 26 | ES8311_MIC_GAIN_ENUM = { 27 | "MIN": es8311_mic_gain.ES8311_MIC_GAIN_MIN, 28 | "0DB": es8311_mic_gain.ES8311_MIC_GAIN_0DB, 29 | "6DB": es8311_mic_gain.ES8311_MIC_GAIN_6DB, 30 | "12DB": es8311_mic_gain.ES8311_MIC_GAIN_12DB, 31 | "18DB": es8311_mic_gain.ES8311_MIC_GAIN_18DB, 32 | "24DB": es8311_mic_gain.ES8311_MIC_GAIN_24DB, 33 | "30DB": es8311_mic_gain.ES8311_MIC_GAIN_30DB, 34 | "36DB": es8311_mic_gain.ES8311_MIC_GAIN_36DB, 35 | "42DB": es8311_mic_gain.ES8311_MIC_GAIN_42DB, 36 | "MAX": es8311_mic_gain.ES8311_MIC_GAIN_MAX, 37 | } 38 | 39 | 40 | _validate_bits = cv.float_with_unit("bits", "bit") 41 | 42 | CONFIG_SCHEMA = ( 43 | cv.Schema( 44 | { 45 | cv.GenerateID(): cv.declare_id(ES8311), 46 | cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All( 47 | _validate_bits, cv.enum(ES8311_BITS_PER_SAMPLE_ENUM) 48 | ), 49 | cv.Optional(CONF_MIC_GAIN, default="42DB"): cv.enum( 50 | ES8311_MIC_GAIN_ENUM, upper=True 51 | ), 52 | cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1), 53 | cv.Optional(CONF_USE_MCLK, default=True): cv.boolean, 54 | cv.Optional(CONF_USE_MICROPHONE, default=False): cv.boolean, 55 | cv.Optional(CONF_MCLK_MULTIPLE, default=256): cv.int_range(min=64, max=1024), 56 | cv.Optional(CONF_FORCE_MASTER, default=False): cv.boolean, 57 | } 58 | ) 59 | .extend(cv.COMPONENT_SCHEMA) 60 | .extend(i2c.i2c_device_schema(0x18)) 61 | ) 62 | 63 | 64 | async def to_code(config): 65 | var = cg.new_Pvariable(config[CONF_ID]) 66 | await cg.register_component(var, config) 67 | await i2c.register_i2c_device(var, config) 68 | 69 | cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) 70 | cg.add(var.set_mic_gain(config[CONF_MIC_GAIN])) 71 | cg.add(var.set_sample_frequency(config[CONF_SAMPLE_RATE])) 72 | cg.add(var.set_use_mclk(config[CONF_USE_MCLK])) 73 | cg.add(var.set_use_mic(config[CONF_USE_MICROPHONE])) 74 | cg.add(var.set_mclk_multiple(config[CONF_MCLK_MULTIPLE])) 75 | cg.add(var.set_force_master(config[CONF_FORCE_MASTER])) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ESPHome configuration for enabeling WAVESHARE-S3-AUDIO-BOARD (https://www.waveshare.com/esp32-s3-audio-board.htm) 2 | to be used as a HomeAssistant Voice Satellite. Features such as simultanious music/and announcements and 3 | continious on-board wake-word detection. 4 | 5 | The device's DAC and ADC share pins for the i2s bus so to be able to configure two i2s_audio points for 6 | simultanious audio in/out I had to path the DAC (es8311) component, adding a setting to force it to become 7 | i2s master while still configuring the ESP and ADC to act as i2s slaves. I also had to fix MCLK/BLCK calculations. 8 | 9 | example code features: 10 | * complete Voice Assistant setup 11 | * Onboard Wake-Word detection 12 | * Led animations and event sounds 13 | * working control buttons 14 | * exposed announcement and music media_players 15 | * built in alarm and timer 16 | 17 | Steps to use the custom ES8311 component: 18 | ``` yaml 19 | substitutions: 20 | i2c_id: internal_i2c 21 | i2s_mclk_multiple: 256 22 | i2s_bps_spk: 16bit 23 | i2s_bps_mic: 16bit 24 | i2s_sample_rate_spk: 16000 25 | i2s_sample_rate_mic: 16000 # must be 16000 for voice_assistant to work(?) 26 | i2s_use_apll: true 27 | 28 | external_components: 29 | - source: 30 | type: git 31 | url: https://github.com/sw3Dan/waveshare-s2-audio_esphome_voice 32 | ref: main 33 | components: [ es8311 ] 34 | refresh: 0s 35 | 36 | i2c: 37 | - id: $i2c_id 38 | sda: GPIO11 39 | scl: GPIO10 40 | scan: true 41 | 42 | i2s_audio: 43 | - id: i2s_output 44 | i2s_lrclk_pin: 45 | number: GPIO14 46 | allow_other_uses: true 47 | i2s_bclk_pin: 48 | number: GPIO13 49 | allow_other_uses: true 50 | i2s_mclk_pin: # <-- for ESP to know what port to configure MCLK output 51 | number: GPIO12 52 | 53 | - id: i2s_input 54 | i2s_lrclk_pin: 55 | number: GPIO14 56 | allow_other_uses: true 57 | i2s_bclk_pin: 58 | number: GPIO13 59 | allow_other_uses: true 60 | 61 | audio_dac: 62 | - platform: es8311 63 | id: es8311_dac 64 | i2c_id: $i2c_id 65 | use_mclk: true 66 | sample_rate: $i2s_sample_rate 67 | bits_per_sample: $i2s_bps_spk 68 | force_master: true # <-- New: to configure device as i2s master 69 | mclk_multiple: i2s_mclk_multiple # <-- New: for the i2s bus MCLK/BLCK calculations 70 | 71 | audio_adc: 72 | - platform: es7210 73 | id: adc_mic 74 | i2c_id: $i2c_id 75 | sample_rate: $i2s_sample_rate 76 | bits_per_sample: $i2s_bps_mic 77 | 78 | microphone: 79 | - platform: i2s_audio 80 | id: i2s_mics 81 | i2s_din_pin: GPIO15 82 | adc_type: external 83 | pdm: false 84 | i2s_audio_id: i2s_input 85 | i2s_mode: secondary # <-- so that ESP wont generate LRCLK/BLCK 86 | mclk_multiple: $i2s_mclk_multiple 87 | sample_rate: $i2s_sample_rate_mic # must be 16000 for VA (?) 88 | bits_per_sample: $i2s_bps_mic 89 | 90 | speaker: 91 | - platform: i2s_audio 92 | id: i2s_audio_speaker 93 | i2s_dout_pin: GPIO16 94 | i2s_audio_id: i2s_output 95 | i2s_mode: secondary # component patch will force ES8311 to take command 96 | dac_type: external 97 | timeout: never 98 | buffer_duration: 100ms 99 | audio_dac: es8311_dac 100 | sample_rate: $i2s_sample_rate_spk 101 | bits_per_sample: $i2s_bps_spk 102 | use_apll: $i2s_use_apll # dont know if this is enforced when in 'secondary' i2s mode 103 | mclk_multiple: $i2s_mclk_multiple 104 | channel: stereo 105 | 106 | - platform: mixer 107 | id: mixing_speaker 108 | output_speaker: i2s_audio_speaker 109 | num_channels: 2 110 | source_speakers: 111 | 112 | ** Then create speaker: speaker/resamplers as needed ** 113 | ``` 114 | 115 | If you need a camera include this yaml package. 116 | This package comes with a preconfigured camera and a bunch of camera control is exposed to HomeAssistant. 117 | ``` yaml 118 | packages: 119 | remote_package_shorthand: github://esphome/sw3Dan/waveshare-s2-audio_esphome_voice/waveshare_camera_pkg.yaml@main 120 | ``` 121 | 122 | General TODO/wishlist for the device. 123 | * filter speaker output from mic input using IDF-SR library (IN PROGRESS!) 124 | * UI: disable LEDS 125 | * UI: disable Voice/wake-word 126 | * UI/ESP: Forward output to external speaker (music assistant announcement support?) 127 | * ESP: lower volume on room speakers when wake-word detected 128 | * UI: expose mic and model sensitivity calibration settings 129 | * UI: select buttons behavior 130 | * ESP: long-press (volume) and double-click (deactivate Voice) button support 131 | * ESP: Volume percentage Led animation 132 | * ESP: rotary rainbow - assistant working 133 | * ESP: alarm flash led animation 134 | * ESP: pulsing red - no connection led animation 135 | * ESP: code cleanup - change code structure and split into packages 136 | * UI: structure/names 137 | * ESP: mmWave module (control mic activation to limit room exposure) 138 | * ESP: stream/listen to audio (mic output volume trigger) 139 | * ESP: fix RTC 140 | * UI: set TimeZone 141 | -------------------------------------------------------------------------------- /components/es8311/es8311.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/components/audio_dac/audio_dac.h" 4 | #include "esphome/components/i2c/i2c.h" 5 | #include "esphome/core/component.h" 6 | 7 | namespace esphome { 8 | namespace es8311 { 9 | 10 | enum ES8311MicGain { 11 | ES8311_MIC_GAIN_MIN = -1, 12 | ES8311_MIC_GAIN_0DB, 13 | ES8311_MIC_GAIN_6DB, 14 | ES8311_MIC_GAIN_12DB, 15 | ES8311_MIC_GAIN_18DB, 16 | ES8311_MIC_GAIN_24DB, 17 | ES8311_MIC_GAIN_30DB, 18 | ES8311_MIC_GAIN_36DB, 19 | ES8311_MIC_GAIN_42DB, 20 | ES8311_MIC_GAIN_MAX 21 | }; 22 | 23 | enum ES8311Resolution : uint8_t { 24 | ES8311_RESOLUTION_16 = 16, 25 | ES8311_RESOLUTION_18 = 18, 26 | ES8311_RESOLUTION_20 = 20, 27 | ES8311_RESOLUTION_24 = 24, 28 | ES8311_RESOLUTION_32 = 32 29 | }; 30 | 31 | struct ES8311Coefficient { 32 | uint32_t mclk; // mclk frequency 33 | uint32_t rate; // sample rate 34 | uint8_t pre_div; // the pre divider with range from 1 to 8 35 | uint8_t pre_mult; // the pre multiplier with x1, x2, x4 and x8 selection 36 | uint8_t adc_div; // adcclk divider 37 | uint8_t dac_div; // dacclk divider 38 | uint8_t fs_mode; // single speed (0) or double speed (1) 39 | uint8_t lrck_h; // adc lrck divider and dac lrck divider 40 | uint8_t lrck_l; // 41 | uint8_t bclk_div; // sclk divider 42 | uint8_t adc_osr; // adc osr 43 | uint8_t dac_osr; // dac osr 44 | }; 45 | 46 | class ES8311 : public audio_dac::AudioDac, public Component, public i2c::I2CDevice { 47 | public: 48 | ///////////////////////// 49 | // Component overrides // 50 | ///////////////////////// 51 | 52 | void setup() override; 53 | void dump_config() override; 54 | 55 | //////////////////////// 56 | // AudioDac overrides // 57 | //////////////////////// 58 | 59 | /// @brief Writes the volume out to the DAC 60 | /// @param volume floating point between 0.0 and 1.0 61 | /// @return True if successful and false otherwise 62 | bool set_volume(float volume) override; 63 | 64 | /// @brief Gets the current volume out from the DAC 65 | /// @return floating point between 0.0 and 1.0 66 | float volume() override; 67 | 68 | /// @brief Disables mute for audio out 69 | /// @return True if successful and false otherwise 70 | bool set_mute_off() override { return this->set_mute_state_(false); } 71 | 72 | /// @brief Enables mute for audio out 73 | /// @return True if successful and false otherwise 74 | bool set_mute_on() override { return this->set_mute_state_(true); } 75 | 76 | bool is_muted() override { return this->is_muted_; } 77 | 78 | ////////////////////////////////// 79 | // ES8311 configuration setters // 80 | ////////////////////////////////// 81 | 82 | void set_use_mclk(bool use_mclk) { this->use_mclk_ = use_mclk; } 83 | void set_bits_per_sample(ES8311Resolution resolution) { 84 | this->resolution_in_ = resolution; 85 | this->resolution_out_ = resolution; 86 | } 87 | void set_sample_frequency(uint32_t sample_frequency) { this->sample_frequency_ = sample_frequency; } 88 | void set_mclk_multiple(uint16_t v) { this->mclk_multiple_ = v; } 89 | void set_use_mic(bool use_mic) { this->use_mic_ = use_mic; } 90 | void set_mic_gain(ES8311MicGain mic_gain) { this->mic_gain_ = mic_gain; } 91 | void set_force_master(bool v) { this->force_master_ = v; } 92 | 93 | protected: 94 | /// @brief Computes the register value for the configured resolution (bits per sample) 95 | /// @param resolution bits per sample enum for both audio in and audio out 96 | /// @return register value 97 | static uint8_t calculate_resolution_value(ES8311Resolution resolution); 98 | 99 | /// @brief Retrieves the appropriate registers values for the configured mclk and rate 100 | /// @param mclk mlck frequency in Hz 101 | /// @param rate sample rate frequency in Hz 102 | /// @return ES8311Coeffecient containing appropriate register values to configure the ES8311 or nullptr if impossible 103 | static const ES8311Coefficient *get_coefficient(uint32_t mclk, uint32_t rate); 104 | 105 | /// @brief Configures the ES8311 registers for the chosen sample rate 106 | /// @return True if successful and false otherwise 107 | bool configure_clock_(); 108 | 109 | /// @brief Configures the ES8311 registers for the chosen bits per sample 110 | /// @return True if successful and false otherwise 111 | bool configure_format_(); 112 | 113 | /// @brief Configures the ES8311 microphone registers 114 | /// @return True if successful and false otherwise 115 | bool configure_mic_(); 116 | 117 | /// @brief Mutes or unmute the DAC audio out 118 | /// @param mute_state True to mute, false to unmute 119 | /// @return 120 | bool set_mute_state_(bool mute_state); 121 | 122 | bool use_mic_; 123 | ES8311MicGain mic_gain_; 124 | 125 | bool force_master_{false}; // true = set i2s mode as master 126 | 127 | bool use_mclk_; // true = use dedicated MCLK pin, false = use SCLK 128 | bool sclk_inverted_{false}; // SCLK is inverted 129 | bool mclk_inverted_{false}; // MCLK is inverted (ignored if use_mclk_ == false) 130 | uint32_t mclk_multiple_{256}; // MCLK frequency is sample rate * mclk_multiple_ (ignored if use_mclk_ == false) 131 | 132 | uint32_t sample_frequency_; // in Hz 133 | ES8311Resolution resolution_in_; 134 | ES8311Resolution resolution_out_; 135 | }; 136 | 137 | } // namespace es8311 138 | } // namespace esphome -------------------------------------------------------------------------------- /waveshare_camera_pkg.yaml: -------------------------------------------------------------------------------- 1 | 2 | substitutions: 3 | # Pin definitions 4 | i2c_id: internal_i2c 5 | reset_pin: 6 | tca9555: ioexp 7 | number: 7 # EXIO7 8 | inverted: true 9 | camera_data_pins: [GPIO2, GPIO17, GPIO18, GPIO39, GPIO45, GPIO46, GPIO47, GPIO48] 10 | camera_vsync_pin: GPIO21 11 | camera_href_pin: GPIO1 12 | camera_pclk_pin: GPIO44 13 | camera_xclk_pin: GPIO43 #External clock 14 | camera_power_pin: 15 | tca9555: ioexp 16 | number: 6 #EXIO6 17 | inverted: true 18 | camera_max_framerate: '15fps' 19 | camera_jpeg_quality: '10' 20 | camera_vertical_flip: 'true' 21 | camera_horizontal_mirror: 'true' 22 | camera_contrast: '0' 23 | camera_brightness: '0' 24 | camera_saturation: '0' 25 | camera_special_effect: 'none' 26 | camera_aec_mode: 'auto' 27 | camera_aec2: 'false' 28 | camera_ae_level: '0' 29 | camera_aec_value: '300' 30 | camera_agc_mode: 'auto' 31 | camera_agc_value: '0' 32 | camera_agc_ceiling: '2x' 33 | camera_white_balance: 'auto' 34 | 35 | #TODO: make HA switches 36 | camera_xclk_frequency: 12MHz 37 | camera_idle_framerate: 0fps 38 | 39 | esphome: 40 | on_boot: 41 | - priority: -100 42 | then: 43 | - if: 44 | condition: 45 | lambda: 'return id(image_rotated);' 46 | then: 47 | - lambda: |- 48 | sensor_t *s = esp_camera_sensor_get(); 49 | if (s != nullptr) { 50 | s->set_vflip(s, 1); 51 | s->set_hmirror(s, 1); 52 | } 53 | - switch.turn_on: camera_rotation_switch 54 | - if: 55 | condition: 56 | lambda: 'return id(auto_exposure_enabled);' 57 | then: 58 | - lambda: |- 59 | sensor_t *s = esp_camera_sensor_get(); 60 | if (s != nullptr) { s->set_exposure_ctrl(s, 1); } 61 | - switch.turn_on: camera_aec_switch 62 | else: 63 | - lambda: |- 64 | sensor_t *s = esp_camera_sensor_get(); 65 | if (s != nullptr) { s->set_exposure_ctrl(s, 0); } 66 | - switch.turn_off: camera_aec_switch 67 | 68 | globals: 69 | # TODO: fuse substitution set settings to boot settings 70 | - id: image_rotated 71 | type: bool 72 | restore_value: yes 73 | initial_value: 'false' 74 | 75 | - id: auto_exposure_enabled 76 | type: bool 77 | restore_value: yes 78 | initial_value: 'true' # Default to ON 79 | 80 | # Camera configuration 81 | esp32_camera: 82 | name: "ESP32 Camera" 83 | id: seeed_camera 84 | data_pins: $camera_data_pins 85 | vsync_pin: $camera_vsync_pin 86 | href_pin: $camera_href_pin 87 | pixel_clock_pin: $camera_pclk_pin 88 | external_clock: 89 | pin: $camera_xclk_pin 90 | frequency: $camera_xclk_frequency #12MHz 91 | i2c_id: $i2c_id 92 | #power_down_pin: ${camera_power_pin} 93 | 94 | #frame settings 95 | max_framerate: ${camera_max_framerate} 96 | idle_framerate: ${camera_idle_framerate} #0.1fps #default 97 | 98 | #Image Settings 99 | resolution: 1280X1024 100 | jpeg_quality: ${camera_jpeg_quality} 101 | vertical_flip: ${camera_vertical_flip} 102 | horizontal_mirror: ${camera_horizontal_mirror} 103 | brightness: ${camera_brightness} 104 | contrast: ${camera_contrast} 105 | saturation: ${camera_saturation} 106 | special_effect: ${camera_special_effect} 107 | 108 | #Exposure settings 109 | aec_mode: ${camera_aec_mode} 110 | aec2: ${camera_aec2} 111 | ae_level: ${camera_ae_level} 112 | aec_value: ${camera_ae_level} 113 | 114 | #Sensor gain settings 115 | agc_mode: ${camera_aec_mode} 116 | agc_gain_ceiling: ${camera_agc_ceiling} 117 | agc_value: ${camera_agc_value} 118 | 119 | #White balance settings 120 | wb_mode: ${camera_white_balance} 121 | 122 | sensor: 123 | - platform: homeassistant 124 | id: camera_jpeg_quality 125 | entity_id: input_number.camera_jpeg_quality 126 | 127 | - platform: homeassistant 128 | id: camera_vertical_flip 129 | entity_id: input_boolean.camera_vertical_flip 130 | 131 | - platform: homeassistant 132 | id: camera_horizontal_mirror 133 | entity_id: input_boolean.camera_horizontal_mirror 134 | 135 | - platform: homeassistant 136 | id: camera_brightness 137 | entity_id: input_number.camera_brightness 138 | 139 | - platform: homeassistant 140 | id: camera_contrast 141 | entity_id: input_number.camera_contrast 142 | 143 | - platform: homeassistant 144 | id: camera_saturation 145 | entity_id: input_number.camera_saturation 146 | 147 | - platform: homeassistant 148 | id: camera_special_effect 149 | entity_id: input_select.camera_special_effect 150 | 151 | - platform: homeassistant 152 | id: camera_aec_mode 153 | entity_id: input_select.camera_aec_mode 154 | 155 | - platform: homeassistant 156 | id: camera_aec2 157 | entity_id: input_boolean.camera_aec2 158 | 159 | - platform: homeassistant 160 | id: camera_ae_level 161 | entity_id: input_number.camera_ae_level 162 | 163 | - platform: homeassistant 164 | id: camera_ae_value 165 | entity_id: input_number.camera_ae_value 166 | 167 | - platform: homeassistant 168 | id: camera_agc_mode 169 | entity_id: input_select.camera_agc_mode 170 | 171 | - platform: homeassistant 172 | id: camera_agc_ceiling 173 | entity_id: input_number.camera_agc_ceiling 174 | 175 | - platform: homeassistant 176 | id: camera_agc_value 177 | entity_id: input_number.camera_agc_value 178 | 179 | - platform: homeassistant 180 | id: camera_white_balance 181 | entity_id: input_select.camera_white_balance 182 | 183 | select: 184 | - platform: template 185 | name: "Camera Resolution" 186 | id: camera_resolution_select 187 | icon: "mdi:aspect-ratio" 188 | options: ["QVGA (320x240)", "VGA (640x480)", "SVGA (800x600)", "XGA (1024x768)", "UXGA (1600x1200)"] 189 | set_action: 190 | - lambda: |- 191 | sensor_t *s = esp_camera_sensor_get(); 192 | if (s == nullptr) { return; } 193 | framesize_t new_framesize = FRAMESIZE_INVALID; 194 | if (x == "QVGA (320x240)") new_framesize = FRAMESIZE_QVGA; 195 | else if (x == "VGA (640x480)") new_framesize = FRAMESIZE_VGA; 196 | else if (x == "SVGA (800x600)") new_framesize = FRAMESIZE_SVGA; 197 | else if (x == "XGA (1024x768)") new_framesize = FRAMESIZE_XGA; 198 | else if (x == "UXGA (1600x1200)") new_framesize = FRAMESIZE_UXGA; 199 | if (new_framesize != FRAMESIZE_INVALID && s->set_framesize(s, new_framesize) == 0) { 200 | id(camera_resolution_select).publish_state(x); 201 | id(current_resolution).publish_state(x); 202 | } 203 | 204 | - platform: template 205 | name: "JPEG Quality" 206 | id: camera_jpeg_quality_select 207 | icon: "mdi:image-size-select-large" 208 | options: ["Best (10)", "Good (15)", "Medium (25)", "Low (40)", "Fastest (63)"] 209 | set_action: 210 | - lambda: |- 211 | sensor_t *s = esp_camera_sensor_get(); 212 | if (s == nullptr) { return; } 213 | size_t first = x.find("("); 214 | size_t last = x.find(")"); 215 | int quality = std::stoi(x.substr(first + 1, last - first - 1)); 216 | if (s->set_quality(s, quality) == 0) { 217 | id(camera_jpeg_quality_select).publish_state(x); 218 | id(current_jpeg_quality).publish_state(x); 219 | } 220 | 221 | text_sensor: 222 | - platform: template 223 | name: "Current Camera Resolution" 224 | id: current_resolution 225 | icon: "mdi:camera-control" 226 | - platform: template 227 | name: "Current JPEG Quality" 228 | id: current_jpeg_quality 229 | icon: "mdi:quality-high" 230 | 231 | switch: 232 | - platform: template 233 | name: "Camera Control" 234 | id: camera_control_template 235 | optimistic: True 236 | turn_on_action: 237 | then: 238 | - switch.turn_on: camera_power_output 239 | turn_off_action: 240 | then: 241 | - switch.turn_off: camera_power_output 242 | 243 | - platform: restart 244 | id: seecd_restart 245 | name: "Camera Restart" 246 | 247 | - platform: gpio 248 | pin: ${camera_power_pin} 249 | id: camera_power_output 250 | name: "camera output" 251 | 252 | - platform: template 253 | name: "Reset Camera" 254 | id: reset_camera_settings 255 | turn_on_action: # Reset values to default 256 | - lambda: |- 257 | // Set camera to default values using substitutions and proper conversions 258 | id(seeed_camera).set_contrast(0); 259 | id(seeed_camera).set_brightness(0); 260 | - switch.turn_off: reset_camera_settings # Ensure the switch is momentary 261 | 262 | - platform: template 263 | name: "Rotate Image 180°" 264 | id: camera_rotation_switch 265 | icon: "mdi:rotate-3d-variant" 266 | lambda: 'return id(image_rotated);' 267 | turn_on_action: 268 | - lambda: |- 269 | sensor_t *s = esp_camera_sensor_get(); 270 | if (s != nullptr) { 271 | s->set_vflip(s, 1); 272 | s->set_hmirror(s, 1); 273 | id(image_rotated) = true; 274 | } 275 | turn_off_action: 276 | - lambda: |- 277 | sensor_t *s = esp_camera_sensor_get(); 278 | if (s != nullptr) { 279 | s->set_vflip(s, 0); 280 | s->set_hmirror(s, 0); 281 | id(image_rotated) = false; 282 | } 283 | 284 | - platform: template 285 | name: "Auto Exposure Control" 286 | id: camera_aec_switch 287 | icon: "mdi:auto-fix-high" 288 | lambda: 'return id(auto_exposure_enabled);' 289 | turn_on_action: 290 | - lambda: |- 291 | sensor_t *s = esp_camera_sensor_get(); 292 | if (s != nullptr) { 293 | s->set_exposure_ctrl(s, 1); 294 | id(auto_exposure_enabled) = true; 295 | } 296 | turn_off_action: 297 | - lambda: |- 298 | sensor_t *s = esp_camera_sensor_get(); 299 | if (s != nullptr) { 300 | s->set_exposure_ctrl(s, 0); 301 | id(auto_exposure_enabled) = false; 302 | } 303 | 304 | number: 305 | - platform: template 306 | name: "AE Level" 307 | id: camera_ae_level_number 308 | icon: "mdi:exposure" 309 | optimistic: true 310 | min_value: -2 311 | max_value: 2 312 | step: 1 313 | mode: slider 314 | set_action: 315 | - lambda: |- 316 | sensor_t *s = esp_camera_sensor_get(); 317 | if (s != nullptr) { 318 | s->set_ae_level(s, (int)x); 319 | } 320 | 321 | - platform: template 322 | name: "Manual Exposure Time" 323 | id: camera_aec_value_number 324 | icon: "mdi:camera-timer" 325 | optimistic: true 326 | min_value: 0 327 | max_value: 1200 328 | step: 1 329 | mode: slider 330 | set_action: 331 | - lambda: |- 332 | sensor_t *s = esp_camera_sensor_get(); 333 | if (s != nullptr) { 334 | s->set_aec_value(s, (int)x); 335 | } 336 | -------------------------------------------------------------------------------- /components/es8311/es8311_const.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "es8311.h" 4 | 5 | namespace esphome { 6 | namespace es8311 { 7 | 8 | // ES8311 register addresses 9 | static const uint8_t ES8311_REG00_RESET = 0x00; // Reset 10 | static const uint8_t ES8311_REG01_CLK_MANAGER = 0x01; // Clock Manager: select clk src for mclk, enable clock for codec 11 | static const uint8_t ES8311_REG02_CLK_MANAGER = 0x02; // Clock Manager: clk divider and clk multiplier 12 | static const uint8_t ES8311_REG03_CLK_MANAGER = 0x03; // Clock Manager: adc fsmode and osr 13 | static const uint8_t ES8311_REG04_CLK_MANAGER = 0x04; // Clock Manager: dac osr 14 | static const uint8_t ES8311_REG05_CLK_MANAGER = 0x05; // Clock Manager: clk divider for adc and dac 15 | static const uint8_t ES8311_REG06_CLK_MANAGER = 0x06; // Clock Manager: bclk inverter BIT(5) and divider 16 | static const uint8_t ES8311_REG07_CLK_MANAGER = 0x07; // Clock Manager: tri-state, lrck divider 17 | static const uint8_t ES8311_REG08_CLK_MANAGER = 0x08; // Clock Manager: lrck divider 18 | static const uint8_t ES8311_REG09_SDPIN = 0x09; // Serial Digital Port: DAC 19 | static const uint8_t ES8311_REG0A_SDPOUT = 0x0A; // Serial Digital Port: ADC 20 | static const uint8_t ES8311_REG0B_SYSTEM = 0x0B; // System 21 | static const uint8_t ES8311_REG0C_SYSTEM = 0x0C; // System 22 | static const uint8_t ES8311_REG0D_SYSTEM = 0x0D; // System: power up/down 23 | static const uint8_t ES8311_REG0E_SYSTEM = 0x0E; // System: power up/down 24 | static const uint8_t ES8311_REG0F_SYSTEM = 0x0F; // System: low power 25 | static const uint8_t ES8311_REG10_SYSTEM = 0x10; // System 26 | static const uint8_t ES8311_REG11_SYSTEM = 0x11; // System 27 | static const uint8_t ES8311_REG12_SYSTEM = 0x12; // System: Enable DAC 28 | static const uint8_t ES8311_REG13_SYSTEM = 0x13; // System 29 | static const uint8_t ES8311_REG14_SYSTEM = 0x14; // System: select DMIC, select analog pga gain 30 | static const uint8_t ES8311_REG15_ADC = 0x15; // ADC: adc ramp rate, dmic sense 31 | static const uint8_t ES8311_REG16_ADC = 0x16; // ADC 32 | static const uint8_t ES8311_REG17_ADC = 0x17; // ADC: volume 33 | static const uint8_t ES8311_REG18_ADC = 0x18; // ADC: alc enable and winsize 34 | static const uint8_t ES8311_REG19_ADC = 0x19; // ADC: alc maxlevel 35 | static const uint8_t ES8311_REG1A_ADC = 0x1A; // ADC: alc automute 36 | static const uint8_t ES8311_REG1B_ADC = 0x1B; // ADC: alc automute, adc hpf s1 37 | static const uint8_t ES8311_REG1C_ADC = 0x1C; // ADC: equalizer, hpf s2 38 | static const uint8_t ES8311_REG1D_ADCEQ = 0x1D; // ADCEQ: equalizer B0 39 | static const uint8_t ES8311_REG1E_ADCEQ = 0x1E; // ADCEQ: equalizer B0 40 | static const uint8_t ES8311_REG1F_ADCEQ = 0x1F; // ADCEQ: equalizer B0 41 | static const uint8_t ES8311_REG20_ADCEQ = 0x20; // ADCEQ: equalizer B0 42 | static const uint8_t ES8311_REG21_ADCEQ = 0x21; // ADCEQ: equalizer A1 43 | static const uint8_t ES8311_REG22_ADCEQ = 0x22; // ADCEQ: equalizer A1 44 | static const uint8_t ES8311_REG23_ADCEQ = 0x23; // ADCEQ: equalizer A1 45 | static const uint8_t ES8311_REG24_ADCEQ = 0x24; // ADCEQ: equalizer A1 46 | static const uint8_t ES8311_REG25_ADCEQ = 0x25; // ADCEQ: equalizer A2 47 | static const uint8_t ES8311_REG26_ADCEQ = 0x26; // ADCEQ: equalizer A2 48 | static const uint8_t ES8311_REG27_ADCEQ = 0x27; // ADCEQ: equalizer A2 49 | static const uint8_t ES8311_REG28_ADCEQ = 0x28; // ADCEQ: equalizer A2 50 | static const uint8_t ES8311_REG29_ADCEQ = 0x29; // ADCEQ: equalizer B1 51 | static const uint8_t ES8311_REG2A_ADCEQ = 0x2A; // ADCEQ: equalizer B1 52 | static const uint8_t ES8311_REG2B_ADCEQ = 0x2B; // ADCEQ: equalizer B1 53 | static const uint8_t ES8311_REG2C_ADCEQ = 0x2C; // ADCEQ: equalizer B1 54 | static const uint8_t ES8311_REG2D_ADCEQ = 0x2D; // ADCEQ: equalizer B2 55 | static const uint8_t ES8311_REG2E_ADCEQ = 0x2E; // ADCEQ: equalizer B2 56 | static const uint8_t ES8311_REG2F_ADCEQ = 0x2F; // ADCEQ: equalizer B2 57 | static const uint8_t ES8311_REG30_ADCEQ = 0x30; // ADCEQ: equalizer B2 58 | static const uint8_t ES8311_REG31_DAC = 0x31; // DAC: mute 59 | static const uint8_t ES8311_REG32_DAC = 0x32; // DAC: volume 60 | static const uint8_t ES8311_REG33_DAC = 0x33; // DAC: offset 61 | static const uint8_t ES8311_REG34_DAC = 0x34; // DAC: drc enable, drc winsize 62 | static const uint8_t ES8311_REG35_DAC = 0x35; // DAC: drc maxlevel, minilevel 63 | static const uint8_t ES8311_REG36_DAC = 0x36; // DAC 64 | static const uint8_t ES8311_REG37_DAC = 0x37; // DAC: ramprate 65 | static const uint8_t ES8311_REG38_DACEQ = 0x38; // DACEQ: equalizer B0 66 | static const uint8_t ES8311_REG39_DACEQ = 0x39; // DACEQ: equalizer B0 67 | static const uint8_t ES8311_REG3A_DACEQ = 0x3A; // DACEQ: equalizer B0 68 | static const uint8_t ES8311_REG3B_DACEQ = 0x3B; // DACEQ: equalizer B0 69 | static const uint8_t ES8311_REG3C_DACEQ = 0x3C; // DACEQ: equalizer B1 70 | static const uint8_t ES8311_REG3D_DACEQ = 0x3D; // DACEQ: equalizer B1 71 | static const uint8_t ES8311_REG3E_DACEQ = 0x3E; // DACEQ: equalizer B1 72 | static const uint8_t ES8311_REG3F_DACEQ = 0x3F; // DACEQ: equalizer B1 73 | static const uint8_t ES8311_REG40_DACEQ = 0x40; // DACEQ: equalizer A1 74 | static const uint8_t ES8311_REG41_DACEQ = 0x41; // DACEQ: equalizer A1 75 | static const uint8_t ES8311_REG42_DACEQ = 0x42; // DACEQ: equalizer A1 76 | static const uint8_t ES8311_REG43_DACEQ = 0x43; // DACEQ: equalizer A1 77 | static const uint8_t ES8311_REG44_GPIO = 0x44; // GPIO: dac2adc for test 78 | static const uint8_t ES8311_REG45_GP = 0x45; // GPIO: GP control 79 | static const uint8_t ES8311_REGFA_I2C = 0xFA; // I2C: reset registers 80 | static const uint8_t ES8311_REGFC_FLAG = 0xFC; // Flag 81 | static const uint8_t ES8311_REGFD_CHD1 = 0xFD; // Chip: ID1 82 | static const uint8_t ES8311_REGFE_CHD2 = 0xFE; // Chip: ID2 83 | static const uint8_t ES8311_REGFF_CHVER = 0xFF; // Chip: Version 84 | 85 | // ES8311 clock divider coefficients 86 | static const ES8311Coefficient ES8311_COEFFICIENTS[] = { 87 | // clang-format off 88 | 89 | // mclk, rate, pre_ pre_ adc_ dac_ fs_ lrck lrck bclk_ adc_ dac_ 90 | // div, mult, div, div, mode, _h, _l, div, osr, osr 91 | 92 | // 8k 93 | {12288000, 8000, 0x06, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 94 | {18432000, 8000, 0x03, 0x02, 0x03, 0x03, 0x00, 0x05, 0xff, 0x18, 0x10, 0x20}, 95 | {16384000, 8000, 0x08, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 96 | { 8192000, 8000, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 97 | { 6144000, 8000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 98 | { 4096000, 8000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 99 | { 3072000, 8000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 100 | { 2048000, 8000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 101 | { 1536000, 8000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 102 | { 1024000, 8000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 103 | 104 | // 11.025k 105 | {11289600, 11025, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 106 | { 5644800, 11025, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 107 | { 2822400, 11025, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 108 | { 1411200, 11025, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 109 | 110 | // 12k 111 | {12288000, 12000, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 112 | { 6144000, 12000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 113 | { 3072000, 12000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 114 | { 1536000, 12000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 115 | 116 | // 16k 117 | {12288000, 16000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 118 | {18432000, 16000, 0x03, 0x02, 0x03, 0x03, 0x00, 0x02, 0xff, 0x0c, 0x10, 0x20}, 119 | {16384000, 16000, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 120 | { 8192000, 16000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 121 | { 6144000, 16000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 122 | { 4096000, 16000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 123 | { 3072000, 16000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 124 | { 2048000, 16000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 125 | { 1536000, 16000, 0x03, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 126 | { 1024000, 16000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, 127 | 128 | // 22.05k 129 | {11289600, 22050, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 130 | { 5644800, 22050, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 131 | { 2822400, 22050, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 132 | { 1411200, 22050, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 133 | 134 | // 24k 135 | {12288000, 24000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 136 | {18432000, 24000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 137 | { 6144000, 24000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 138 | { 3072000, 24000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 139 | { 1536000, 24000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 140 | 141 | // 32k 142 | {12288000, 32000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 143 | {18432000, 32000, 0x03, 0x04, 0x03, 0x03, 0x00, 0x02, 0xff, 0x0c, 0x10, 0x10}, 144 | {16384000, 32000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 145 | { 8192000, 32000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 146 | { 6144000, 32000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 147 | { 4096000, 32000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 148 | { 3072000, 32000, 0x03, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 149 | { 2048000, 32000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 150 | { 1536000, 32000, 0x03, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10}, 151 | { 1024000, 32000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 152 | 153 | // 44.1k 154 | {11289600, 44100, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 155 | { 5644800, 44100, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 156 | { 2822400, 44100, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 157 | { 1411200, 44100, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 158 | 159 | // 48k 160 | {12288000, 48000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 161 | {18432000, 48000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 162 | { 6144000, 48000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 163 | { 3072000, 48000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 164 | { 1536000, 48000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 165 | 166 | // 64k 167 | {12288000, 64000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 168 | {18432000, 64000, 0x03, 0x04, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10}, 169 | {16384000, 64000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 170 | { 8192000, 64000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 171 | { 6144000, 64000, 0x01, 0x04, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10}, 172 | { 4096000, 64000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 173 | { 3072000, 64000, 0x01, 0x08, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10}, 174 | { 2048000, 64000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 175 | { 1536000, 64000, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0xbf, 0x03, 0x18, 0x18}, 176 | { 1024000, 64000, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10}, 177 | 178 | // 88.2k 179 | {11289600, 88200, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 180 | { 5644800, 88200, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 181 | { 2822400, 88200, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 182 | { 1411200, 88200, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10}, 183 | 184 | // 96k 185 | {12288000, 96000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 186 | {18432000, 96000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 187 | { 6144000, 96000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 188 | { 3072000, 96000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, 189 | { 1536000, 96000, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10}, 190 | 191 | // clang-format on 192 | }; 193 | 194 | } // namespace es8311 195 | } // namespace esphome -------------------------------------------------------------------------------- /components/es8311/es8311.cpp: -------------------------------------------------------------------------------- 1 | #include "es8311.h" 2 | #include "es8311_const.h" 3 | #include "esphome/core/hal.h" 4 | #include "esphome/core/log.h" 5 | #include 6 | 7 | namespace esphome { 8 | namespace es8311 { 9 | 10 | static const char *const TAG = "es8311"; 11 | 12 | // Mark the component as failed; use only in setup 13 | #define ES8311_ERROR_FAILED(func) \ 14 | if (!(func)) { \ 15 | this->mark_failed(); \ 16 | return; \ 17 | } 18 | // Return false; use outside of setup 19 | #define ES8311_ERROR_CHECK(func) \ 20 | if (!(func)) { \ 21 | return false; \ 22 | } 23 | 24 | void ES8311::setup() { 25 | // Reset 26 | ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x1F)); 27 | ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x00)); 28 | 29 | ES8311_ERROR_FAILED(this->configure_clock_()); 30 | ES8311_ERROR_FAILED(this->configure_format_()); 31 | ES8311_ERROR_FAILED(this->configure_mic_()); 32 | 33 | // Set initial volume 34 | this->set_volume(0.75); // 0.75 = 0xBF = 0dB 35 | 36 | // Power up analog circuitry 37 | ES8311_ERROR_FAILED(this->write_byte(ES8311_REG0D_SYSTEM, 0x01)); 38 | // Enable analog PGA, enable ADC modulator 39 | ES8311_ERROR_FAILED(this->write_byte(ES8311_REG0E_SYSTEM, 0x02)); 40 | // Power up DAC 41 | ES8311_ERROR_FAILED(this->write_byte(ES8311_REG12_SYSTEM, 0x00)); 42 | // Enable output to HP drive 43 | ES8311_ERROR_FAILED(this->write_byte(ES8311_REG13_SYSTEM, 0x10)); 44 | // ADC Equalizer bypass, cancel DC offset in digital domain 45 | ES8311_ERROR_FAILED(this->write_byte(ES8311_REG1C_ADC, 0x6A)); 46 | // Bypass DAC equalizer 47 | ES8311_ERROR_FAILED(this->write_byte(ES8311_REG37_DAC, 0x08)); 48 | // Power On 49 | ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x80)); 50 | // Power On (preserve/keep MSC if we forced master) 51 | uint8_t reg00_po = 0; 52 | ES8311_ERROR_FAILED(this->read_byte(ES8311_REG00_RESET, ®00_po)); 53 | reg00_po |= 0x80; // power on bit 54 | if (this->force_master_) { 55 | reg00_po |= 0x40; // keep MSC=1 (master) 56 | } 57 | ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, reg00_po)); 58 | } 59 | 60 | void ES8311::dump_config() { 61 | ESP_LOGCONFIG(TAG, 62 | "ES8311 Audio Codec:\n" 63 | " Use MCLK: %s\n" 64 | " Use Microphone: %s\n" 65 | " DAC Bits per Sample: %" PRIu8 "\n" 66 | " Sample Rate: %" PRIu32, 67 | YESNO(this->use_mclk_), YESNO(this->use_mic_), this->resolution_out_, this->sample_frequency_); 68 | // ESP_LOGCONFIG(TAG, " I2S Role: %s", this->force_master_ ? "MASTER (driving BCLK/LRCK)" : "SLAVE"); 69 | // ESP_LOGCONFIG(TAG, " MCLK multiple (codec): %" PRIu32 " (MCLK ~= %" PRIu32 " Hz)", this->mclk_multiple_, this->sample_frequency_ * this->mclk_multiple_); 70 | 71 | 72 | // Report I2S master/slave role 73 | uint8_t reg00 = 0; 74 | if (this->read_byte(ES8311_REG00_RESET, ®00)) { 75 | bool master = (reg00 & 0x40) != 0; // MSC bit 76 | ESP_LOGCONFIG(TAG, " I2S Role: %s", master ? "MASTER (driving BCLK/LRCK)" : "SLAVE"); 77 | } 78 | 79 | // If we’re using external MCLK, show the multiple we expect (codec side) 80 | // MCLK = sample_rate * mclk_multiple_ 81 | { 82 | uint32_t mclk = this->sample_frequency_ * this->mclk_multiple_; 83 | ESP_LOGCONFIG(TAG, " MCLK multiple (codec): %" PRIu32 " (MCLK ~= %" PRIu32 " Hz)", 84 | (uint32_t) this->mclk_multiple_, mclk); 85 | } 86 | 87 | // Read out key registers for clocking and format 88 | uint8_t r01=0, r02=0, r03=0, r04=0, r05=0, r06=0, r07=0, r08=0, r09=0, r0A=0, r31=0, r32=0; 89 | bool ok = true; 90 | ok &= this->read_byte(ES8311_REG01_CLK_MANAGER, &r01); 91 | ok &= this->read_byte(ES8311_REG02_CLK_MANAGER, &r02); 92 | ok &= this->read_byte(ES8311_REG03_CLK_MANAGER, &r03); 93 | ok &= this->read_byte(ES8311_REG04_CLK_MANAGER, &r04); 94 | ok &= this->read_byte(ES8311_REG05_CLK_MANAGER, &r05); 95 | ok &= this->read_byte(ES8311_REG06_CLK_MANAGER, &r06); 96 | ok &= this->read_byte(ES8311_REG07_CLK_MANAGER, &r07); 97 | ok &= this->read_byte(ES8311_REG08_CLK_MANAGER, &r08); 98 | ok &= this->read_byte(ES8311_REG09_SDPIN, &r09); 99 | ok &= this->read_byte(ES8311_REG0A_SDPOUT, &r0A); 100 | ok &= this->read_byte(ES8311_REG31_DAC, &r31); 101 | ok &= this->read_byte(ES8311_REG32_DAC, &r32); 102 | 103 | if (ok) { 104 | ESP_LOGCONFIG(TAG, " REG00=0x%02X REG01=0x%02X REG02=0x%02X REG03=0x%02X", 105 | reg00, r01, r02, r03); 106 | ESP_LOGCONFIG(TAG, " REG04=0x%02X REG05=0x%02X REG06=0x%02X REG07=0x%02X REG08=0x%02X", 107 | r04, r05, r06, r07, r08); 108 | ESP_LOGCONFIG(TAG, " REG09(SDPIN)=0x%02X REG0A(SDPOUT)=0x%02X REG31(DAC mute)=0x%02X REG32(DAC vol)=0x%02X", 109 | r09, r0A, r31, r32); 110 | } else { 111 | ESP_LOGCONFIG(TAG, " (Some register reads failed; bus busy or codec not ready)"); 112 | } 113 | 114 | /*/Debug 115 | uint8_t r00,r06,r07,r08; 116 | read_byte(ES8311_REG00_RESET,&r00); 117 | read_byte(ES8311_REG06_CLK_MANAGER,&r06); 118 | read_byte(ES8311_REG07_CLK_MANAGER,&r07); 119 | read_byte(ES8311_REG08_CLK_MANAGER,&r08); 120 | ESP_LOGCONFIG(TAG, "REG00=0x%02X REG06=0x%02X REG07=0x%02X REG08=0x%02X", r00,r06,r07,r08); 121 | */ 122 | 123 | if (this->is_failed()) { 124 | ESP_LOGCONFIG(TAG, " Failed to initialize!"); 125 | return; 126 | } 127 | } 128 | 129 | bool ES8311::set_volume(float volume) { 130 | volume = clamp(volume, 0.0f, 1.0f); 131 | uint8_t reg32 = remap(volume, 0.0f, 1.0f, 0, 255); 132 | return this->write_byte(ES8311_REG32_DAC, reg32); 133 | } 134 | 135 | float ES8311::volume() { 136 | uint8_t reg32; 137 | this->read_byte(ES8311_REG32_DAC, ®32); 138 | return remap(reg32, 0, 255, 0.0f, 1.0f); 139 | } 140 | 141 | uint8_t ES8311::calculate_resolution_value(ES8311Resolution resolution) { 142 | switch (resolution) { 143 | case ES8311_RESOLUTION_16: 144 | return (3 << 2); 145 | case ES8311_RESOLUTION_18: 146 | return (2 << 2); 147 | case ES8311_RESOLUTION_20: 148 | return (1 << 2); 149 | case ES8311_RESOLUTION_24: 150 | return (0 << 2); 151 | case ES8311_RESOLUTION_32: 152 | return (4 << 2); 153 | default: 154 | return 0; 155 | } 156 | } 157 | 158 | const ES8311Coefficient *ES8311::get_coefficient(uint32_t mclk, uint32_t rate) { 159 | for (const auto &coefficient : ES8311_COEFFICIENTS) { 160 | if (coefficient.mclk == mclk && coefficient.rate == rate) 161 | return &coefficient; 162 | } 163 | return nullptr; 164 | } 165 | 166 | 167 | // Helper: numeric bits from enum 168 | static inline uint8_t es8311_bits_from_resolution(ES8311Resolution r) { 169 | switch (r) { 170 | case ES8311_RESOLUTION_16: return 16; 171 | case ES8311_RESOLUTION_18: return 18; 172 | case ES8311_RESOLUTION_20: return 20; 173 | case ES8311_RESOLUTION_24: return 24; 174 | case ES8311_RESOLUTION_32: return 32; 175 | } 176 | return 16; 177 | } 178 | 179 | bool ES8311::configure_clock_() { 180 | // Register 0x01: select clock source for internal MCLK and determine its frequency 181 | uint8_t reg01 = 0x3F; // Enable all clocks 182 | 183 | uint32_t mclk_frequency = this->sample_frequency_ * this->mclk_multiple_; 184 | if (!this->use_mclk_) { 185 | reg01 |= BIT(7); // Use SCLK 186 | mclk_frequency = this->sample_frequency_ * (int) this->resolution_out_ * 2; 187 | } 188 | if (this->mclk_inverted_) { 189 | reg01 |= BIT(6); // Invert MCLK pin 190 | } 191 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG01_CLK_MANAGER, reg01)); 192 | 193 | // Get clock coefficients from coefficient table 194 | auto *coefficient = get_coefficient(mclk_frequency, this->sample_frequency_); 195 | if (coefficient == nullptr) { 196 | ESP_LOGE(TAG, "Unable to configure sample rate %" PRIu32 "Hz with %" PRIu32 "Hz MCLK", this->sample_frequency_, 197 | mclk_frequency); 198 | return false; 199 | } 200 | 201 | // Register 0x02 202 | uint8_t reg02; 203 | ES8311_ERROR_CHECK(this->read_byte(ES8311_REG02_CLK_MANAGER, ®02)); 204 | reg02 &= 0x07; 205 | reg02 |= (coefficient->pre_div - 1) << 5; 206 | reg02 |= coefficient->pre_mult << 3; 207 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG02_CLK_MANAGER, reg02)); 208 | 209 | // Register 0x03 210 | const uint8_t reg03 = (coefficient->fs_mode << 6) | coefficient->adc_osr; 211 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG03_CLK_MANAGER, reg03)); 212 | 213 | // Register 0x04 214 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG04_CLK_MANAGER, coefficient->dac_osr)); 215 | 216 | // Register 0x05 217 | const uint8_t reg05 = ((coefficient->adc_div - 1) << 4) | (coefficient->dac_div - 1); 218 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG05_CLK_MANAGER, reg05)); 219 | 220 | /* 221 | // Register 0x06 222 | uint8_t reg06; 223 | ES8311_ERROR_CHECK(this->read_byte(ES8311_REG06_CLK_MANAGER, ®06)); 224 | if (this->sclk_inverted_) { 225 | reg06 |= BIT(5); 226 | } else { 227 | reg06 &= ~BIT(5); 228 | } 229 | reg06 &= 0xE0; 230 | if (coefficient->bclk_div < 19) { 231 | reg06 |= (coefficient->bclk_div - 1) << 0; 232 | } else { 233 | reg06 |= (coefficient->bclk_div) << 0; 234 | } 235 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG06_CLK_MANAGER, reg06)); 236 | */ 237 | // Register 0x06: **override** BCLK divider analytically for master mode. 238 | // Target: BCLK = fs * bits_per_slot * channels(=2). ES8311 derives both BCLK and LRCK from MCLK. 239 | // We compute div = round(MCLK / target_bclk) and program it explicitly. 240 | { 241 | uint8_t reg06_cur; 242 | ES8311_ERROR_CHECK(this->read_byte(ES8311_REG06_CLK_MANAGER, ®06_cur)); 243 | 244 | // Preserve INV_SCLK (bit 5), clear div bits 245 | uint8_t reg06_new = reg06_cur & 0xE0; 246 | if (this->sclk_inverted_) { 247 | reg06_new |= (1u << 5); 248 | } else { 249 | reg06_new &= ~(1u << 5); 250 | } 251 | 252 | // Calculate divider from actual MCLK used above 253 | uint32_t mclk_frequency = this->sample_frequency_ * this->mclk_multiple_; 254 | if (!this->use_mclk_) { 255 | // not expected for master, but keep behavior consistent 256 | mclk_frequency = this->sample_frequency_ * (uint32_t) es8311_bits_from_resolution(this->resolution_out_) * 2u; 257 | } 258 | 259 | const uint8_t bits = es8311_bits_from_resolution(this->resolution_out_); 260 | const uint32_t channels = 2; // ES8311 DAC outputs stereo frames on I2S 261 | const uint32_t target_bclk = this->sample_frequency_ * bits * channels; // e.g. 16k * 16 * 2 = 512k 262 | 263 | // Round to nearest integer divider, clamp to valid range. 264 | uint32_t div = (target_bclk == 0) ? 1 : ( (mclk_frequency + (target_bclk/2)) / target_bclk ); 265 | if (div < 1) div = 1; 266 | if (div > 31) div = 31; // 5 bits available via ES8311 encoding (<=18 uses div-1 encoding) 267 | 268 | // ES8311 REG06 encoding quirk kept from original driver: 269 | // if div < 19 -> program (div-1); else program (div) 270 | uint8_t reg06_div_field = (div < 19) ? (uint8_t)(div - 1) : (uint8_t)div; 271 | reg06_new |= (reg06_div_field & 0x1F); 272 | 273 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG06_CLK_MANAGER, reg06_new)); 274 | 275 | // Verbose diag to confirm the math on real hardware 276 | ESP_LOGI(TAG, 277 | "MASTER clocking: fs=%" PRIu32 " Hz, bits=%u, ch=2 -> BCLK=%" PRIu32 278 | " Hz, MCLK=%" PRIu32 " Hz, BCLK_DIV=%" PRIu32 " (REG06=0x%02X)", 279 | this->sample_frequency_, bits, target_bclk, mclk_frequency, div, reg06_new); 280 | } 281 | // Register 0x07 282 | uint8_t reg07; 283 | ES8311_ERROR_CHECK(this->read_byte(ES8311_REG07_CLK_MANAGER, ®07)); 284 | reg07 &= 0xC0; 285 | reg07 |= coefficient->lrck_h << 0; 286 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG07_CLK_MANAGER, reg07)); 287 | 288 | // Register 0x08 289 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG08_CLK_MANAGER, coefficient->lrck_l)); 290 | 291 | // Successfully configured the clock 292 | return true; 293 | } 294 | 295 | bool ES8311::configure_format_() { 296 | // Configure I2S mode and format 297 | uint8_t reg00; 298 | ES8311_ERROR_CHECK(this->read_byte(ES8311_REG00_RESET, ®00)); 299 | if (this->force_master_) { 300 | reg00 |= BIT(6); // MSC=1 → MASTER (drive BCLK/LRCK) 301 | } else { 302 | reg00 &= ~BIT(6); // MSC=0 → SLAVE (current default) 303 | } 304 | 305 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG00_RESET, reg00)); 306 | 307 | // Configure SDP in resolution 308 | uint8_t reg09 = calculate_resolution_value(this->resolution_in_); 309 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG09_SDPIN, reg09)); 310 | 311 | // Configure SDP out resolution 312 | uint8_t reg0a = calculate_resolution_value(this->resolution_out_); 313 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG0A_SDPOUT, reg0a)); 314 | 315 | // Successfully configured the format 316 | return true; 317 | } 318 | 319 | bool ES8311::configure_mic_() { 320 | uint8_t reg14 = 0x1A; // Enable analog MIC and max PGA gain 321 | if (this->use_mic_) { 322 | reg14 |= BIT(6); // Enable PDM digital microphone 323 | } 324 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG14_SYSTEM, reg14)); 325 | 326 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG16_ADC, this->mic_gain_)); // ADC gain scale up 327 | ES8311_ERROR_CHECK(this->write_byte(ES8311_REG17_ADC, 0xC8)); // Set ADC gain 328 | 329 | // Successfully configured the microphones 330 | return true; 331 | } 332 | 333 | bool ES8311::set_mute_state_(bool mute_state) { 334 | uint8_t reg31; 335 | 336 | this->is_muted_ = mute_state; 337 | 338 | if (!this->read_byte(ES8311_REG31_DAC, ®31)) { 339 | return false; 340 | } 341 | 342 | if (mute_state) { 343 | reg31 |= BIT(6) | BIT(5); 344 | } else { 345 | reg31 &= ~(BIT(6) | BIT(5)); 346 | } 347 | 348 | return this->write_byte(ES8311_REG31_DAC, reg31); 349 | } 350 | 351 | } // namespace es8311 352 | } // namespace esphome -------------------------------------------------------------------------------- /waveshare-s3-audio.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | device_name: esp32-audio-s3 3 | 4 | # Phases of the Voice Assistant 5 | voice_assist_idle_phase_id: '1' # ready to be triggered by a wake word 6 | voice_assist_waiting_for_command_phase_id: '2' # waiting for a voice command (after being triggered by the wake word) 7 | voice_assist_listening_for_command_phase_id: '3' # listening for a voice command 8 | voice_assist_thinking_phase_id: '4' # processing the command 9 | voice_assist_replying_phase_id: '5' # replying to the command 10 | voice_assist_not_ready_phase_id: '10' # not ready 11 | voice_assist_error_phase_id: '11' #encountered an error 12 | 13 | i2s_mclk: GPIO12 14 | i2s_bclk: GPIO13 #SCLK 15 | i2s_lrclk: GPIO14 #LRCK/WS 16 | 17 | amp_ctrl: 8 #9 #Extended IO pin that controls the PA_EN on the TCA9555 18 | i2c_scl: GPIO10 19 | i2c_sda: GPIO11 20 | rtc_int: 5 #ExtendedIO 21 | 22 | #mic lookup 23 | mic_channel_0: 'left' 24 | mic_channel_1: 'right' # mic wont allow right 25 | mic_channel_2: 'stereo' # voice assistant does not support stereo 26 | 27 | #WORKING 28 | which_mic: 0 #0=left, 1=right, 2=stereo 29 | i2s_sample_rate: 16000 # Works as in sync with microphone (mic must have 16000 for voice assistant) 30 | i2s_bits_per_sample: 32 31 | #i2s_bits_per_sample: 16 32 | i2s_mode_mic: secondary 33 | i2s_mode_speaker: secondary 34 | i2s_use_apll: true 35 | i2s_bps_spk: 16bit 36 | i2s_bps_mic: 16bit 37 | i2s_mclk_multiple: 256 # 128, 256, 384, 512 38 | 39 | hidden_ssid: "false" 40 | led_num: '7' # WS2812 ring leds 41 | 42 | # Pre-configured camera code 43 | # exposes UI controls to Homeassistant 44 | # ==================================== 45 | #packages: # REMOTE (GIT) 46 | # remote_package_shorthand: github://esphome/sw3Dan/waveshare-s2-audio_esphome_voice/waveshare_camera_pkg.yaml@main 47 | #packages: # LOCAL FILE 48 | # dynamic-cam: !include 49 | # file: waveshare_camera_pkg.yaml 50 | 51 | esphome: 52 | name: $device_name 53 | friendly_name: Waveshare-Audio-3 54 | min_version: 2025.8.0 55 | on_boot: 56 | - priority: -100 57 | then: 58 | - if: 59 | condition: 60 | switch.is_on: diag_disable_mic 61 | then: 62 | - logger.log: "Mic ENABLED" 63 | else: 64 | - logger.log: "Mic DISABLED for diagnostics" 65 | - micro_wake_word.stop: 66 | - voice_assistant.stop: 67 | - microphone.stop_capture: i2s_mics 68 | - priority: 375 69 | then: 70 | - sensor.template.publish: 71 | id: next_timer 72 | state: -1 73 | - script.execute: control_leds 74 | - delay: 10min 75 | - if: 76 | condition: 77 | lambda: return id(init_in_progress); 78 | then: 79 | - lambda: id(init_in_progress) = false; 80 | - script.execute: control_leds 81 | - priority: -100 82 | then: 83 | - lambda: |- 84 | id(alarm_time).publish_state(id(saved_alarm_time)); 85 | - lambda: |- 86 | auto call = id(alarm_action).make_call(); 87 | call.set_option(id(saved_alarm_action)); 88 | call.perform(); 89 | - lambda: |- 90 | setenv("TZ", id(saved_time_zone).c_str(), 1); 91 | tzset(); 92 | 93 | esp32: 94 | board: esp32-s3-devkitc-1 95 | cpu_frequency: 240MHz 96 | variant: esp32s3 97 | flash_size: 16MB 98 | framework: 99 | type: esp-idf 100 | version: recommended 101 | sdkconfig_options: 102 | CONFIG_ESP32S3_DATA_CACHE_64KB: "y" 103 | CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" 104 | CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB: "y" 105 | 106 | # Considerably speeds up mWW at the cost of using more PSRAM. 107 | CONFIG_SPIRAM_RODATA: "y" 108 | CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y" 109 | 110 | CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: "y" 111 | CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY: "y" 112 | 113 | CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC: "y" 114 | CONFIG_MBEDTLS_SSL_PROTO_TLS1_3: "y" # TLS1.3 support isn't enabled by default in IDF 5.1.5 115 | 116 | wifi: 117 | id: wifi_id 118 | fast_connect: ${hidden_ssid} 119 | ssid: !secret wifi_ssid 120 | password: !secret wifi_password 121 | power_save_mode: none # Disable power saving for better performance 122 | reboot_timeout: 10min # Prevent unnecessary reboots 123 | on_connect: 124 | - lambda: id(improv_ble_in_progress) = false; 125 | - script.execute: control_leds 126 | on_disconnect: 127 | - script.execute: control_leds 128 | 129 | #network: 130 | # enable_ipv6: true 131 | 132 | debug: 133 | update_interval: 5s 134 | 135 | logger: 136 | level: DEBUG 137 | 138 | api: 139 | id: api_id 140 | actions: 141 | - action: set_led_color 142 | variables: 143 | red: float 144 | green: float 145 | blue: float 146 | then: 147 | - lambda: |- 148 | id(led_ring_color_r) = std::min(1.0f, std::max(0.0f, red)); 149 | id(led_ring_color_g) = std::min(1.0f, std::max(0.0f, green)); 150 | id(led_ring_color_b) = std::min(1.0f, std::max(0.0f, blue)); 151 | - action: start_va 152 | then: 153 | - voice_assistant.start 154 | - action: stop_va 155 | then: 156 | - voice_assistant.stop 157 | - action: set_alarm_time 158 | variables: 159 | alarm_time_hh_mm: string 160 | then: 161 | - lambda: |- 162 | if (alarm_time_hh_mm.length() == 5 && 163 | isdigit(alarm_time_hh_mm[0]) && isdigit(alarm_time_hh_mm[1]) && 164 | isdigit(alarm_time_hh_mm[3]) && isdigit(alarm_time_hh_mm[4])) { 165 | id(alarm_time).publish_state(alarm_time_hh_mm); 166 | id(saved_alarm_time) = alarm_time_hh_mm; 167 | } 168 | - action: set_time_zone 169 | variables: 170 | posix_time_zone: string 171 | then: 172 | - lambda: |- 173 | setenv("TZ", posix_time_zone.c_str(), 1); 174 | tzset(); 175 | id(saved_time_zone) = posix_time_zone; 176 | id(publish_current_time).execute(); 177 | on_client_connected: 178 | - script.execute: control_leds 179 | on_client_disconnected: 180 | - script.execute: control_leds 181 | 182 | encryption: 183 | key: !secret api_esp32-audio-s3 184 | 185 | # http_request: 186 | # verify_ssl: false 187 | 188 | ota: 189 | - platform: esphome 190 | id: ota_esphome 191 | password: !secret ota_password 192 | 193 | i2c: 194 | - id: internal_i2c 195 | sda: GPIO11 196 | scl: GPIO10 197 | scan: true 198 | frequency: 100kHz 199 | 200 | # I/O Expander that controls the amplifier enable pin 201 | tca9555: 202 | id: ioexp 203 | i2c_id: internal_i2c 204 | address: 0x20 205 | 206 | psram: 207 | mode: octal 208 | speed: 80MHz 209 | 210 | time: 211 | - platform: homeassistant 212 | #- platform: pcf85063 213 | id: rtc 214 | # address: 0x51 215 | #RTC_INT = $rtc_int # extIO 5 216 | # i2c_id: internal_i2c 217 | 218 | on_time: 219 | - seconds: 0 220 | minutes: /1 221 | then: 222 | - script.execute: check_alarm 223 | on_time_sync: 224 | - script.execute: publish_current_time 225 | 226 | light: 227 | - platform: esp32_rmt_led_strip 228 | id: status_ring 229 | name: "Status Ring" 230 | pin: GPIO38 231 | num_leds: ${led_num} 232 | chipset: WS2812 233 | rgb_order: RGB 234 | default_transition_length: 150ms 235 | effects: 236 | - pulse: 237 | name: "Pulse Slow" 238 | transition_length: 1000ms 239 | update_interval: 16ms 240 | - pulse: 241 | name: "Pulse Medium" 242 | transition_length: 600ms 243 | update_interval: 16ms 244 | - pulse: 245 | name: "Pulse Fast" 246 | transition_length: 300ms 247 | update_interval: 16ms 248 | - addressable_rainbow: 249 | name: "Rainbow Slow" 250 | speed: 6 251 | width: 7 252 | - addressable_color_wipe: 253 | name: "Wipe" 254 | 255 | i2s_audio: 256 | - id: i2s_output 257 | i2s_lrclk_pin: 258 | number: $i2s_lrclk 259 | allow_other_uses: true 260 | i2s_bclk_pin: 261 | number: $i2s_bclk 262 | allow_other_uses: true 263 | i2s_mclk_pin: 264 | number: $i2s_mclk 265 | 266 | - id: i2s_input 267 | i2s_lrclk_pin: 268 | number: $i2s_lrclk 269 | allow_other_uses: true 270 | i2s_bclk_pin: 271 | number: $i2s_bclk 272 | allow_other_uses: true 273 | 274 | audio_adc: 275 | - platform: es7210 276 | id: adc_mic 277 | i2c_id: internal_i2c 278 | sample_rate: $i2s_sample_rate 279 | bits_per_sample: $i2s_bps_mic 280 | #bits_per_sample: "${i2s_bits_per_sample}bit" 281 | 282 | # external_components: 283 | # - source: 284 | # type: local 285 | # path: components 286 | # components: [ es8311 ] 287 | external_components: 288 | - source: 289 | type: git 290 | url: "https://github.com/sw3Dan/waveshare-s2-audio_esphome_voice" 291 | ref: main 292 | components: [ es8311 ] 293 | refresh: 0s 294 | 295 | audio_dac: 296 | - platform: es8311 297 | id: es8311_dac 298 | i2c_id: internal_i2c 299 | use_mclk: true 300 | sample_rate: $i2s_sample_rate 301 | bits_per_sample: $i2s_bps_spk 302 | #bits_per_sample: "${i2s_bits_per_sample}bit" 303 | force_master: true 304 | mclk_multiple: $i2s_mclk_multiple 305 | 306 | microphone: 307 | - platform: i2s_audio 308 | id: i2s_mics 309 | i2s_din_pin: GPIO15 310 | adc_type: external 311 | pdm: false 312 | i2s_audio_id: i2s_input 313 | i2s_mode: $i2s_mode_mic 314 | 315 | #sample_rate: $i2s_sample_rate 316 | sample_rate: 16000 317 | 318 | bits_per_sample: $i2s_bps_mic 319 | #bits_per_sample: "${i2s_bits_per_sample}bit" 320 | 321 | use_apll: $i2s_use_apll 322 | mclk_multiple: $i2s_mclk_multiple 323 | #channel: stereo 324 | channel: ${mic_channel_${which_mic}} 325 | 326 | speaker: 327 | - platform: i2s_audio 328 | id: i2s_audio_speaker 329 | i2s_dout_pin: GPIO16 330 | 331 | i2s_audio_id: i2s_output 332 | i2s_mode: secondary #using the patched es8311 333 | 334 | dac_type: external 335 | timeout: never 336 | buffer_duration: 100ms 337 | audio_dac: es8311_dac 338 | sample_rate: $i2s_sample_rate 339 | #bits_per_sample: "${i2s_bits_per_sample}bit" 340 | #bits_per_sample: $i2s_bps_spk 341 | bits_per_sample: 16bit #TEMP FIX 342 | use_apll: $i2s_use_apll 343 | mclk_multiple: $i2s_mclk_multiple 344 | #channel: mono #WORKS 345 | channel: stereo #WORKS #TEST: STREAM ERROR 346 | #channel: ${mic_channel_${which_mic}} 347 | 348 | # Virtual speakers to combine the announcement and media streams together into one output 349 | - platform: mixer 350 | id: mixing_speaker 351 | output_speaker: i2s_audio_speaker 352 | num_channels: 2 353 | source_speakers: 354 | - id: announcement_mixing_input 355 | timeout: never 356 | - id: media_mixing_input 357 | timeout: never 358 | 359 | # Vritual speakers to resample each pipelines' audio, if necessary, as the mixer speaker requires the same sample rate 360 | - platform: resampler 361 | id: announcement_resampling_speaker 362 | output_speaker: announcement_mixing_input 363 | - platform: resampler 364 | id: media_resampling_speaker 365 | output_speaker: media_mixing_input 366 | 367 | media_player: 368 | - platform: speaker 369 | id: external_media_player 370 | name: None 371 | internal: False 372 | volume_increment: 0.01 373 | volume_min: 0.4 374 | volume_max: 0.8 375 | announcement_pipeline: 376 | speaker: announcement_resampling_speaker 377 | format: FLAC # FLAC is the least processor intensive codec 378 | num_channels: 1 # Stereo audio is unnecessary for announcements 379 | sample_rate: 48000 #Supported by Music Assistant 380 | media_pipeline: 381 | speaker: media_resampling_speaker 382 | format: FLAC # FLAC is the least processor intensive codec 383 | sample_rate: 48000 384 | on_announcement: 385 | - mixer_speaker.apply_ducking: 386 | id: media_mixing_input 387 | decibel_reduction: 20 388 | duration: 0.0s 389 | on_state: 390 | if: 391 | condition: 392 | and: 393 | - switch.is_off: timer_ringing 394 | - not: 395 | voice_assistant.is_running: 396 | - not: 397 | media_player.is_announcing: 398 | then: 399 | - mixer_speaker.apply_ducking: 400 | id: media_mixing_input 401 | decibel_reduction: 0 402 | duration: 1.0s 403 | on_mute: 404 | - delay: 100ms # Debounce 405 | - script.execute: control_leds_volume_changed 406 | on_unmute: 407 | - delay: 100ms # Debounce 408 | - script.execute: control_leds_volume_changed 409 | on_volume: 410 | if: 411 | condition: 412 | - lambda: return !id(init_in_progress); 413 | then: 414 | - delay: 100ms # Debounce 415 | - script.execute: control_leds_volume_changed 416 | files: 417 | - id: mute_switch_on_sound 418 | file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/mute_switch_on.flac 419 | - id: mute_switch_off_sound 420 | file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/mute_switch_off.flac 421 | - id: timer_finished_sound 422 | file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac 423 | - id: wake_word_triggered_sound 424 | file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/wake_word_triggered.flac 425 | - id: error_cloud_expired 426 | file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/error_cloud_expired.mp3 427 | # - id: center_button_press_sound 428 | # file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_press.flac 429 | # - id: center_button_double_press_sound 430 | # file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_double_press.flac 431 | # - id: center_button_triple_press_sound 432 | # file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_triple_press.flac 433 | # - id: center_button_long_press_sound 434 | # file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_long_press.flac 435 | # - id: factory_reset_initiated_sound 436 | # file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_initiated.mp3 437 | # - id: factory_reset_cancelled_sound 438 | # file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_cancelled.mp3 439 | # - id: factory_reset_confirmed_sound 440 | # file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_confirmed.mp3 441 | 442 | voice_assistant: 443 | id: va 444 | microphone: 445 | microphone: i2s_mics 446 | #channels: 1 #Right 447 | #channels: 0 #Left 448 | channels: $which_mic #correct that it's left/right cannel OR number of channels? 449 | media_player: external_media_player 450 | micro_wake_word: mww 451 | use_wake_word: false 452 | noise_suppression_level: 0 #2 #0 453 | auto_gain: 0 dbfs 454 | volume_multiplier: 1 455 | on_client_connected: 456 | - if: 457 | condition: 458 | - lambda: return id(init_in_progress); 459 | - switch.is_on: mic_mute_switch 460 | then: 461 | - switch.turn_off: mic_mute_switch 462 | - lambda: id(init_in_progress) = false; 463 | - micro_wake_word.start: 464 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 465 | - script.execute: control_leds 466 | on_client_disconnected: 467 | - voice_assistant.stop: 468 | - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; 469 | - script.execute: control_leds 470 | on_error: 471 | # Only set the error phase if the error code is different than duplicate_wake_up_detected or stt-no-text-recognized 472 | # These two are ignored for a better user experience 473 | - if: 474 | condition: 475 | and: 476 | - lambda: return !id(init_in_progress); 477 | - lambda: return code != "duplicate_wake_up_detected"; 478 | - lambda: return code != "stt-no-text-recognized"; 479 | then: 480 | - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; 481 | - script.execute: control_leds 482 | # If the error code is cloud-auth-failed, serve a local audio file guiding the user. 483 | - if: 484 | condition: 485 | - lambda: return code == "cloud-auth-failed"; 486 | then: 487 | - script.execute: 488 | id: play_sound 489 | priority: true 490 | sound_file: !lambda return id(error_cloud_expired); 491 | # When the voice assistant starts: Play a wake up sound, duck audio. 492 | on_start: 493 | - mixer_speaker.apply_ducking: 494 | id: media_mixing_input 495 | decibel_reduction: 30 #20 # Number of dB quieter; higher implies more quiet, 0 implies full volume 496 | duration: 0.0s # The duration of the transition (default is no transition) 497 | on_listening: 498 | - lambda: id(voice_assistant_phase) = ${voice_assist_waiting_for_command_phase_id}; 499 | - script.execute: control_leds 500 | on_stt_vad_start: 501 | - lambda: id(voice_assistant_phase) = ${voice_assist_listening_for_command_phase_id}; 502 | - script.execute: control_leds 503 | on_stt_vad_end: 504 | - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; 505 | - script.execute: control_leds 506 | on_intent_progress: 507 | - if: 508 | condition: 509 | # A nonempty x variable means a streaming TTS url was sent to the media player 510 | lambda: 'return !x.empty();' 511 | then: 512 | - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; 513 | - script.execute: control_leds 514 | # Start a script that would potentially enable the stop word if the response is longer than a second 515 | - script.execute: activate_stop_word_once 516 | on_tts_start: 517 | - if: 518 | condition: 519 | # The intent_progress trigger didn't start the TTS Reponse 520 | lambda: 'return id(voice_assistant_phase) != ${voice_assist_replying_phase_id};' 521 | then: 522 | - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; 523 | - script.execute: control_leds 524 | # Start a script that would potentially enable the stop word if the response is longer than a second 525 | - script.execute: activate_stop_word_once 526 | on_tts_end: 527 | - script.execute: 528 | id: send_tts_uri_event 529 | tts_uri: !lambda 'return x;' 530 | on_stt_end: 531 | - script.execute: 532 | id: send_stt_text_event 533 | stt_text: !lambda 'return x;' 534 | 535 | # When the voice assistant ends ... 536 | on_end: 537 | - wait_until: 538 | not: 539 | voice_assistant.is_running: 540 | # Stop ducking audio. 541 | - mixer_speaker.apply_ducking: 542 | id: media_mixing_input 543 | decibel_reduction: 0 544 | duration: 1.0s 545 | # If the end happened because of an error, let the error phase on for a second 546 | - if: 547 | condition: 548 | lambda: return id(voice_assistant_phase) == ${voice_assist_error_phase_id}; 549 | then: 550 | - delay: 1s 551 | # Reset the voice assistant phase id and reset the LED animations. 552 | - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; 553 | - script.execute: control_leds 554 | on_timer_finished: 555 | - switch.turn_on: timer_ringing 556 | - lambda: | 557 | id(next_timer).publish_state(-1); 558 | id(next_timer_name).publish_state("-"); 559 | on_timer_started: 560 | - lambda: | 561 | id(check_if_timers_active).execute(); 562 | if (id(is_timer_active)) { 563 | id(fetch_first_active_timer).execute(); 564 | id(next_timer).publish_state(id(first_active_timer).seconds_left); 565 | id(next_timer_name).publish_state(id(first_active_timer).name); 566 | } 567 | - script.execute: control_leds 568 | on_timer_cancelled: 569 | - lambda: | 570 | id(check_if_timers_active).execute(); 571 | if (id(is_timer_active)) { 572 | id(fetch_first_active_timer).execute(); 573 | id(next_timer).publish_state(id(first_active_timer).seconds_left); 574 | id(next_timer_name).publish_state(id(first_active_timer).name); 575 | } else { 576 | id(next_timer).publish_state(-1); 577 | id(next_timer_name).publish_state("-"); 578 | } 579 | - script.execute: control_leds 580 | on_timer_updated: 581 | - lambda: | 582 | id(check_if_timers_active).execute(); 583 | if (id(is_timer_active)) { 584 | id(fetch_first_active_timer).execute(); 585 | id(next_timer).publish_state(id(first_active_timer).seconds_left); 586 | id(next_timer_name).publish_state(id(first_active_timer).name); 587 | } 588 | - script.execute: control_leds 589 | on_timer_tick: 590 | - lambda: | 591 | id(fetch_first_active_timer).execute(); 592 | int seconds_left = id(first_active_timer).seconds_left; 593 | if (seconds_left % 5 == 0) { 594 | id(next_timer).publish_state(seconds_left); 595 | } 596 | // LED updates are handled centrally by control_leds() 597 | 598 | micro_wake_word: 599 | id: mww 600 | microphone: 601 | microphone: i2s_mics 602 | channels: $which_mic 603 | #gain_factor: 4 604 | stop_after_detection: false 605 | models: 606 | - model: https://github.com/kahrendt/microWakeWord/releases/download/okay_nabu_20241226.3/okay_nabu.json 607 | #probability_cutoff: 0.03 # 0.8 608 | id: okay_nabu 609 | - model: https://raw.githubusercontent.com/formatBCE/Respeaker-Lite-ESPHome-integration/refs/heads/main/microwakeword/models/v2/kenobi.json 610 | id: kenobi 611 | - model: hey_jarvis 612 | id: hey_jarvis 613 | #- model: hey_mycroft 614 | # id: hey_mycroft 615 | - model: https://github.com/kahrendt/microWakeWord/releases/download/stop/stop.json 616 | id: stop 617 | internal: true #false 618 | vad: 619 | probability_cutoff: 0.03 #0.03 620 | on_wake_word_detected: 621 | # If the wake word is detected when the device is muted (Possible with the software mute switch): Do nothing 622 | - if: 623 | condition: 624 | switch.is_off: mic_mute_switch 625 | then: 626 | # If a timer is ringing: Stop it, do not start the voice assistant (We can stop timer from voice!) 627 | - if: 628 | condition: 629 | switch.is_on: timer_ringing 630 | then: 631 | - switch.turn_off: timer_ringing 632 | # Stop voice assistant if running 633 | else: 634 | - if: 635 | condition: 636 | voice_assistant.is_running: 637 | then: 638 | voice_assistant.stop: 639 | # Stop any other media player announcement 640 | else: 641 | - if: 642 | condition: 643 | media_player.is_announcing: 644 | then: 645 | - media_player.stop: 646 | announcement: true 647 | # Start the voice assistant and play the wake sound, if enabled 648 | else: 649 | - if: 650 | condition: 651 | switch.is_on: wake_sound 652 | then: 653 | - script.execute: 654 | id: play_sound 655 | priority: true 656 | sound_file: !lambda return id(wake_word_triggered_sound); 657 | - delay: 300ms 658 | - voice_assistant.start: 659 | wake_word: !lambda return wake_word; 660 | 661 | globals: 662 | - id: mic_gain_saved 663 | type: float 664 | restore_value: yes 665 | initial_value: '32.0' #'24.0' DEBUG MIC 666 | - id: mic_is_muted 667 | type: bool 668 | restore_value: yes 669 | initial_value: 'false' 670 | 671 | - id: led_ring_color_r 672 | type: float 673 | restore_value: yes 674 | initial_value: '1.0' 675 | - id: led_ring_color_g 676 | type: float 677 | restore_value: yes 678 | initial_value: '0.0' 679 | - id: led_ring_color_b 680 | type: float 681 | restore_value: yes 682 | initial_value: '1.0' 683 | - id: init_in_progress 684 | type: bool 685 | restore_value: no 686 | initial_value: 'true' 687 | - id: improv_ble_in_progress 688 | type: bool 689 | restore_value: no 690 | initial_value: 'false' 691 | - id: voice_assistant_phase 692 | type: int 693 | restore_value: no 694 | initial_value: ${voice_assist_not_ready_phase_id} 695 | - id: saved_alarm_time 696 | type: std::string 697 | restore_value: yes 698 | initial_value: '"Unknown"' 699 | - id: saved_time_zone 700 | type: std::string 701 | restore_value: yes 702 | initial_value: '"UTC0"' 703 | - id: saved_alarm_action 704 | type: std::string 705 | restore_value: yes 706 | initial_value: '"Play sound"' 707 | - id: first_active_timer 708 | type: voice_assistant::Timer 709 | restore_value: no 710 | - id: is_timer_active 711 | type: bool 712 | restore_value: no 713 | - id: factory_reset_requested 714 | type: bool 715 | restore_value: no 716 | initial_value: 'false' 717 | 718 | 719 | script: 720 | - id: flash_ring 721 | then: 722 | - light.turn_on: { id: status_ring, red: 1.0, green: 0.0, blue: 0.0, brightness: 100% } 723 | - delay: 250ms 724 | - light.turn_off: status_ring 725 | 726 | # Minimal dispatcher for LEDs: off / solid / built-in effects 727 | - id: led_set_effect 728 | mode: restart 729 | parameters: 730 | effect: std::string 731 | r: float 732 | g: float 733 | b: float 734 | then: 735 | - lambda: |- 736 | id(led_ring_color_r) = r; 737 | id(led_ring_color_g) = g; 738 | id(led_ring_color_b) = b; 739 | ESP_LOGI("led", "led_set_effect: effect='%s' rgb(%.2f, %.2f, %.2f) brightness=%.2f", 740 | effect.c_str(), r, g, b, (float) id(led_ring_brightness).state); 741 | - if: 742 | condition: 743 | lambda: 'return effect == "off";' 744 | then: 745 | - light.turn_off: status_ring 746 | else: 747 | - if: 748 | condition: 749 | lambda: 'return effect == "solid";' 750 | then: 751 | - light.turn_on: 752 | id: status_ring 753 | brightness: !lambda 'return id(led_ring_brightness).state;' 754 | red: !lambda 'return id(led_ring_color_r);' 755 | green: !lambda 'return id(led_ring_color_g);' 756 | blue: !lambda 'return id(led_ring_color_b);' 757 | else: 758 | - light.turn_on: 759 | id: status_ring 760 | brightness: !lambda 'return id(led_ring_brightness).state;' 761 | red: !lambda 'return id(led_ring_color_r);' 762 | green: !lambda 'return id(led_ring_color_g);' 763 | blue: !lambda 'return id(led_ring_color_b);' 764 | effect: !lambda 'return effect;' 765 | 766 | # Single LED state machine (small footprint) 767 | - id: control_leds 768 | mode: single 769 | then: 770 | - lambda: |- 771 | const bool wifi_connected = id(wifi_id).is_connected(); 772 | const bool api_connected = id(api_id).is_connected(); 773 | const bool ringing = id(timer_ringing).state; 774 | const bool improv_ble = id(improv_ble_in_progress); 775 | const bool init_progress = id(init_in_progress); 776 | const int phase = id(voice_assistant_phase); 777 | 778 | // Refresh timer cache sparsely when needed 779 | id(check_if_timers_active).execute(); 780 | const bool timers_active = id(is_timer_active); 781 | 782 | ESP_LOGD("led", 783 | "control_leds: wifi=%d api=%d improv=%d init=%d ringing=%d timers_active=%d phase=%d", 784 | (int)wifi_connected, (int)api_connected, (int)improv_ble, (int)init_progress, 785 | (int)ringing, (int)timers_active, phase); 786 | 787 | auto set = [&](const char* effect, float r, float g, float b) { 788 | id(led_set_effect).execute(effect, r, g, b); 789 | }; 790 | 791 | if (improv_ble) { // Provisioning 792 | set("Pulse Medium", 1.0, 0.89, 0.71); // warm white 793 | return; 794 | } 795 | if (init_progress) { // Boot/init 796 | //if (wifi_connected) set("Pulse Fast", 0.09, 0.73, 0.95); else set("Pulse Slow", 0.09, 0.73, 0.95); 797 | if (wifi_connected) set("Rainbow Slow", 0.9, 0.1, 0.85); else set("Rainbow Slow", 1.0, 0.1, 0.1); 798 | id(play_sound).execute(1, id(wake_word_triggered_sound)); //TEST 799 | return; 800 | } 801 | if (!wifi_connected || !api_connected) { // HA disconnected 802 | set("Pulse Medium", 1.0, 0.0, 0.0); 803 | return; 804 | } 805 | if (ringing) { // Timer ringing 806 | set("Pulse Fast", 0.6, 0.0, 0.6); 807 | return; 808 | } 809 | 810 | // Voice assistant phases 811 | switch (phase) { 812 | case ${voice_assist_waiting_for_command_phase_id}: // just woke 813 | set("solid", 0.5, 0.0, 0.5); // calm solid violet 814 | return; 815 | case ${voice_assist_listening_for_command_phase_id}: 816 | set("Pulse Medium", 0.8, 0.0, 0.8); 817 | return; 818 | case ${voice_assist_thinking_phase_id}: 819 | set("Pulse Slow", 0.6, 0.0, 0.6); 820 | return; 821 | case ${voice_assist_replying_phase_id}: 822 | set("Wipe", 0.6, 0.0, 0.6); 823 | return; 824 | case ${voice_assist_error_phase_id}: 825 | set("Pulse Fast", 1.0, 0.0, 0.0); 826 | return; 827 | case ${voice_assist_not_ready_phase_id}: 828 | set("Pulse Slow", 1.0, 0.0, 0.0); 829 | return; 830 | default: break; 831 | } 832 | 833 | // Timers ticking (no bar, minimal footprint) 834 | if (timers_active) { 835 | set("Pulse Slow", 0.6, 0.0, 0.6); 836 | return; 837 | } 838 | 839 | // Idle 840 | set("off", 0, 0, 0); 841 | 842 | # Brief green pulse when volume changes, then restore 843 | - id: control_leds_volume_changed 844 | mode: restart 845 | then: 846 | - light.turn_on: 847 | id: status_ring 848 | brightness: !lambda 'return id(led_ring_brightness).state;' 849 | red: 0.0 850 | green: 1.0 851 | blue: 0.0 852 | effect: "Pulse Fast" 853 | - lambda: |- 854 | ESP_LOGD("led", "volume_changed: flash green"); //DEBUG 855 | - delay: 1.2s 856 | - script.execute: control_leds 857 | 858 | # Timer sound handling 859 | - id: ring_timer 860 | then: 861 | - script.execute: enable_repeat_one 862 | - script.execute: 863 | id: play_sound 864 | priority: true 865 | sound_file: !lambda return id(timer_finished_sound); 866 | 867 | - id: enable_repeat_one 868 | then: 869 | - lambda: |- 870 | id(external_media_player) 871 | ->make_call() 872 | .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE) 873 | .set_announcement(true) 874 | .perform(); 875 | id(external_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 500); 876 | 877 | - id: disable_repeat 878 | then: 879 | - lambda: |- 880 | id(external_media_player) 881 | ->make_call() 882 | .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF) 883 | .set_announcement(true) 884 | .perform(); 885 | id(external_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0); 886 | 887 | - id: play_sound 888 | parameters: 889 | priority: bool 890 | sound_file: "audio::AudioFile*" 891 | then: 892 | - lambda: |- 893 | if (priority) { 894 | id(external_media_player) 895 | ->make_call() 896 | .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP) 897 | .set_announcement(true) 898 | .perform(); 899 | } 900 | if ((id(external_media_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING) || priority) { 901 | id(external_media_player)->play_file(sound_file, true, false); 902 | } 903 | 904 | # Timer helpers 905 | - id: fetch_first_active_timer 906 | mode: single 907 | then: 908 | - lambda: | 909 | static uint32_t last_fetch_time = 0; 910 | uint32_t now = millis(); 911 | if (now - last_fetch_time < 500) return; 912 | last_fetch_time = now; 913 | 914 | const auto timers = id(va).get_timers(); 915 | if (timers.empty()) return; 916 | 917 | auto output_timer = timers.begin()->second; 918 | for (auto &iterable_timer : timers) { 919 | if (iterable_timer.second.is_active && 920 | iterable_timer.second.seconds_left <= output_timer.seconds_left) { 921 | output_timer = iterable_timer.second; 922 | } 923 | } 924 | id(first_active_timer) = output_timer; 925 | 926 | - id: check_if_timers_active 927 | then: 928 | - lambda: | 929 | const auto timers = id(va).get_timers(); 930 | bool output = false; 931 | if (!timers.empty()) { 932 | for (auto &iterable_timer : timers) { 933 | if (iterable_timer.second.is_active) { 934 | output = true; 935 | break; 936 | } 937 | } 938 | } 939 | id(is_timer_active) = output; 940 | 941 | # Enable/disable the "stop" wake word around long TTS 942 | - id: activate_stop_word_once 943 | then: 944 | - delay: 1s 945 | - if: 946 | condition: 947 | switch.is_off: timer_ringing 948 | then: 949 | - micro_wake_word.enable_model: stop 950 | - wait_until: 951 | not: 952 | media_player.is_announcing: 953 | - if: 954 | condition: 955 | switch.is_off: timer_ringing 956 | then: 957 | - micro_wake_word.disable_model: stop 958 | 959 | # Alarm handling + time publishing 960 | - id: check_alarm 961 | then: 962 | - lambda: |- 963 | id(publish_current_time).execute(); 964 | if (id(alarm_on).state && id(alarm_time).has_state()) { 965 | auto set_alarm_time = id(alarm_time).state; 966 | if (set_alarm_time.length() == 5 && 967 | isdigit(set_alarm_time[0]) && isdigit(set_alarm_time[1]) && 968 | isdigit(set_alarm_time[3]) && isdigit(set_alarm_time[4])) { 969 | auto alarm_hour = std::stoi(set_alarm_time.substr(0, 2)); 970 | auto alarm_minute = std::stoi(set_alarm_time.substr(3, 2)); 971 | 972 | auto time_now = id(rtc).now(); 973 | if (time_now.hour == alarm_hour && time_now.minute == alarm_minute) { 974 | auto action = id(alarm_action).state; 975 | if (action == "Play sound") { 976 | id(timer_ringing).turn_on(); 977 | } else if (action == "Send event") { 978 | id(send_alarm_event).execute(); 979 | } else if (action == "Sound and event") { 980 | id(timer_ringing).turn_on(); 981 | id(send_alarm_event).execute(); 982 | } 983 | } 984 | } else { 985 | ESP_LOGW("alarm", "Incorrect alarm time setting"); 986 | } 987 | } 988 | 989 | - id: send_alarm_event 990 | then: 991 | - homeassistant.event: 992 | event: esphome.alarm_ringing 993 | 994 | - id: send_tts_uri_event 995 | parameters: 996 | tts_uri: string 997 | then: 998 | - homeassistant.event: 999 | event: esphome.tts_uri 1000 | data: 1001 | uri: !lambda return tts_uri; 1002 | 1003 | - id: send_stt_text_event 1004 | parameters: 1005 | stt_text: string 1006 | then: 1007 | - homeassistant.event: 1008 | event: esphome.stt_text 1009 | data: 1010 | text: !lambda return stt_text; 1011 | 1012 | - id: publish_current_time 1013 | mode: single 1014 | then: 1015 | - lambda: |- 1016 | static std::string last_time_string = ""; 1017 | auto time_now = id(rtc).now(); 1018 | std::string current_time_string = time_now.strftime("%H:%M"); 1019 | if (current_time_string != last_time_string) { 1020 | id(current_time).publish_state(current_time_string); 1021 | last_time_string = current_time_string; 1022 | } 1023 | 1024 | select: 1025 | - platform: template 1026 | name: "Wake word sensitivity" 1027 | optimistic: true 1028 | initial_option: Slightly sensitive 1029 | restore_value: true 1030 | entity_category: config 1031 | options: 1032 | - Slightly sensitive 1033 | - Moderately sensitive 1034 | - Very sensitive 1035 | on_value: 1036 | # Sets specific wake word probabilities computed for each particular model 1037 | # Note probability cutoffs are set as a quantized uint8 value, each comment has the corresponding floating point cutoff 1038 | # False Accepts per Hour values are tested against all units and channels from the Dinner Party Corpus. 1039 | # These cutoffs apply only to the specific models included in the firmware: okay_nabu@20241226.3, hey_jarvis@v2, hey_mycroft@v2 1040 | lambda: |- 1041 | if (x == "Slightly sensitive") { 1042 | id(okay_nabu).set_probability_cutoff(217); // 0.85 -> 0.000 FAPH on DipCo (Manifest's default) 1043 | //id(hey_jarvis).set_probability_cutoff(247); // 0.97 -> 0.563 FAPH on DipCo (Manifest's default) 1044 | //id(hey_mycroft).set_probability_cutoff(253); // 0.99 -> 0.567 FAPH on DipCo 1045 | } else if (x == "Moderately sensitive") { 1046 | id(okay_nabu).set_probability_cutoff(176); // 0.69 -> 0.376 FAPH on DipCo 1047 | //id(hey_jarvis).set_probability_cutoff(235); // 0.92 -> 0.939 FAPH on DipCo 1048 | //id(hey_mycroft).set_probability_cutoff(242); // 0.95 -> 1.502 FAPH on DipCo (Manifest's default) 1049 | } else if (x == "Very sensitive") { 1050 | id(okay_nabu).set_probability_cutoff(50); 1051 | //id(okay_nabu).set_probability_cutoff(143); // 0.56 -> 0.751 FAPH on DipCo 1052 | //id(hey_jarvis).set_probability_cutoff(212); // 0.83 -> 1.502 FAPH on DipCo 1053 | //id(hey_mycroft).set_probability_cutoff(237); // 0.93 -> 1.878 FAPH on DipCo 1054 | } 1055 | - platform: logger 1056 | id: logger_select 1057 | name: Logger Level 1058 | disabled_by_default: true 1059 | - platform: template 1060 | optimistic: true 1061 | name: "Alarm action" 1062 | id: alarm_action 1063 | icon: mdi:bell-plus 1064 | options: 1065 | - "Play sound" 1066 | - "Send event" 1067 | - "Sound and event" 1068 | initial_option: "Play sound" 1069 | on_value: 1070 | then: 1071 | - lambda: |- 1072 | id(saved_alarm_action) = x; 1073 | 1074 | 1075 | switch: 1076 | - platform: template #TODO: make react live 1077 | id: diag_disable_mic 1078 | name: "Diag: disable microphone" 1079 | optimistic: true 1080 | restore_mode: RESTORE_DEFAULT_OFF # default OFF = mic works as usual 1081 | 1082 | - platform: gpio 1083 | id: amp_enable_8 1084 | name: Amplifier 1085 | icon: mdi:speaker 1086 | entity_category: config 1087 | pin: 1088 | tca9555: ioexp 1089 | number: $amp_ctrl 1090 | inverted: false 1091 | restore_mode: RESTORE_DEFAULT_ON 1092 | 1093 | - platform: template 1094 | id: mic_mute_switch 1095 | name: "Microphone Mute" 1096 | icon: mdi:microphone-off 1097 | restore_mode: RESTORE_DEFAULT_OFF 1098 | lambda: |- 1099 | return id(mic_is_muted); 1100 | turn_on_action: 1101 | - lambda: |- 1102 | id(mic_is_muted) = true; 1103 | // "Mute" by dropping gain to 0 dB (effectively off for many capsules) 1104 | id(adc_mic).set_mic_gain(0.0f); 1105 | - script.execute: flash_ring 1106 | turn_off_action: 1107 | - lambda: |- 1108 | id(mic_is_muted) = false; 1109 | id(adc_mic).set_mic_gain(id(mic_gain_saved)); 1110 | - script.execute: flash_ring 1111 | 1112 | - platform: template 1113 | id: mute_sound 1114 | name: Mute/unmute sound 1115 | icon: "mdi:bullhorn" 1116 | entity_category: config 1117 | optimistic: true 1118 | restore_mode: RESTORE_DEFAULT_ON 1119 | 1120 | - platform: template 1121 | id: wake_sound 1122 | name: Wake sound 1123 | icon: "mdi:bullhorn" 1124 | entity_category: config 1125 | optimistic: true 1126 | restore_mode: RESTORE_DEFAULT_ON 1127 | 1128 | - platform: template 1129 | id: timer_ringing 1130 | optimistic: true 1131 | internal: true 1132 | restore_mode: ALWAYS_OFF 1133 | on_turn_off: 1134 | - micro_wake_word.disable_model: stop 1135 | - script.execute: disable_repeat 1136 | - if: 1137 | condition: 1138 | media_player.is_announcing: 1139 | then: 1140 | media_player.stop: 1141 | announcement: true 1142 | - mixer_speaker.apply_ducking: 1143 | id: media_mixing_input 1144 | decibel_reduction: 0 1145 | duration: 1.0s 1146 | - script.execute: control_leds 1147 | on_turn_on: 1148 | - mixer_speaker.apply_ducking: 1149 | id: media_mixing_input 1150 | decibel_reduction: 20 1151 | duration: 0.0s 1152 | - micro_wake_word.enable_model: stop 1153 | - script.execute: ring_timer 1154 | - script.execute: control_leds 1155 | - delay: 15min 1156 | - switch.turn_off: timer_ringing 1157 | 1158 | - platform: template 1159 | optimistic: true 1160 | restore_mode: RESTORE_DEFAULT_OFF 1161 | id: alarm_on 1162 | icon: mdi:bell-badge 1163 | name: "Alarm on" 1164 | on_turn_on: 1165 | - script.execute: control_leds 1166 | on_turn_off: 1167 | - script.execute: control_leds 1168 | 1169 | number: 1170 | - platform: template # MIC DEBUG 1171 | id: mic_gain_db 1172 | name: "Mic Gain (ES7210 dB)" 1173 | entity_category: config 1174 | min_value: 0 1175 | max_value: 42 1176 | step: 1 1177 | restore_value: true 1178 | initial_value: 24 1179 | set_action: 1180 | - lambda: |- 1181 | id(adc_mic).set_mic_gain(x); // adjust PGA on the fly 1182 | - logger.log: 1183 | format: "[MIC] Set ES7210 gain to %.0f dB" 1184 | args: ["x"] 1185 | 1186 | - platform: template 1187 | id: led_ring_brightness 1188 | name: "LED Ring Brightness" 1189 | icon: mdi:brightness-6 1190 | entity_category: config 1191 | optimistic: true 1192 | restore_value: true 1193 | min_value: 0.4 1194 | max_value: 1.0 1195 | step: 0.05 1196 | initial_value: 0.8 1197 | mode: slider 1198 | on_value: 1199 | - if: 1200 | condition: 1201 | light.is_on: status_ring 1202 | then: 1203 | - light.turn_on: 1204 | id: status_ring 1205 | brightness: !lambda 'return x;' 1206 | 1207 | 1208 | sensor: 1209 | 1210 | - platform: template 1211 | id: next_timer 1212 | name: "Next timer" 1213 | update_interval: never 1214 | disabled_by_default: true 1215 | device_class: duration 1216 | unit_of_measurement: s 1217 | icon: "mdi:timer" 1218 | accuracy_decimals: 0 1219 | 1220 | text_sensor: 1221 | - platform: template 1222 | id: next_timer_name 1223 | name: "Next timer name" 1224 | icon: "mdi:timer" 1225 | disabled_by_default: true 1226 | - platform: template 1227 | name: "Alarm time" 1228 | id: alarm_time 1229 | icon: mdi:bell-ring 1230 | - platform: template 1231 | name: "Current device time" 1232 | id: current_time 1233 | icon: mdi:clock 1234 | 1235 | button: 1236 | - platform: factory_reset 1237 | id: factory_reset_button 1238 | name: "Factory Reset" 1239 | entity_category: diagnostic 1240 | internal: true 1241 | - platform: restart 1242 | id: restart_button 1243 | name: "Restart" 1244 | entity_category: config 1245 | disabled_by_default: true 1246 | icon: "mdi:restart" 1247 | 1248 | binary_sensor: 1249 | - platform: gpio 1250 | name: "Key1" 1251 | pin: 1252 | tca9555: ioexp 1253 | number: 9 # not '12' as schematics say! 1254 | inverted: true 1255 | on_press: 1256 | - media_player.volume_down: external_media_player 1257 | 1258 | - platform: gpio 1259 | name: "Key2" 1260 | pin: 1261 | tca9555: ioexp 1262 | number: 10 1263 | inverted: true 1264 | on_press: 1265 | - media_player.toggle: external_media_player 1266 | 1267 | - platform: gpio 1268 | name: "Key3" 1269 | pin: 1270 | tca9555: ioexp 1271 | number: 11 1272 | inverted: true 1273 | on_press: 1274 | - media_player.volume_up: external_media_player 1275 | --------------------------------------------------------------------------------