esphome 2025.4.1__py3-none-any.whl → 2025.5.0b2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- esphome/components/ac_dimmer/ac_dimmer.cpp +3 -2
- esphome/components/adc/__init__.py +51 -34
- esphome/components/airthings_wave_base/__init__.py +1 -1
- esphome/components/alarm_control_panel/__init__.py +37 -2
- esphome/components/am43/cover/__init__.py +4 -5
- esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp +6 -4
- esphome/components/analog_threshold/analog_threshold_binary_sensor.h +4 -5
- esphome/components/analog_threshold/binary_sensor.py +10 -8
- esphome/components/anova/climate.py +4 -5
- esphome/components/api/__init__.py +25 -8
- esphome/components/api/api_connection.cpp +77 -10
- esphome/components/api/api_connection.h +6 -1
- esphome/components/api/api_frame_helper.cpp +98 -130
- esphome/components/api/api_frame_helper.h +12 -2
- esphome/components/api/api_noise_context.h +13 -4
- esphome/components/api/api_pb2.cpp +1422 -1
- esphome/components/api/api_pb2.h +255 -1
- esphome/components/api/api_pb2_service.cpp +162 -49
- esphome/components/api/api_pb2_service.h +90 -51
- esphome/components/api/api_pb2_size.h +361 -0
- esphome/components/api/api_server.cpp +110 -34
- esphome/components/api/api_server.h +8 -0
- esphome/components/api/proto.h +38 -9
- esphome/components/as3935_i2c/as3935_i2c.h +0 -3
- esphome/components/as7341/as7341.h +1 -1
- esphome/components/atm90e32/__init__.py +1 -0
- esphome/components/atm90e32/atm90e32.cpp +576 -199
- esphome/components/atm90e32/atm90e32.h +128 -31
- esphome/components/atm90e32/atm90e32_reg.h +4 -2
- esphome/components/atm90e32/button/__init__.py +62 -10
- esphome/components/atm90e32/button/atm90e32_button.cpp +63 -4
- esphome/components/atm90e32/button/atm90e32_button.h +36 -4
- esphome/components/atm90e32/number/__init__.py +130 -0
- esphome/components/atm90e32/number/atm90e32_number.h +16 -0
- esphome/components/atm90e32/sensor.py +21 -4
- esphome/components/atm90e32/text_sensor/__init__.py +48 -0
- esphome/components/audio/__init__.py +96 -49
- esphome/components/audio/audio.h +48 -0
- esphome/components/audio/audio_decoder.cpp +1 -1
- esphome/components/audio/audio_resampler.cpp +2 -0
- esphome/components/audio/audio_resampler.h +1 -0
- esphome/components/ballu/climate.py +2 -9
- esphome/components/bang_bang/climate.py +5 -6
- esphome/components/bedjet/climate/__init__.py +3 -8
- esphome/components/bedjet/fan/__init__.py +2 -11
- esphome/components/binary/fan/__init__.py +13 -16
- esphome/components/binary_sensor/__init__.py +13 -10
- esphome/components/binary_sensor/binary_sensor.cpp +6 -10
- esphome/components/binary_sensor/binary_sensor.h +1 -1
- esphome/components/binary_sensor/filter.cpp +21 -21
- esphome/components/binary_sensor/filter.h +10 -10
- esphome/components/bl0906/constants.h +16 -16
- esphome/components/ble_client/text_sensor/__init__.py +3 -5
- esphome/components/bluetooth_proxy/bluetooth_connection.cpp +4 -6
- esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +135 -21
- esphome/components/bluetooth_proxy/bluetooth_proxy.h +7 -0
- esphome/components/button/__init__.py +11 -8
- esphome/components/canbus/canbus.cpp +3 -0
- esphome/components/canbus/canbus.h +16 -0
- esphome/components/climate/__init__.py +35 -2
- esphome/components/climate/climate_mode.h +1 -1
- esphome/components/climate/climate_traits.h +63 -57
- esphome/components/climate_ir/__init__.py +57 -17
- esphome/components/climate_ir_lg/climate.py +2 -5
- esphome/components/climate_ir_lg/climate_ir_lg.cpp +7 -7
- esphome/components/climate_ir_lg/climate_ir_lg.h +1 -1
- esphome/components/color/__init__.py +2 -0
- esphome/components/const/__init__.py +5 -0
- esphome/components/coolix/climate.py +2 -9
- esphome/components/copy/cover/__init__.py +10 -9
- esphome/components/copy/fan/__init__.py +11 -9
- esphome/components/copy/lock/__init__.py +11 -9
- esphome/components/copy/text/__init__.py +9 -6
- esphome/components/cover/__init__.py +37 -2
- esphome/components/cst226/binary_sensor/__init__.py +28 -0
- esphome/components/cst226/binary_sensor/cs226_button.h +22 -0
- esphome/components/cst226/binary_sensor/cstt6_button.cpp +19 -0
- esphome/components/cst226/touchscreen/cst226_touchscreen.cpp +27 -5
- esphome/components/cst226/touchscreen/cst226_touchscreen.h +10 -10
- esphome/components/current_based/cover.py +37 -36
- esphome/components/daikin/climate.py +2 -9
- esphome/components/daikin/daikin.cpp +15 -9
- esphome/components/daikin/daikin.h +5 -5
- esphome/components/daikin_arc/climate.py +2 -7
- esphome/components/daikin_brc/climate.py +3 -5
- esphome/components/dallas_temp/dallas_temp.cpp +17 -24
- esphome/components/dallas_temp/dallas_temp.h +0 -1
- esphome/components/debug/debug_component.cpp +5 -0
- esphome/components/debug/debug_component.h +6 -0
- esphome/components/debug/debug_esp32.cpp +109 -254
- esphome/components/debug/sensor.py +14 -0
- esphome/components/deep_sleep/deep_sleep_esp32.cpp +13 -1
- esphome/components/delonghi/climate.py +2 -9
- esphome/components/demo/__init__.py +18 -20
- esphome/components/dfrobot_sen0395/switch/__init__.py +21 -22
- esphome/components/display/rect.cpp +4 -9
- esphome/components/display/rect.h +1 -1
- esphome/components/emmeti/climate.py +2 -9
- esphome/components/endstop/cover.py +17 -16
- esphome/components/esp32/__init__.py +60 -3
- esphome/components/esp32/core.cpp +11 -5
- esphome/components/esp32/gpio.cpp +86 -24
- esphome/components/esp32/gpio.py +15 -16
- esphome/components/esp32/gpio_esp32.py +1 -2
- esphome/components/esp32/gpio_esp32_c2.py +1 -1
- esphome/components/esp32/gpio_esp32_c3.py +1 -1
- esphome/components/esp32/gpio_esp32_c6.py +1 -1
- esphome/components/esp32/gpio_esp32_h2.py +1 -1
- esphome/components/esp32_ble/ble.cpp +1 -8
- esphome/components/esp32_ble/ble.h +5 -3
- esphome/components/esp32_ble/ble_advertising.h +1 -0
- esphome/components/esp32_ble_server/__init__.py +3 -0
- esphome/components/esp32_ble_tracker/__init__.py +7 -1
- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +192 -118
- esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +29 -3
- esphome/components/esp32_can/esp32_can.cpp +1 -1
- esphome/components/esp32_rmt_led_strip/led_strip.cpp +1 -1
- esphome/components/esp32_rmt_led_strip/led_strip.h +7 -5
- esphome/components/esp32_rmt_led_strip/light.py +9 -1
- esphome/components/esp8266/gpio.cpp +69 -8
- esphome/components/event/__init__.py +13 -10
- esphome/components/factory_reset/switch/__init__.py +7 -21
- esphome/components/fan/__init__.py +52 -5
- esphome/components/fastled_base/__init__.py +1 -4
- esphome/components/fastled_base/fastled_light.cpp +1 -1
- esphome/components/feedback/cover.py +38 -33
- esphome/components/fujitsu_general/climate.py +2 -9
- esphome/components/gpio/one_wire/gpio_one_wire.cpp +45 -43
- esphome/components/gpio/one_wire/gpio_one_wire.h +2 -1
- esphome/components/gpio_expander/cached_gpio.h +22 -7
- esphome/components/gps/__init__.py +11 -2
- esphome/components/gps/gps.cpp +11 -8
- esphome/components/gps/gps.h +9 -6
- esphome/components/graph/__init__.py +1 -2
- esphome/components/gree/climate.py +4 -6
- esphome/components/gree/gree.cpp +16 -2
- esphome/components/gree/gree.h +2 -2
- esphome/components/haier/climate.py +37 -34
- esphome/components/hbridge/fan/__init__.py +19 -17
- esphome/components/he60r/cover.py +4 -5
- esphome/components/heatpumpir/climate.py +3 -6
- esphome/components/hitachi_ac344/climate.py +2 -9
- esphome/components/hitachi_ac424/climate.py +2 -9
- esphome/components/hlw8012/hlw8012.cpp +1 -1
- esphome/components/hm3301/hm3301.h +1 -1
- esphome/components/http_request/__init__.py +39 -6
- esphome/components/http_request/http_request.cpp +20 -0
- esphome/components/http_request/http_request.h +57 -15
- esphome/components/http_request/http_request_arduino.cpp +22 -6
- esphome/components/http_request/http_request_arduino.h +4 -3
- esphome/components/http_request/http_request_host.cpp +141 -0
- esphome/components/http_request/http_request_host.h +37 -0
- esphome/components/http_request/http_request_idf.cpp +35 -3
- esphome/components/http_request/http_request_idf.h +10 -3
- esphome/components/http_request/httplib.h +9691 -0
- esphome/components/http_request/update/__init__.py +11 -8
- esphome/components/i2c/i2c.h +4 -0
- esphome/components/i2c/i2c_bus_esp_idf.cpp +1 -1
- esphome/components/i2s_audio/__init__.py +131 -22
- esphome/components/i2s_audio/i2s_audio.h +44 -4
- esphome/components/i2s_audio/media_player/__init__.py +19 -9
- esphome/components/i2s_audio/microphone/__init__.py +63 -5
- esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +351 -61
- esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +40 -6
- esphome/components/i2s_audio/speaker/__init__.py +31 -5
- esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +155 -19
- esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +17 -4
- esphome/components/ili9xxx/ili9xxx_init.h +1 -1
- esphome/components/image/__init__.py +37 -17
- esphome/components/image/image.cpp +25 -8
- esphome/components/internal_temperature/internal_temperature.cpp +6 -4
- esphome/components/key_collector/__init__.py +35 -0
- esphome/components/key_collector/key_collector.cpp +8 -0
- esphome/components/key_collector/key_collector.h +10 -0
- esphome/components/ld2410/ld2410.h +1 -1
- esphome/components/ld2450/ld2450.h +1 -1
- esphome/components/light/__init__.py +57 -0
- esphome/components/lock/__init__.py +51 -4
- esphome/components/lock/automation.h +2 -13
- esphome/components/logger/__init__.py +21 -0
- esphome/components/logger/logger.cpp +125 -95
- esphome/components/logger/logger.h +160 -35
- esphome/components/logger/task_log_buffer.cpp +138 -0
- esphome/components/logger/task_log_buffer.h +69 -0
- esphome/components/lvgl/__init__.py +13 -5
- esphome/components/lvgl/automation.py +50 -1
- esphome/components/lvgl/defines.py +0 -1
- esphome/components/lvgl/lv_validation.py +10 -1
- esphome/components/lvgl/lvgl_esphome.cpp +5 -1
- esphome/components/lvgl/schemas.py +14 -14
- esphome/components/lvgl/text/__init__.py +1 -2
- esphome/components/lvgl/widgets/arc.py +7 -6
- esphome/components/lvgl/widgets/buttonmatrix.py +3 -3
- esphome/components/lvgl/widgets/checkbox.py +2 -2
- esphome/components/lvgl/widgets/dropdown.py +2 -1
- esphome/components/lvgl/widgets/img.py +15 -12
- esphome/components/mapping/__init__.py +134 -0
- esphome/components/max7219digit/max7219digit.cpp +27 -27
- esphome/components/mdns/__init__.py +11 -5
- esphome/components/mdns/mdns_component.cpp +11 -5
- esphome/components/mdns/mdns_component.h +3 -2
- esphome/components/mdns/mdns_esp32.cpp +4 -3
- esphome/components/mdns/mdns_esp8266.cpp +4 -2
- esphome/components/mdns/mdns_libretiny.cpp +4 -2
- esphome/components/mdns/mdns_rp2040.cpp +4 -2
- esphome/components/media_player/__init__.py +40 -6
- esphome/components/micro_wake_word/__init__.py +99 -31
- esphome/components/micro_wake_word/automation.h +54 -0
- esphome/components/micro_wake_word/micro_wake_word.cpp +331 -319
- esphome/components/micro_wake_word/micro_wake_word.h +58 -105
- esphome/components/micro_wake_word/preprocessor_settings.h +19 -2
- esphome/components/micro_wake_word/streaming_model.cpp +158 -41
- esphome/components/micro_wake_word/streaming_model.h +85 -13
- esphome/components/microphone/__init__.py +139 -9
- esphome/components/microphone/automation.h +14 -2
- esphome/components/microphone/microphone.cpp +21 -0
- esphome/components/microphone/microphone.h +14 -5
- esphome/components/microphone/microphone_source.cpp +95 -0
- esphome/components/microphone/microphone_source.h +80 -0
- esphome/components/mics_4514/sensor.py +25 -14
- esphome/components/midea/climate.py +3 -4
- esphome/components/midea_ir/climate.py +3 -5
- esphome/components/mipi_spi/__init__.py +15 -0
- esphome/components/mipi_spi/display.py +474 -0
- esphome/components/mipi_spi/mipi_spi.cpp +481 -0
- esphome/components/mipi_spi/mipi_spi.h +171 -0
- esphome/components/mipi_spi/models/__init__.py +65 -0
- esphome/components/mipi_spi/models/amoled.py +72 -0
- esphome/components/mipi_spi/models/commands.py +82 -0
- esphome/components/mipi_spi/models/cyd.py +10 -0
- esphome/components/mipi_spi/models/ili.py +749 -0
- esphome/components/mipi_spi/models/jc.py +260 -0
- esphome/components/mipi_spi/models/lanbon.py +15 -0
- esphome/components/mipi_spi/models/lilygo.py +60 -0
- esphome/components/mipi_spi/models/waveshare.py +139 -0
- esphome/components/mitsubishi/climate.py +2 -5
- esphome/components/mitsubishi/mitsubishi.cpp +9 -9
- esphome/components/mixer/speaker/mixer_speaker.cpp +12 -22
- esphome/components/mixer/speaker/mixer_speaker.h +1 -3
- esphome/components/mlx90393/sensor.py +5 -0
- esphome/components/mlx90393/sensor_mlx90393.cpp +195 -13
- esphome/components/mlx90393/sensor_mlx90393.h +21 -4
- esphome/components/mqtt/__init__.py +1 -1
- esphome/components/mqtt/mqtt_client.cpp +5 -1
- esphome/components/mqtt/mqtt_const.h +4 -0
- esphome/components/mqtt/mqtt_fan.cpp +39 -0
- esphome/components/mqtt/mqtt_fan.h +2 -0
- esphome/components/network/__init__.py +1 -1
- esphome/components/nextion/base_component.py +17 -16
- esphome/components/nextion/display.py +11 -2
- esphome/components/nextion/nextion.cpp +39 -1
- esphome/components/nextion/nextion.h +50 -0
- esphome/components/noblex/climate.py +2 -9
- esphome/components/number/__init__.py +12 -9
- esphome/components/one_wire/one_wire_bus.cpp +14 -10
- esphome/components/one_wire/one_wire_bus.h +14 -8
- esphome/components/online_image/bmp_image.cpp +48 -11
- esphome/components/online_image/bmp_image.h +2 -0
- esphome/components/opentherm/binary_sensor/__init__.py +2 -4
- esphome/components/opentherm/number/__init__.py +11 -20
- esphome/components/opentherm/sensor/__init__.py +3 -3
- esphome/components/opentherm/switch/__init__.py +3 -5
- esphome/components/output/lock/__init__.py +11 -9
- esphome/components/packages/__init__.py +33 -31
- esphome/components/packet_transport/__init__.py +201 -0
- esphome/components/packet_transport/binary_sensor.py +19 -0
- esphome/components/packet_transport/packet_transport.cpp +534 -0
- esphome/components/packet_transport/packet_transport.h +154 -0
- esphome/components/packet_transport/sensor.py +19 -0
- esphome/components/pca9685/pca9685_output.cpp +2 -1
- esphome/components/pid/climate.py +2 -4
- esphome/components/pm2005/__init__.py +1 -0
- esphome/components/pm2005/pm2005.cpp +123 -0
- esphome/components/pm2005/pm2005.h +46 -0
- esphome/components/pm2005/sensor.py +86 -0
- esphome/components/pmsa003i/pmsa003i.cpp +43 -16
- esphome/components/pmsa003i/pmsa003i.h +25 -25
- esphome/components/pmsx003/pmsx003.cpp +193 -229
- esphome/components/pmsx003/pmsx003.h +51 -33
- esphome/components/pmsx003/sensor.py +21 -11
- esphome/components/pn7150/pn7150.h +2 -2
- esphome/components/pn7160/pn7160.h +2 -2
- esphome/components/prometheus/prometheus_handler.cpp +174 -0
- esphome/components/prometheus/prometheus_handler.h +17 -0
- esphome/components/psram/__init__.py +7 -5
- esphome/components/pulse_meter/pulse_meter_sensor.cpp +32 -12
- esphome/components/pulse_meter/pulse_meter_sensor.h +5 -5
- esphome/components/qspi_dbi/__init__.py +0 -1
- esphome/components/qspi_dbi/display.py +2 -1
- esphome/components/qspi_dbi/models.py +1 -2
- esphome/components/remote_base/__init__.py +91 -0
- esphome/components/remote_base/beo4_protocol.cpp +153 -0
- esphome/components/remote_base/beo4_protocol.h +43 -0
- esphome/components/remote_base/gobox_protocol.cpp +131 -0
- esphome/components/remote_base/gobox_protocol.h +54 -0
- esphome/components/remote_receiver/remote_receiver_esp32.cpp +16 -9
- esphome/components/resampler/speaker/resampler_speaker.cpp +12 -10
- esphome/components/resampler/speaker/resampler_speaker.h +1 -1
- esphome/components/scd30/sensor.py +2 -3
- esphome/components/scd4x/sensor.py +4 -5
- esphome/components/sdp3x/sensor.py +2 -1
- esphome/components/select/__init__.py +19 -20
- esphome/components/sen5x/sensor.py +1 -1
- esphome/components/sensor/__init__.py +158 -14
- esphome/components/sensor/filter.cpp +23 -0
- esphome/components/sensor/filter.h +22 -0
- esphome/components/sgp4x/sensor.py +1 -1
- esphome/components/sht4x/sht4x.cpp +43 -22
- esphome/components/sht4x/sht4x.h +1 -1
- esphome/components/sml/text_sensor/__init__.py +4 -6
- esphome/components/sound_level/__init__.py +0 -0
- esphome/components/sound_level/sensor.py +97 -0
- esphome/components/sound_level/sound_level.cpp +194 -0
- esphome/components/sound_level/sound_level.h +73 -0
- esphome/components/speaker/media_player/__init__.py +4 -8
- esphome/components/speaker/media_player/speaker_media_player.cpp +0 -18
- esphome/components/speaker/media_player/speaker_media_player.h +0 -11
- esphome/components/speaker/speaker.h +4 -7
- esphome/components/speed/fan/__init__.py +17 -16
- esphome/components/spi/spi.h +11 -1
- esphome/components/sprinkler/__init__.py +18 -19
- esphome/components/switch/__init__.py +32 -42
- esphome/components/syslog/__init__.py +41 -0
- esphome/components/syslog/esphome_syslog.cpp +49 -0
- esphome/components/syslog/esphome_syslog.h +27 -0
- esphome/components/tca9555/tca9555.cpp +11 -6
- esphome/components/tcl112/climate.py +2 -9
- esphome/components/template/alarm_control_panel/__init__.py +7 -6
- esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +21 -17
- esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +2 -1
- esphome/components/template/cover/__init__.py +27 -21
- esphome/components/template/fan/__init__.py +14 -12
- esphome/components/template/lock/__init__.py +20 -25
- esphome/components/template/lock/automation.h +18 -0
- esphome/components/template/text/__init__.py +4 -3
- esphome/components/template/valve/__init__.py +32 -21
- esphome/components/template/valve/automation.h +24 -0
- esphome/components/text/__init__.py +32 -1
- esphome/components/text_sensor/__init__.py +24 -29
- esphome/components/thermostat/climate.py +5 -5
- esphome/components/time_based/cover.py +17 -16
- esphome/components/tm1638/switch/__init__.py +10 -7
- esphome/components/tormatic/cover.py +4 -5
- esphome/components/toshiba/climate.py +3 -5
- esphome/components/touchscreen/touchscreen.cpp +3 -1
- esphome/components/tt21100/touchscreen/tt21100.cpp +1 -1
- esphome/components/tuya/climate/__init__.py +5 -6
- esphome/components/tuya/cover/__init__.py +6 -11
- esphome/components/tuya/select/__init__.py +15 -5
- esphome/components/tuya/select/tuya_select.cpp +6 -1
- esphome/components/tuya/select/tuya_select.h +5 -1
- esphome/components/uart/packet_transport/__init__.py +20 -0
- esphome/components/uart/packet_transport/uart_transport.cpp +88 -0
- esphome/components/uart/packet_transport/uart_transport.h +41 -0
- esphome/components/udp/__init__.py +126 -128
- esphome/components/udp/automation.h +40 -0
- esphome/components/udp/binary_sensor.py +3 -25
- esphome/components/udp/packet_transport/__init__.py +29 -0
- esphome/components/udp/packet_transport/udp_transport.cpp +36 -0
- esphome/components/udp/packet_transport/udp_transport.h +28 -0
- esphome/components/udp/sensor.py +3 -25
- esphome/components/udp/udp_component.cpp +26 -470
- esphome/components/udp/udp_component.h +21 -128
- esphome/components/update/__init__.py +31 -1
- esphome/components/uponor_smatrix/climate/__init__.py +4 -9
- esphome/components/uptime/text_sensor/__init__.py +47 -7
- esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +12 -7
- esphome/components/uptime/text_sensor/uptime_text_sensor.h +19 -0
- esphome/components/valve/__init__.py +34 -3
- esphome/components/valve/automation.h +1 -19
- esphome/components/vl53l0x/sensor.py +11 -0
- esphome/components/vl53l0x/vl53l0x_sensor.cpp +5 -1
- esphome/components/vl53l0x/vl53l0x_sensor.h +2 -1
- esphome/components/voice_assistant/__init__.py +36 -10
- esphome/components/voice_assistant/voice_assistant.cpp +170 -144
- esphome/components/voice_assistant/voice_assistant.h +26 -25
- esphome/components/waveshare_epaper/display.py +6 -0
- esphome/components/waveshare_epaper/waveshare_epaper.cpp +439 -37
- esphome/components/waveshare_epaper/waveshare_epaper.h +60 -11
- esphome/components/whirlpool/climate.py +3 -5
- esphome/components/whynter/climate.py +3 -5
- esphome/components/xpt2046/touchscreen/xpt2046.cpp +1 -1
- esphome/components/yashima/climate.py +6 -6
- esphome/components/zhlt01/climate.py +2 -7
- esphome/config_validation.py +38 -58
- esphome/const.py +15 -1
- esphome/core/__init__.py +2 -0
- esphome/core/application.cpp +1 -0
- esphome/core/application.h +4 -0
- esphome/core/automation.h +4 -3
- esphome/core/component.cpp +19 -3
- esphome/core/component.h +5 -0
- esphome/core/defines.h +23 -17
- esphome/core/macros.h +4 -0
- esphome/core/scheduler.cpp +3 -0
- esphome/cpp_generator.py +6 -2
- esphome/dashboard/web_server.py +3 -3
- esphome/helpers.py +39 -0
- esphome/loader.py +4 -0
- esphome/mqtt.py +21 -8
- esphome/platformio_api.py +1 -1
- esphome/schema_extractors.py +0 -1
- esphome/vscode.py +15 -0
- esphome/wizard.py +2 -2
- esphome/zeroconf.py +7 -3
- {esphome-2025.4.1.dist-info → esphome-2025.5.0b2.dist-info}/METADATA +10 -11
- {esphome-2025.4.1.dist-info → esphome-2025.5.0b2.dist-info}/RECORD +411 -352
- {esphome-2025.4.1.dist-info → esphome-2025.5.0b2.dist-info}/WHEEL +1 -1
- esphome/components/esp32_ble/const_esp32c6.h +0 -74
- {esphome-2025.4.1.dist-info → esphome-2025.5.0b2.dist-info}/entry_points.txt +0 -0
- {esphome-2025.4.1.dist-info → esphome-2025.5.0b2.dist-info}/licenses/LICENSE +0 -0
- {esphome-2025.4.1.dist-info → esphome-2025.5.0b2.dist-info}/top_level.txt +0 -0
@@ -4,6 +4,8 @@
|
|
4
4
|
|
5
5
|
#include "preprocessor_settings.h"
|
6
6
|
|
7
|
+
#include "esphome/core/preferences.h"
|
8
|
+
|
7
9
|
#include <tensorflow/lite/core/c/common.h>
|
8
10
|
#include <tensorflow/lite/micro/micro_interpreter.h>
|
9
11
|
#include <tensorflow/lite/micro/micro_mutable_op_resolver.h>
|
@@ -11,31 +13,71 @@
|
|
11
13
|
namespace esphome {
|
12
14
|
namespace micro_wake_word {
|
13
15
|
|
16
|
+
static const uint8_t MIN_SLICES_BEFORE_DETECTION = 100;
|
14
17
|
static const uint32_t STREAMING_MODEL_VARIABLE_ARENA_SIZE = 1024;
|
15
18
|
|
19
|
+
struct DetectionEvent {
|
20
|
+
std::string *wake_word;
|
21
|
+
bool detected;
|
22
|
+
bool partially_detection; // Set if the most recent probability exceed the threshold, but the sliding window average
|
23
|
+
// hasn't yet
|
24
|
+
uint8_t max_probability;
|
25
|
+
uint8_t average_probability;
|
26
|
+
bool blocked_by_vad = false;
|
27
|
+
};
|
28
|
+
|
16
29
|
class StreamingModel {
|
17
30
|
public:
|
18
31
|
virtual void log_model_config() = 0;
|
19
|
-
virtual
|
32
|
+
virtual DetectionEvent determine_detected() = 0;
|
20
33
|
|
34
|
+
// Performs inference on the given features.
|
35
|
+
// - If the model is enabled but not loaded, it will load it
|
36
|
+
// - If the model is disabled but loaded, it will unload it
|
37
|
+
// Returns true if sucessful or false if there is an error
|
21
38
|
bool perform_streaming_inference(const int8_t features[PREPROCESSOR_FEATURE_SIZE]);
|
22
39
|
|
23
|
-
/// @brief Sets all recent_streaming_probabilities to 0
|
40
|
+
/// @brief Sets all recent_streaming_probabilities to 0 and resets the ignore window count
|
24
41
|
void reset_probabilities();
|
25
42
|
|
26
|
-
/// @brief Allocates tensor and variable arenas and sets up the model interpreter
|
27
|
-
/// @param op_resolver MicroMutableOpResolver object that must exist until the model is unloaded
|
28
|
-
/// @return True if successful, false otherwise
|
29
|
-
bool load_model(tflite::MicroMutableOpResolver<20> &op_resolver);
|
30
|
-
|
31
43
|
/// @brief Destroys the TFLite interpreter and frees the tensor and variable arenas' memory
|
32
44
|
void unload_model();
|
33
45
|
|
46
|
+
/// @brief Enable the model. The next performing_streaming_inference call will load it.
|
47
|
+
virtual void enable() { this->enabled_ = true; }
|
48
|
+
|
49
|
+
/// @brief Disable the model. The next performing_streaming_inference call will unload it.
|
50
|
+
virtual void disable() { this->enabled_ = false; }
|
51
|
+
|
52
|
+
/// @brief Return true if the model is enabled.
|
53
|
+
bool is_enabled() const { return this->enabled_; }
|
54
|
+
|
55
|
+
bool get_unprocessed_probability_status() const { return this->unprocessed_probability_status_; }
|
56
|
+
|
57
|
+
// Quantized probability cutoffs mapping 0.0 - 1.0 to 0 - 255
|
58
|
+
uint8_t get_default_probability_cutoff() const { return this->default_probability_cutoff_; }
|
59
|
+
uint8_t get_probability_cutoff() const { return this->probability_cutoff_; }
|
60
|
+
void set_probability_cutoff(uint8_t probability_cutoff) { this->probability_cutoff_ = probability_cutoff; }
|
61
|
+
|
34
62
|
protected:
|
63
|
+
/// @brief Allocates tensor and variable arenas and sets up the model interpreter
|
64
|
+
/// @return True if successful, false otherwise
|
65
|
+
bool load_model_();
|
66
|
+
/// @brief Returns true if successfully registered the streaming model's TensorFlow operations
|
67
|
+
bool register_streaming_ops_(tflite::MicroMutableOpResolver<20> &op_resolver);
|
68
|
+
|
69
|
+
tflite::MicroMutableOpResolver<20> streaming_op_resolver_;
|
70
|
+
|
71
|
+
bool loaded_{false};
|
72
|
+
bool enabled_{true};
|
73
|
+
bool unprocessed_probability_status_{false};
|
35
74
|
uint8_t current_stride_step_{0};
|
75
|
+
int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION};
|
36
76
|
|
37
|
-
|
77
|
+
uint8_t default_probability_cutoff_;
|
78
|
+
uint8_t probability_cutoff_;
|
38
79
|
size_t sliding_window_size_;
|
80
|
+
|
39
81
|
size_t last_n_index_{0};
|
40
82
|
size_t tensor_arena_size_;
|
41
83
|
std::vector<uint8_t> recent_streaming_probabilities_;
|
@@ -50,32 +92,62 @@ class StreamingModel {
|
|
50
92
|
|
51
93
|
class WakeWordModel final : public StreamingModel {
|
52
94
|
public:
|
53
|
-
|
54
|
-
|
95
|
+
/// @brief Constructs a wake word model object
|
96
|
+
/// @param id (std::string) identifier for this model
|
97
|
+
/// @param model_start (const uint8_t *) pointer to the start of the model's TFLite FlatBuffer
|
98
|
+
/// @param default_probability_cutoff (uint8_t) probability cutoff for acceping the wake word has been said
|
99
|
+
/// @param sliding_window_average_size (size_t) the length of the sliding window computing the mean rolling
|
100
|
+
/// probability
|
101
|
+
/// @param wake_word (std::string) Friendly name of the wake word
|
102
|
+
/// @param tensor_arena_size (size_t) Size in bytes for allocating the tensor arena
|
103
|
+
/// @param default_enabled (bool) If true, it will be enabled by default on first boot
|
104
|
+
/// @param internal_only (bool) If true, the model will not be exposed to HomeAssistant as an available model
|
105
|
+
WakeWordModel(const std::string &id, const uint8_t *model_start, uint8_t default_probability_cutoff,
|
106
|
+
size_t sliding_window_average_size, const std::string &wake_word, size_t tensor_arena_size,
|
107
|
+
bool default_enabled, bool internal_only);
|
55
108
|
|
56
109
|
void log_model_config() override;
|
57
110
|
|
58
111
|
/// @brief Checks for the wake word by comparing the mean probability in the sliding window with the probability
|
59
112
|
/// cutoff
|
60
113
|
/// @return True if wake word is detected, false otherwise
|
61
|
-
|
114
|
+
DetectionEvent determine_detected() override;
|
62
115
|
|
116
|
+
const std::string &get_id() const { return this->id_; }
|
63
117
|
const std::string &get_wake_word() const { return this->wake_word_; }
|
64
118
|
|
119
|
+
void add_trained_language(const std::string &language) { this->trained_languages_.push_back(language); }
|
120
|
+
const std::vector<std::string> &get_trained_languages() const { return this->trained_languages_; }
|
121
|
+
|
122
|
+
/// @brief Enable the model and save to flash. The next performing_streaming_inference call will load it.
|
123
|
+
void enable() override;
|
124
|
+
|
125
|
+
/// @brief Disable the model and save to flash. The next performing_streaming_inference call will unload it.
|
126
|
+
void disable() override;
|
127
|
+
|
128
|
+
bool get_internal_only() { return this->internal_only_; }
|
129
|
+
|
65
130
|
protected:
|
131
|
+
std::string id_;
|
66
132
|
std::string wake_word_;
|
133
|
+
std::vector<std::string> trained_languages_;
|
134
|
+
|
135
|
+
bool internal_only_;
|
136
|
+
|
137
|
+
ESPPreferenceObject pref_;
|
67
138
|
};
|
68
139
|
|
69
140
|
class VADModel final : public StreamingModel {
|
70
141
|
public:
|
71
|
-
VADModel(const uint8_t *model_start,
|
142
|
+
VADModel(const uint8_t *model_start, uint8_t default_probability_cutoff, size_t sliding_window_size,
|
143
|
+
size_t tensor_arena_size);
|
72
144
|
|
73
145
|
void log_model_config() override;
|
74
146
|
|
75
147
|
/// @brief Checks for voice activity by comparing the max probability in the sliding window with the probability
|
76
148
|
/// cutoff
|
77
149
|
/// @return True if voice activity is detected, false otherwise
|
78
|
-
|
150
|
+
DetectionEvent determine_detected() override;
|
79
151
|
};
|
80
152
|
|
81
153
|
} // namespace micro_wake_word
|
@@ -1,12 +1,21 @@
|
|
1
1
|
from esphome import automation
|
2
2
|
from esphome.automation import maybe_simple_id
|
3
3
|
import esphome.codegen as cg
|
4
|
+
from esphome.components import audio
|
4
5
|
import esphome.config_validation as cv
|
5
|
-
from esphome.const import
|
6
|
+
from esphome.const import (
|
7
|
+
CONF_BITS_PER_SAMPLE,
|
8
|
+
CONF_CHANNELS,
|
9
|
+
CONF_GAIN_FACTOR,
|
10
|
+
CONF_ID,
|
11
|
+
CONF_MICROPHONE,
|
12
|
+
CONF_TRIGGER_ID,
|
13
|
+
)
|
6
14
|
from esphome.core import CORE
|
7
15
|
from esphome.coroutine import coroutine_with_priority
|
8
16
|
|
9
|
-
|
17
|
+
AUTO_LOAD = ["audio"]
|
18
|
+
CODEOWNERS = ["@jesserockz", "@kahrendt"]
|
10
19
|
|
11
20
|
IS_PLATFORM_COMPONENT = True
|
12
21
|
|
@@ -15,6 +24,7 @@ CONF_ON_DATA = "on_data"
|
|
15
24
|
microphone_ns = cg.esphome_ns.namespace("microphone")
|
16
25
|
|
17
26
|
Microphone = microphone_ns.class_("Microphone")
|
27
|
+
MicrophoneSource = microphone_ns.class_("MicrophoneSource")
|
18
28
|
|
19
29
|
CaptureAction = microphone_ns.class_(
|
20
30
|
"CaptureAction", automation.Action, cg.Parented.template(Microphone)
|
@@ -22,16 +32,23 @@ CaptureAction = microphone_ns.class_(
|
|
22
32
|
StopCaptureAction = microphone_ns.class_(
|
23
33
|
"StopCaptureAction", automation.Action, cg.Parented.template(Microphone)
|
24
34
|
)
|
35
|
+
MuteAction = microphone_ns.class_(
|
36
|
+
"MuteAction", automation.Action, cg.Parented.template(Microphone)
|
37
|
+
)
|
38
|
+
UnmuteAction = microphone_ns.class_(
|
39
|
+
"UnmuteAction", automation.Action, cg.Parented.template(Microphone)
|
40
|
+
)
|
25
41
|
|
26
42
|
|
27
43
|
DataTrigger = microphone_ns.class_(
|
28
44
|
"DataTrigger",
|
29
|
-
automation.Trigger.template(cg.std_vector.template(cg.
|
45
|
+
automation.Trigger.template(cg.std_vector.template(cg.uint8).operator("ref")),
|
30
46
|
)
|
31
47
|
|
32
48
|
IsCapturingCondition = microphone_ns.class_(
|
33
49
|
"IsCapturingCondition", automation.Condition
|
34
50
|
)
|
51
|
+
IsMutedCondition = microphone_ns.class_("IsMutedCondition", automation.Condition)
|
35
52
|
|
36
53
|
|
37
54
|
async def setup_microphone_core_(var, config):
|
@@ -39,7 +56,7 @@ async def setup_microphone_core_(var, config):
|
|
39
56
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
40
57
|
await automation.build_automation(
|
41
58
|
trigger,
|
42
|
-
[(cg.std_vector.template(cg.
|
59
|
+
[(cg.std_vector.template(cg.uint8).operator("ref").operator("const"), "x")],
|
43
60
|
conf,
|
44
61
|
)
|
45
62
|
|
@@ -50,7 +67,7 @@ async def register_microphone(var, config):
|
|
50
67
|
await setup_microphone_core_(var, config)
|
51
68
|
|
52
69
|
|
53
|
-
MICROPHONE_SCHEMA = cv.Schema(
|
70
|
+
MICROPHONE_SCHEMA = cv.Schema.extend(audio.AUDIO_COMPONENT_SCHEMA).extend(
|
54
71
|
{
|
55
72
|
cv.Optional(CONF_ON_DATA): automation.validate_automation(
|
56
73
|
{
|
@@ -64,7 +81,110 @@ MICROPHONE_SCHEMA = cv.Schema(
|
|
64
81
|
MICROPHONE_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(Microphone)})
|
65
82
|
|
66
83
|
|
67
|
-
|
84
|
+
def microphone_source_schema(
|
85
|
+
min_bits_per_sample: int = 16,
|
86
|
+
max_bits_per_sample: int = 16,
|
87
|
+
min_channels: int = 1,
|
88
|
+
max_channels: int = 1,
|
89
|
+
):
|
90
|
+
"""Schema for a microphone source
|
91
|
+
|
92
|
+
Components requesting microphone data should use this schema instead of accessing a microphone directly.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
min_bits_per_sample (int, optional): Minimum number of bits per sample the requesting component supports. Defaults to 16.
|
96
|
+
max_bits_per_sample (int, optional): Maximum number of bits per sample the requesting component supports. Defaults to 16.
|
97
|
+
min_channels (int, optional): Minimum number of channels the requesting component supports. Defaults to 1.
|
98
|
+
max_channels (int, optional): Maximum number of channels the requesting component supports. Defaults to 1.
|
99
|
+
"""
|
100
|
+
|
101
|
+
def _validate_unique_channels(config):
|
102
|
+
if len(config) != len(set(config)):
|
103
|
+
raise cv.Invalid("Channels must be unique")
|
104
|
+
return config
|
105
|
+
|
106
|
+
return cv.All(
|
107
|
+
automation.maybe_conf(
|
108
|
+
CONF_MICROPHONE,
|
109
|
+
{
|
110
|
+
cv.GenerateID(CONF_ID): cv.declare_id(MicrophoneSource),
|
111
|
+
cv.GenerateID(CONF_MICROPHONE): cv.use_id(Microphone),
|
112
|
+
cv.Optional(CONF_BITS_PER_SAMPLE, default=16): cv.int_range(
|
113
|
+
min_bits_per_sample, max_bits_per_sample
|
114
|
+
),
|
115
|
+
cv.Optional(CONF_CHANNELS, default="0"): cv.All(
|
116
|
+
cv.ensure_list(cv.int_range(0, 7)),
|
117
|
+
cv.Length(min=min_channels, max=max_channels),
|
118
|
+
_validate_unique_channels,
|
119
|
+
),
|
120
|
+
cv.Optional(CONF_GAIN_FACTOR, default="1"): cv.int_range(1, 64),
|
121
|
+
},
|
122
|
+
),
|
123
|
+
)
|
124
|
+
|
125
|
+
|
126
|
+
def final_validate_microphone_source_schema(
|
127
|
+
component_name: str, sample_rate: int = cv.UNDEFINED
|
128
|
+
):
|
129
|
+
"""Validates that the microphone source can provide audio in the correct format. In particular it validates the sample rate and the enabled channels.
|
130
|
+
|
131
|
+
Note that:
|
132
|
+
- MicrophoneSource class automatically handles converting bits per sample, so no need to validate
|
133
|
+
- microphone_source_schema already validates that channels are unique and specifies the max number of channels the component supports
|
134
|
+
|
135
|
+
Args:
|
136
|
+
component_name (str): The name of the component requesting mic audio
|
137
|
+
sample_rate (int, optional): The sample rate the component requesting mic audio requires
|
138
|
+
"""
|
139
|
+
|
140
|
+
def _validate_audio_compatability(config):
|
141
|
+
if sample_rate is not cv.UNDEFINED:
|
142
|
+
# Issues require changing the microphone configuration
|
143
|
+
# - Verifies sample rates match
|
144
|
+
audio.final_validate_audio_schema(
|
145
|
+
component_name,
|
146
|
+
audio_device=CONF_MICROPHONE,
|
147
|
+
sample_rate=sample_rate,
|
148
|
+
audio_device_issue=True,
|
149
|
+
)(config)
|
150
|
+
|
151
|
+
# Issues require changing the MicrophoneSource configuration
|
152
|
+
# - Verifies that each of the enabled channels are available
|
153
|
+
audio.final_validate_audio_schema(
|
154
|
+
component_name,
|
155
|
+
audio_device=CONF_MICROPHONE,
|
156
|
+
enabled_channels=config[CONF_CHANNELS],
|
157
|
+
audio_device_issue=False,
|
158
|
+
)(config)
|
159
|
+
|
160
|
+
return config
|
161
|
+
|
162
|
+
return _validate_audio_compatability
|
163
|
+
|
164
|
+
|
165
|
+
async def microphone_source_to_code(config, passive=False):
|
166
|
+
"""Creates a MicrophoneSource variable for codegen.
|
167
|
+
|
168
|
+
Setting passive to true makes the MicrophoneSource never start/stop the microphone, but only receives audio when another component has actively started the Microphone. If false, then the microphone needs to be explicitly started/stopped.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
config (Schema): Created with `microphone_source_schema` specifying bits per sample, channels, and gain factor
|
172
|
+
passive (bool): Enable passive mode for the MicrophoneSource
|
173
|
+
"""
|
174
|
+
mic = await cg.get_variable(config[CONF_MICROPHONE])
|
175
|
+
mic_source = cg.new_Pvariable(
|
176
|
+
config[CONF_ID],
|
177
|
+
mic,
|
178
|
+
config[CONF_BITS_PER_SAMPLE],
|
179
|
+
config[CONF_GAIN_FACTOR],
|
180
|
+
passive,
|
181
|
+
)
|
182
|
+
for channel in config[CONF_CHANNELS]:
|
183
|
+
cg.add(mic_source.add_channel(channel))
|
184
|
+
return mic_source
|
185
|
+
|
186
|
+
|
187
|
+
async def microphone_action(config, action_id, template_arg, args):
|
68
188
|
var = cg.new_Pvariable(action_id, template_arg)
|
69
189
|
await cg.register_parented(var, config[CONF_ID])
|
70
190
|
return var
|
@@ -72,15 +192,25 @@ async def media_player_action(config, action_id, template_arg, args):
|
|
72
192
|
|
73
193
|
automation.register_action(
|
74
194
|
"microphone.capture", CaptureAction, MICROPHONE_ACTION_SCHEMA
|
75
|
-
)(
|
195
|
+
)(microphone_action)
|
76
196
|
|
77
197
|
automation.register_action(
|
78
198
|
"microphone.stop_capture", StopCaptureAction, MICROPHONE_ACTION_SCHEMA
|
79
|
-
)(
|
199
|
+
)(microphone_action)
|
200
|
+
|
201
|
+
automation.register_action("microphone.mute", MuteAction, MICROPHONE_ACTION_SCHEMA)(
|
202
|
+
microphone_action
|
203
|
+
)
|
204
|
+
automation.register_action("microphone.unmute", UnmuteAction, MICROPHONE_ACTION_SCHEMA)(
|
205
|
+
microphone_action
|
206
|
+
)
|
80
207
|
|
81
208
|
automation.register_condition(
|
82
209
|
"microphone.is_capturing", IsCapturingCondition, MICROPHONE_ACTION_SCHEMA
|
83
|
-
)(
|
210
|
+
)(microphone_action)
|
211
|
+
automation.register_condition(
|
212
|
+
"microphone.is_muted", IsMutedCondition, MICROPHONE_ACTION_SCHEMA
|
213
|
+
)(microphone_action)
|
84
214
|
|
85
215
|
|
86
216
|
@coroutine_with_priority(100.0)
|
@@ -16,10 +16,17 @@ template<typename... Ts> class StopCaptureAction : public Action<Ts...>, public
|
|
16
16
|
void play(Ts... x) override { this->parent_->stop(); }
|
17
17
|
};
|
18
18
|
|
19
|
-
class
|
19
|
+
template<typename... Ts> class MuteAction : public Action<Ts...>, public Parented<Microphone> {
|
20
|
+
void play(Ts... x) override { this->parent_->set_mute_state(true); }
|
21
|
+
};
|
22
|
+
template<typename... Ts> class UnmuteAction : public Action<Ts...>, public Parented<Microphone> {
|
23
|
+
void play(Ts... x) override { this->parent_->set_mute_state(false); }
|
24
|
+
};
|
25
|
+
|
26
|
+
class DataTrigger : public Trigger<const std::vector<uint8_t> &> {
|
20
27
|
public:
|
21
28
|
explicit DataTrigger(Microphone *mic) {
|
22
|
-
mic->add_data_callback([this](const std::vector<
|
29
|
+
mic->add_data_callback([this](const std::vector<uint8_t> &data) { this->trigger(data); });
|
23
30
|
}
|
24
31
|
};
|
25
32
|
|
@@ -28,5 +35,10 @@ template<typename... Ts> class IsCapturingCondition : public Condition<Ts...>, p
|
|
28
35
|
bool check(Ts... x) override { return this->parent_->is_running(); }
|
29
36
|
};
|
30
37
|
|
38
|
+
template<typename... Ts> class IsMutedCondition : public Condition<Ts...>, public Parented<Microphone> {
|
39
|
+
public:
|
40
|
+
bool check(Ts... x) override { return this->parent_->get_mute_state(); }
|
41
|
+
};
|
42
|
+
|
31
43
|
} // namespace microphone
|
32
44
|
} // namespace esphome
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#include "microphone.h"
|
2
|
+
|
3
|
+
namespace esphome {
|
4
|
+
namespace microphone {
|
5
|
+
|
6
|
+
void Microphone::add_data_callback(std::function<void(const std::vector<uint8_t> &)> &&data_callback) {
|
7
|
+
std::function<void(const std::vector<uint8_t> &)> mute_handled_callback =
|
8
|
+
[this, data_callback](const std::vector<uint8_t> &data) { data_callback(this->silence_audio_(data)); };
|
9
|
+
this->data_callbacks_.add(std::move(mute_handled_callback));
|
10
|
+
}
|
11
|
+
|
12
|
+
std::vector<uint8_t> Microphone::silence_audio_(std::vector<uint8_t> data) {
|
13
|
+
if (this->mute_state_) {
|
14
|
+
std::memset((void *) data.data(), 0, data.size());
|
15
|
+
}
|
16
|
+
|
17
|
+
return data;
|
18
|
+
}
|
19
|
+
|
20
|
+
} // namespace microphone
|
21
|
+
} // namespace esphome
|
@@ -1,5 +1,7 @@
|
|
1
1
|
#pragma once
|
2
2
|
|
3
|
+
#include "esphome/components/audio/audio.h"
|
4
|
+
|
3
5
|
#include <cstddef>
|
4
6
|
#include <cstdint>
|
5
7
|
#include <functional>
|
@@ -20,18 +22,25 @@ class Microphone {
|
|
20
22
|
public:
|
21
23
|
virtual void start() = 0;
|
22
24
|
virtual void stop() = 0;
|
23
|
-
void add_data_callback(std::function<void(const std::vector<
|
24
|
-
this->data_callbacks_.add(std::move(data_callback));
|
25
|
-
}
|
26
|
-
virtual size_t read(int16_t *buf, size_t len) = 0;
|
25
|
+
void add_data_callback(std::function<void(const std::vector<uint8_t> &)> &&data_callback);
|
27
26
|
|
28
27
|
bool is_running() const { return this->state_ == STATE_RUNNING; }
|
29
28
|
bool is_stopped() const { return this->state_ == STATE_STOPPED; }
|
30
29
|
|
30
|
+
void set_mute_state(bool is_muted) { this->mute_state_ = is_muted; }
|
31
|
+
bool get_mute_state() { return this->mute_state_; }
|
32
|
+
|
33
|
+
audio::AudioStreamInfo get_audio_stream_info() { return this->audio_stream_info_; }
|
34
|
+
|
31
35
|
protected:
|
36
|
+
std::vector<uint8_t> silence_audio_(std::vector<uint8_t> data);
|
37
|
+
|
32
38
|
State state_{STATE_STOPPED};
|
39
|
+
bool mute_state_{false};
|
40
|
+
|
41
|
+
audio::AudioStreamInfo audio_stream_info_;
|
33
42
|
|
34
|
-
CallbackManager<void(const std::vector<
|
43
|
+
CallbackManager<void(const std::vector<uint8_t> &)> data_callbacks_{};
|
35
44
|
};
|
36
45
|
|
37
46
|
} // namespace microphone
|
@@ -0,0 +1,95 @@
|
|
1
|
+
#include "microphone_source.h"
|
2
|
+
|
3
|
+
namespace esphome {
|
4
|
+
namespace microphone {
|
5
|
+
|
6
|
+
static const int32_t Q25_MAX_VALUE = (1 << 25) - 1;
|
7
|
+
static const int32_t Q25_MIN_VALUE = ~Q25_MAX_VALUE;
|
8
|
+
|
9
|
+
void MicrophoneSource::add_data_callback(std::function<void(const std::vector<uint8_t> &)> &&data_callback) {
|
10
|
+
std::function<void(const std::vector<uint8_t> &)> filtered_callback =
|
11
|
+
[this, data_callback](const std::vector<uint8_t> &data) {
|
12
|
+
if (this->enabled_ || this->passive_) {
|
13
|
+
if (this->processed_samples_.use_count() == 0) {
|
14
|
+
// Create vector if its unused
|
15
|
+
this->processed_samples_ = std::make_shared<std::vector<uint8_t>>();
|
16
|
+
}
|
17
|
+
|
18
|
+
// Take temporary ownership of samples vector to avoid deallaction before the callback finishes
|
19
|
+
std::shared_ptr<std::vector<uint8_t>> output_samples = this->processed_samples_;
|
20
|
+
this->process_audio_(data, *output_samples);
|
21
|
+
data_callback(*output_samples);
|
22
|
+
}
|
23
|
+
};
|
24
|
+
this->mic_->add_data_callback(std::move(filtered_callback));
|
25
|
+
}
|
26
|
+
|
27
|
+
audio::AudioStreamInfo MicrophoneSource::get_audio_stream_info() {
|
28
|
+
return audio::AudioStreamInfo(this->bits_per_sample_, this->channels_.count(),
|
29
|
+
this->mic_->get_audio_stream_info().get_sample_rate());
|
30
|
+
}
|
31
|
+
|
32
|
+
void MicrophoneSource::start() {
|
33
|
+
if (!this->enabled_ && !this->passive_) {
|
34
|
+
this->enabled_ = true;
|
35
|
+
this->mic_->start();
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
void MicrophoneSource::stop() {
|
40
|
+
if (this->enabled_ && !this->passive_) {
|
41
|
+
this->enabled_ = false;
|
42
|
+
this->mic_->stop();
|
43
|
+
this->processed_samples_.reset();
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
void MicrophoneSource::process_audio_(const std::vector<uint8_t> &data, std::vector<uint8_t> &filtered_data) {
|
48
|
+
// - Bit depth conversions are obtained by truncating bits or padding with zeros - no dithering is applied.
|
49
|
+
// - In the comments, Qxx refers to a fixed point number with xx bits of precision for representing fractional values.
|
50
|
+
// For example, audio with a bit depth of 16 can store a sample in a int16, which can be considered a Q15 number.
|
51
|
+
// - All samples are converted to Q25 before applying the gain factor - this results in a small precision loss for
|
52
|
+
// data with 32 bits per sample. Since the maximum gain factor is 64 = (1<<6), this ensures that applying the gain
|
53
|
+
// will never overflow a 32 bit signed integer. This still retains more bit depth than what is audibly noticeable.
|
54
|
+
// - Loops for reading/writing data buffers are unrolled, assuming little endian, for a small performance increase.
|
55
|
+
|
56
|
+
const size_t source_bytes_per_sample = this->mic_->get_audio_stream_info().samples_to_bytes(1);
|
57
|
+
const uint32_t source_channels = this->mic_->get_audio_stream_info().get_channels();
|
58
|
+
|
59
|
+
const size_t source_bytes_per_frame = this->mic_->get_audio_stream_info().frames_to_bytes(1);
|
60
|
+
|
61
|
+
const uint32_t total_frames = this->mic_->get_audio_stream_info().bytes_to_frames(data.size());
|
62
|
+
const size_t target_bytes_per_sample = (this->bits_per_sample_ + 7) / 8;
|
63
|
+
const size_t target_bytes_per_frame = target_bytes_per_sample * this->channels_.count();
|
64
|
+
|
65
|
+
filtered_data.resize(target_bytes_per_frame * total_frames);
|
66
|
+
|
67
|
+
uint8_t *current_data = filtered_data.data();
|
68
|
+
|
69
|
+
for (uint32_t frame_index = 0; frame_index < total_frames; ++frame_index) {
|
70
|
+
for (uint32_t channel_index = 0; channel_index < source_channels; ++channel_index) {
|
71
|
+
if (this->channels_.test(channel_index)) {
|
72
|
+
// Channel's current sample is included in the target mask. Convert bits per sample, if necessary.
|
73
|
+
|
74
|
+
const uint32_t sample_index = frame_index * source_bytes_per_frame + channel_index * source_bytes_per_sample;
|
75
|
+
|
76
|
+
int32_t sample = audio::unpack_audio_sample_to_q31(&data[sample_index], source_bytes_per_sample); // Q31
|
77
|
+
sample >>= 6; // Q31 -> Q25
|
78
|
+
|
79
|
+
// Apply gain using multiplication
|
80
|
+
sample *= this->gain_factor_; // Q25
|
81
|
+
|
82
|
+
// Clamp ``sample`` in case gain multiplication overflows 25 bits
|
83
|
+
sample = clamp<int32_t>(sample, Q25_MIN_VALUE, Q25_MAX_VALUE); // Q25
|
84
|
+
|
85
|
+
sample *= (1 << 6); // Q25 -> Q31
|
86
|
+
|
87
|
+
audio::pack_q31_as_audio_sample(sample, current_data, target_bytes_per_sample);
|
88
|
+
current_data = current_data + target_bytes_per_sample;
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
} // namespace microphone
|
95
|
+
} // namespace esphome
|
@@ -0,0 +1,80 @@
|
|
1
|
+
#pragma once
|
2
|
+
|
3
|
+
#include "microphone.h"
|
4
|
+
|
5
|
+
#include "esphome/components/audio/audio.h"
|
6
|
+
|
7
|
+
#include <bitset>
|
8
|
+
#include <cstddef>
|
9
|
+
#include <cstdint>
|
10
|
+
#include <functional>
|
11
|
+
#include <vector>
|
12
|
+
|
13
|
+
namespace esphome {
|
14
|
+
namespace microphone {
|
15
|
+
|
16
|
+
static const int32_t MAX_GAIN_FACTOR = 64;
|
17
|
+
|
18
|
+
class MicrophoneSource {
|
19
|
+
/*
|
20
|
+
* @brief Helper class that handles converting raw microphone data to a requested format.
|
21
|
+
* Components requesting microphone audio should register a callback through this class instead of registering a
|
22
|
+
* callback directly with the microphone if a particular format is required.
|
23
|
+
*
|
24
|
+
* Raw microphone data may have a different number of bits per sample and number of channels than the requesting
|
25
|
+
* component needs. This class handles the conversion by:
|
26
|
+
* - Internally adds a callback to receive the raw microphone data
|
27
|
+
* - The ``process_audio_`` handles the raw data
|
28
|
+
* - Only the channels set in the ``channels_`` bitset are passed through
|
29
|
+
* - Passed through samples have the bits per sample converted
|
30
|
+
* - A gain factor is optionally applied to increase the volume - audio may clip!
|
31
|
+
* - The processed audio is passed to the callback of the component requesting microphone data
|
32
|
+
* - It tracks an internal enabled state, so it ignores raw microphone data when the component requesting
|
33
|
+
* microphone data is not actively requesting audio.
|
34
|
+
*
|
35
|
+
* Note that this class cannot convert sample rates!
|
36
|
+
*/
|
37
|
+
public:
|
38
|
+
MicrophoneSource(Microphone *mic, uint8_t bits_per_sample, int32_t gain_factor, bool passive)
|
39
|
+
: mic_(mic), bits_per_sample_(bits_per_sample), gain_factor_(gain_factor), passive_(passive) {}
|
40
|
+
|
41
|
+
/// @brief Enables a channel to be processed through the callback.
|
42
|
+
///
|
43
|
+
/// If the microphone component only has reads from one channel, it is always in channel number 0, regardless if it
|
44
|
+
/// represents left or right. If the microphone reads from both left and right, channel number 0 and 1 represent the
|
45
|
+
/// left and right channels respectively.
|
46
|
+
///
|
47
|
+
/// @param channel 0-indexed channel number to enable
|
48
|
+
void add_channel(uint8_t channel) { this->channels_.set(channel); }
|
49
|
+
|
50
|
+
void add_data_callback(std::function<void(const std::vector<uint8_t> &)> &&data_callback);
|
51
|
+
|
52
|
+
void set_gain_factor(int32_t gain_factor) { this->gain_factor_ = clamp<int32_t>(gain_factor, 1, MAX_GAIN_FACTOR); }
|
53
|
+
int32_t get_gain_factor() { return this->gain_factor_; }
|
54
|
+
|
55
|
+
/// @brief Gets the AudioStreamInfo of the data after processing
|
56
|
+
/// @return audio::AudioStreamInfo with the configured bits per sample, configured channel count, and source
|
57
|
+
/// microphone's sample rate
|
58
|
+
audio::AudioStreamInfo get_audio_stream_info();
|
59
|
+
|
60
|
+
void start();
|
61
|
+
void stop();
|
62
|
+
bool is_passive() const { return this->passive_; }
|
63
|
+
bool is_running() const { return (this->mic_->is_running() && (this->enabled_ || this->passive_)); }
|
64
|
+
bool is_stopped() const { return !this->is_running(); };
|
65
|
+
|
66
|
+
protected:
|
67
|
+
void process_audio_(const std::vector<uint8_t> &data, std::vector<uint8_t> &filtered_data);
|
68
|
+
|
69
|
+
std::shared_ptr<std::vector<uint8_t>> processed_samples_;
|
70
|
+
|
71
|
+
Microphone *mic_;
|
72
|
+
uint8_t bits_per_sample_;
|
73
|
+
std::bitset<8> channels_;
|
74
|
+
int32_t gain_factor_;
|
75
|
+
bool enabled_{false};
|
76
|
+
bool passive_; // Only pass audio if ``mic_`` is already running
|
77
|
+
};
|
78
|
+
|
79
|
+
} // namespace microphone
|
80
|
+
} // namespace esphome
|