esphome 2024.12.4__py3-none-any.whl → 2025.2.0__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/__main__.py +16 -3
- esphome/components/adc/__init__.py +17 -11
- esphome/components/adc/adc_sensor.h +17 -0
- esphome/components/adc/adc_sensor_common.cpp +55 -0
- esphome/components/adc/adc_sensor_esp32.cpp +8 -5
- esphome/components/adc/adc_sensor_esp8266.cpp +10 -6
- esphome/components/adc/adc_sensor_libretiny.cpp +11 -6
- esphome/components/adc/adc_sensor_rp2040.cpp +13 -10
- esphome/components/adc/sensor.py +9 -3
- esphome/components/ads1115/ads1115.cpp +56 -7
- esphome/components/ads1115/ads1115.h +13 -1
- esphome/components/ads1115/sensor/__init__.py +16 -0
- esphome/components/ads1115/sensor/ads1115_sensor.cpp +2 -1
- esphome/components/ads1115/sensor/ads1115_sensor.h +2 -0
- esphome/components/animation/__init__.py +23 -261
- esphome/components/animation/animation.cpp +2 -2
- esphome/components/animation/animation.h +2 -1
- esphome/components/api/api_pb2.cpp +14 -0
- esphome/components/api/api_pb2.h +1 -0
- esphome/components/api/client.py +8 -3
- esphome/components/audio/__init__.py +112 -0
- esphome/components/audio/audio.cpp +67 -0
- esphome/components/audio/audio.h +125 -7
- esphome/components/audio/audio_decoder.cpp +361 -0
- esphome/components/audio/audio_decoder.h +135 -0
- esphome/components/audio/audio_reader.cpp +308 -0
- esphome/components/audio/audio_reader.h +85 -0
- esphome/components/audio/audio_resampler.cpp +159 -0
- esphome/components/audio/audio_resampler.h +101 -0
- esphome/components/audio/audio_transfer_buffer.cpp +165 -0
- esphome/components/audio/audio_transfer_buffer.h +139 -0
- esphome/components/audio_adc/__init__.py +41 -0
- esphome/components/audio_adc/audio_adc.h +17 -0
- esphome/components/audio_adc/automation.h +23 -0
- esphome/components/bk72xx/__init__.py +1 -0
- esphome/components/ble_client/ble_client.cpp +1 -2
- esphome/components/ble_client/sensor/__init__.py +1 -1
- esphome/components/ble_client/text_sensor/__init__.py +1 -1
- esphome/components/bluetooth_proxy/bluetooth_connection.cpp +5 -0
- esphome/components/bluetooth_proxy/bluetooth_connection.h +1 -0
- esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +5 -0
- esphome/components/ch422g/ch422g.h +2 -0
- esphome/components/climate/__init__.py +1 -1
- esphome/components/climate_ir/climate_ir.cpp +2 -1
- esphome/components/coolix/coolix.cpp +2 -1
- esphome/components/cse7766/cse7766.cpp +8 -16
- esphome/components/custom/__init__.py +0 -3
- esphome/components/custom/binary_sensor/__init__.py +2 -28
- esphome/components/custom/climate/__init__.py +2 -27
- esphome/components/custom/cover/__init__.py +2 -27
- esphome/components/custom/light/__init__.py +2 -27
- esphome/components/custom/output/__init__.py +2 -58
- esphome/components/custom/sensor/__init__.py +2 -24
- esphome/components/custom/switch/__init__.py +2 -24
- esphome/components/custom/text_sensor/__init__.py +2 -29
- esphome/components/custom_component/__init__.py +3 -27
- esphome/components/daly_bms/daly_bms.cpp +6 -0
- esphome/components/daly_bms/daly_bms.h +2 -0
- esphome/components/daly_bms/sensor.py +6 -0
- esphome/components/debug/debug_component.cpp +4 -0
- esphome/components/debug/debug_component.h +14 -0
- esphome/components/debug/debug_esp32.cpp +154 -74
- esphome/components/dfplayer/dfplayer.cpp +15 -2
- esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp +2 -1
- esphome/components/dht/dht.cpp +4 -2
- esphome/components/dht/sensor.py +1 -1
- esphome/components/display/__init__.py +18 -5
- esphome/components/display/display.cpp +16 -3
- esphome/components/display/rect.cpp +2 -1
- esphome/components/es7210/__init__.py +0 -0
- esphome/components/es7210/audio_adc.py +51 -0
- esphome/components/es7210/es7210.cpp +228 -0
- esphome/components/es7210/es7210.h +62 -0
- esphome/components/es7210/es7210_const.h +129 -0
- esphome/components/es7243e/__init__.py +0 -0
- esphome/components/es7243e/audio_adc.py +34 -0
- esphome/components/es7243e/es7243e.cpp +125 -0
- esphome/components/es7243e/es7243e.h +37 -0
- esphome/components/es7243e/es7243e_const.h +54 -0
- esphome/components/es8156/__init__.py +0 -0
- esphome/components/es8156/audio_dac.py +27 -0
- esphome/components/es8156/es8156.cpp +87 -0
- esphome/components/es8156/es8156.h +51 -0
- esphome/components/es8156/es8156_const.h +68 -0
- esphome/components/es8311/audio_dac.py +1 -2
- esphome/components/esp32/__init__.py +1 -0
- esphome/components/esp32/core.cpp +5 -1
- esphome/components/esp32/gpio.h +2 -0
- esphome/components/esp32_ble/__init__.py +39 -0
- esphome/components/esp32_ble/queue.h +4 -4
- esphome/components/esp32_ble_client/ble_client_base.cpp +46 -0
- esphome/components/esp32_ble_client/ble_client_base.h +2 -0
- esphome/components/esp32_ble_server/__init__.py +582 -12
- esphome/components/esp32_ble_server/ble_characteristic.cpp +48 -60
- esphome/components/esp32_ble_server/ble_characteristic.h +24 -17
- esphome/components/esp32_ble_server/ble_descriptor.cpp +21 -9
- esphome/components/esp32_ble_server/ble_descriptor.h +17 -6
- esphome/components/esp32_ble_server/ble_server.cpp +62 -67
- esphome/components/esp32_ble_server/ble_server.h +28 -32
- esphome/components/esp32_ble_server/ble_server_automations.cpp +77 -0
- esphome/components/esp32_ble_server/ble_server_automations.h +115 -0
- esphome/components/esp32_ble_server/ble_service.cpp +17 -15
- esphome/components/esp32_ble_server/ble_service.h +10 -14
- esphome/components/esp32_ble_tracker/__init__.py +6 -39
- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +33 -10
- esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +8 -4
- esphome/components/esp32_dac/esp32_dac.cpp +16 -7
- esphome/components/esp32_dac/esp32_dac.h +8 -0
- esphome/components/esp32_dac/output.py +16 -4
- esphome/components/esp32_improv/__init__.py +2 -8
- esphome/components/esp32_improv/esp32_improv_component.cpp +21 -20
- esphome/components/esp32_improv/esp32_improv_component.h +3 -4
- esphome/components/esp32_rmt/__init__.py +28 -3
- esphome/components/esp32_rmt_led_strip/led_strip.cpp +73 -6
- esphome/components/esp32_rmt_led_strip/led_strip.h +21 -3
- esphome/components/esp32_rmt_led_strip/light.py +72 -7
- esphome/components/esp32_touch/esp32_touch.cpp +5 -0
- esphome/components/esp8266/__init__.py +1 -0
- esphome/components/esp8266/gpio.h +1 -0
- esphome/components/ethernet/__init__.py +10 -10
- esphome/components/event/event.cpp +4 -2
- esphome/components/event/event.h +2 -0
- esphome/components/event_emitter/__init__.py +5 -0
- esphome/components/event_emitter/event_emitter.cpp +14 -0
- esphome/components/event_emitter/event_emitter.h +63 -0
- esphome/components/font/__init__.py +1 -1
- esphome/components/gcja5/gcja5.cpp +2 -1
- esphome/components/graph/graph.cpp +4 -9
- esphome/components/haier/haier_base.cpp +2 -1
- esphome/components/haier/hon_climate.cpp +2 -1
- esphome/components/heatpumpir/heatpumpir.cpp +2 -1
- esphome/components/host/__init__.py +1 -0
- esphome/components/host/gpio.h +1 -0
- esphome/components/http_request/http_request.h +2 -2
- esphome/components/http_request/http_request_arduino.cpp +1 -1
- esphome/components/http_request/http_request_idf.cpp +1 -1
- esphome/components/i2c/i2c_bus_esp_idf.cpp +4 -0
- esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +7 -5
- esphome/components/i2s_audio/speaker/__init__.py +53 -6
- esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +92 -46
- esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +8 -0
- esphome/components/ili9xxx/display.py +29 -11
- esphome/components/ili9xxx/ili9xxx_display.cpp +2 -5
- esphome/components/ili9xxx/ili9xxx_display.h +2 -1
- esphome/components/image/__init__.py +443 -255
- esphome/components/image/image.cpp +115 -61
- esphome/components/image/image.h +15 -24
- esphome/components/json/json_util.cpp +8 -34
- esphome/components/libretiny/__init__.py +1 -0
- esphome/components/libretiny/gpio_arduino.h +1 -0
- esphome/components/light/light_color_values.h +1 -1
- esphome/components/logger/__init__.py +45 -9
- esphome/components/logger/logger.cpp +16 -14
- esphome/components/logger/logger.h +11 -7
- esphome/components/logger/select/__init__.py +29 -0
- esphome/components/logger/select/logger_level_select.cpp +27 -0
- esphome/components/logger/select/logger_level_select.h +15 -0
- esphome/components/lvgl/__init__.py +96 -73
- esphome/components/lvgl/automation.py +39 -7
- esphome/components/lvgl/defines.py +8 -2
- esphome/components/lvgl/lvgl_esphome.cpp +8 -15
- esphome/components/lvgl/lvgl_esphome.h +20 -5
- esphome/components/lvgl/schemas.py +25 -14
- esphome/components/lvgl/trigger.py +27 -3
- esphome/components/lvgl/widgets/dropdown.py +1 -1
- esphome/components/lvgl/widgets/keyboard.py +8 -1
- esphome/components/lvgl/widgets/meter.py +2 -1
- esphome/components/lvgl/widgets/msgbox.py +1 -1
- esphome/components/lvgl/widgets/obj.py +1 -12
- esphome/components/lvgl/widgets/page.py +37 -2
- esphome/components/lvgl/widgets/tabview.py +1 -1
- esphome/components/max6956/max6956.h +2 -0
- esphome/components/mcp23016/mcp23016.h +2 -0
- esphome/components/mcp23xxx_base/mcp23xxx_base.h +2 -0
- esphome/components/mdns/__init__.py +1 -1
- esphome/components/media_player/__init__.py +37 -8
- esphome/components/media_player/automation.h +11 -2
- esphome/components/media_player/media_player.cpp +8 -0
- esphome/components/media_player/media_player.h +8 -4
- esphome/components/micronova/switch/micronova_switch.cpp +4 -2
- esphome/components/midea/ac_automations.h +3 -1
- esphome/components/midea/air_conditioner.cpp +7 -5
- esphome/components/midea/air_conditioner.h +1 -1
- esphome/components/midea/climate.py +4 -2
- esphome/components/midea/ir_transmitter.h +36 -5
- esphome/components/mixer/__init__.py +0 -0
- esphome/components/mixer/speaker/__init__.py +172 -0
- esphome/components/mixer/speaker/automation.h +19 -0
- esphome/components/mixer/speaker/mixer_speaker.cpp +624 -0
- esphome/components/mixer/speaker/mixer_speaker.h +207 -0
- esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp +7 -13
- esphome/components/mpr121/mpr121.h +2 -0
- esphome/components/mqtt/__init__.py +1 -1
- esphome/components/mqtt/mqtt_client.cpp +7 -1
- esphome/components/mqtt/mqtt_client.h +1 -1
- esphome/components/mqtt/mqtt_climate.cpp +2 -2
- esphome/components/network/ip_address.h +2 -0
- esphome/components/nextion/automation.h +17 -0
- esphome/components/nextion/display.py +42 -17
- esphome/components/nextion/nextion.cpp +4 -10
- esphome/components/nextion/nextion.h +89 -82
- esphome/components/nextion/nextion_commands.cpp +10 -10
- esphome/components/ntc/sensor.py +2 -4
- esphome/components/online_image/__init__.py +98 -46
- esphome/components/online_image/bmp_image.cpp +101 -0
- esphome/components/online_image/bmp_image.h +40 -0
- esphome/components/online_image/image_decoder.cpp +31 -2
- esphome/components/online_image/image_decoder.h +24 -15
- esphome/components/online_image/jpeg_image.cpp +92 -0
- esphome/components/online_image/jpeg_image.h +34 -0
- esphome/components/online_image/online_image.cpp +118 -58
- esphome/components/online_image/online_image.h +39 -9
- esphome/components/online_image/png_image.cpp +7 -3
- esphome/components/online_image/png_image.h +2 -1
- esphome/components/opentherm/__init__.py +73 -7
- esphome/components/opentherm/automation.h +25 -0
- esphome/components/opentherm/const.py +1 -0
- esphome/components/opentherm/generate.py +39 -6
- esphome/components/opentherm/hub.cpp +117 -79
- esphome/components/opentherm/hub.h +31 -15
- esphome/components/opentherm/opentherm.cpp +47 -23
- esphome/components/opentherm/opentherm.h +27 -6
- esphome/components/opentherm/opentherm_macros.h +11 -0
- esphome/components/opentherm/schema.py +78 -1
- esphome/components/opentherm/validate.py +7 -2
- esphome/components/pca6416a/pca6416a.h +2 -0
- esphome/components/pca9554/pca9554.h +2 -0
- esphome/components/pcf8574/pcf8574.h +2 -0
- esphome/components/preferences/__init__.py +2 -4
- esphome/components/preferences/syncer.h +10 -3
- esphome/components/prometheus/prometheus_handler.cpp +313 -0
- esphome/components/prometheus/prometheus_handler.h +48 -7
- esphome/components/psram/psram.cpp +8 -1
- esphome/components/pulse_counter/pulse_counter_sensor.cpp +14 -9
- esphome/components/pulse_counter/pulse_counter_sensor.h +4 -4
- esphome/components/pulse_meter/pulse_meter_sensor.cpp +2 -0
- esphome/components/qspi_dbi/__init__.py +3 -0
- esphome/components/qspi_dbi/display.py +74 -47
- esphome/components/qspi_dbi/models.py +245 -2
- esphome/components/qspi_dbi/qspi_dbi.cpp +9 -16
- esphome/components/qspi_dbi/qspi_dbi.h +2 -2
- esphome/components/remote_base/__init__.py +77 -25
- esphome/components/remote_base/remote_base.cpp +1 -1
- esphome/components/remote_base/remote_base.h +20 -2
- esphome/components/remote_base/toto_protocol.cpp +100 -0
- esphome/components/remote_base/toto_protocol.h +45 -0
- esphome/components/remote_receiver/__init__.py +55 -10
- esphome/components/remote_receiver/remote_receiver.h +36 -3
- esphome/components/remote_receiver/remote_receiver_esp32.cpp +145 -6
- esphome/components/remote_transmitter/__init__.py +62 -4
- esphome/components/remote_transmitter/remote_transmitter.h +21 -2
- esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +140 -4
- esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +3 -3
- esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp +3 -3
- esphome/components/resampler/__init__.py +0 -0
- esphome/components/resampler/speaker/__init__.py +103 -0
- esphome/components/resampler/speaker/resampler_speaker.cpp +318 -0
- esphome/components/resampler/speaker/resampler_speaker.h +107 -0
- esphome/components/resistance/resistance_sensor.h +2 -3
- esphome/components/resistance/sensor.py +2 -9
- esphome/components/rotary_encoder/rotary_encoder.cpp +8 -4
- esphome/components/rp2040/__init__.py +1 -0
- esphome/components/rp2040/gpio.h +1 -0
- esphome/components/rtl87xx/__init__.py +2 -0
- esphome/components/scd30/sensor.py +1 -1
- esphome/components/sdl/binary_sensor.py +270 -0
- esphome/components/sdl/sdl_esphome.cpp +16 -0
- esphome/components/sdl/sdl_esphome.h +9 -0
- esphome/components/seeed_mr60bha2/binary_sensor.py +25 -0
- esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +26 -2
- esphome/components/seeed_mr60bha2/seeed_mr60bha2.h +9 -20
- esphome/components/seeed_mr60bha2/sensor.py +9 -1
- esphome/components/sn74hc165/sn74hc165.h +3 -0
- esphome/components/sn74hc595/sn74hc595.h +3 -0
- esphome/components/speaker/__init__.py +5 -4
- esphome/components/speaker/media_player/__init__.py +458 -0
- esphome/components/speaker/media_player/audio_pipeline.cpp +568 -0
- esphome/components/speaker/media_player/audio_pipeline.h +159 -0
- esphome/components/speaker/media_player/automation.h +26 -0
- esphome/components/speaker/media_player/speaker_media_player.cpp +577 -0
- esphome/components/speaker/media_player/speaker_media_player.h +160 -0
- esphome/components/speaker/speaker.h +20 -0
- esphome/components/spi/__init__.py +1 -5
- esphome/components/spi/spi.cpp +7 -1
- esphome/components/spi/spi.h +21 -2
- esphome/components/spi_led_strip/light.py +3 -5
- esphome/components/spi_led_strip/spi_led_strip.cpp +67 -0
- esphome/components/spi_led_strip/spi_led_strip.h +8 -60
- esphome/components/sprinkler/sprinkler.cpp +3 -1
- esphome/components/sx1509/sx1509_gpio_pin.h +2 -0
- esphome/components/tca9555/tca9555.h +2 -0
- esphome/components/toshiba/toshiba.cpp +2 -1
- esphome/components/tuya/light/tuya_light.cpp +4 -2
- esphome/components/uart/uart_component_esp32_arduino.cpp +2 -2
- esphome/components/uart/uart_component_esp_idf.cpp +2 -2
- esphome/components/udp/__init__.py +8 -2
- esphome/components/udp/udp_component.cpp +25 -56
- esphome/components/udp/udp_component.h +3 -0
- esphome/components/uponor_smatrix/sensor/__init__.py +14 -4
- esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp +5 -0
- esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h +1 -0
- esphome/components/uptime/text_sensor/__init__.py +19 -0
- esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +63 -0
- esphome/components/uptime/text_sensor/uptime_text_sensor.h +25 -0
- esphome/components/voice_assistant/voice_assistant.cpp +24 -14
- esphome/components/voice_assistant/voice_assistant.h +8 -0
- esphome/components/waveshare_epaper/display.py +22 -1
- esphome/components/waveshare_epaper/waveshare_213v3.cpp +9 -3
- esphome/components/waveshare_epaper/waveshare_epaper.cpp +1155 -44
- esphome/components/waveshare_epaper/waveshare_epaper.h +208 -7
- esphome/components/web_server/web_server.cpp +28 -6
- esphome/components/weikai/weikai.h +2 -0
- esphome/components/wifi/__init__.py +6 -6
- esphome/components/wifi/wifi_component.cpp +1 -1
- esphome/components/wifi/wifi_component_esp32_arduino.cpp +30 -1
- esphome/components/wireguard/__init__.py +2 -2
- esphome/components/xl9535/xl9535.h +2 -0
- esphome/components/xxtea/__init__.py +3 -0
- esphome/components/xxtea/xxtea.cpp +46 -0
- esphome/components/xxtea/xxtea.h +26 -0
- esphome/components/yashima/yashima.cpp +2 -1
- esphome/config.py +9 -5
- esphome/config_validation.py +55 -17
- esphome/const.py +7 -10
- esphome/core/__init__.py +6 -13
- esphome/core/base_automation.h +1 -0
- esphome/core/config.py +59 -72
- esphome/core/defines.h +9 -1
- esphome/core/gpio.h +7 -0
- esphome/core/helpers.cpp +19 -15
- esphome/core/helpers.h +57 -8
- esphome/core/log.h +9 -7
- esphome/cpp_generator.py +2 -2
- esphome/dashboard/web_server.py +1 -1
- esphome/espota2.py +3 -2
- esphome/loader.py +12 -4
- esphome/log.py +5 -7
- esphome/yaml_util.py +2 -2
- {esphome-2024.12.4.dist-info → esphome-2025.2.0.dist-info}/METADATA +14 -9
- {esphome-2024.12.4.dist-info → esphome-2025.2.0.dist-info}/RECORD +349 -300
- esphome/components/custom/binary_sensor/custom_binary_sensor.cpp +0 -16
- esphome/components/custom/binary_sensor/custom_binary_sensor.h +0 -26
- esphome/components/custom/climate/custom_climate.h +0 -22
- esphome/components/custom/cover/custom_cover.h +0 -21
- esphome/components/custom/light/custom_light_output.h +0 -24
- esphome/components/custom/output/custom_output.h +0 -37
- esphome/components/custom/sensor/custom_sensor.cpp +0 -16
- esphome/components/custom/sensor/custom_sensor.h +0 -24
- esphome/components/custom/switch/custom_switch.cpp +0 -16
- esphome/components/custom/switch/custom_switch.h +0 -24
- esphome/components/custom/text_sensor/custom_text_sensor.cpp +0 -16
- esphome/components/custom/text_sensor/custom_text_sensor.h +0 -26
- esphome/components/custom_component/custom_component.h +0 -28
- esphome/components/esp32_ble_server/ble_2901.cpp +0 -18
- esphome/components/esp32_ble_server/ble_2901.h +0 -19
- esphome/components/resistance_sampler/__init__.py +0 -6
- esphome/components/resistance_sampler/resistance_sampler.h +0 -10
- esphome/components/uptime/{sensor.py → sensor/__init__.py} +3 -3
- /esphome/components/uptime/{uptime_seconds_sensor.cpp → sensor/uptime_seconds_sensor.cpp} +0 -0
- /esphome/components/uptime/{uptime_seconds_sensor.h → sensor/uptime_seconds_sensor.h} +0 -0
- /esphome/components/uptime/{uptime_timestamp_sensor.cpp → sensor/uptime_timestamp_sensor.cpp} +0 -0
- /esphome/components/uptime/{uptime_timestamp_sensor.h → sensor/uptime_timestamp_sensor.h} +0 -0
- {esphome-2024.12.4.dist-info → esphome-2025.2.0.dist-info}/LICENSE +0 -0
- {esphome-2024.12.4.dist-info → esphome-2025.2.0.dist-info}/WHEEL +0 -0
- {esphome-2024.12.4.dist-info → esphome-2025.2.0.dist-info}/entry_points.txt +0 -0
- {esphome-2024.12.4.dist-info → esphome-2025.2.0.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,7 @@ import logging
|
|
6
6
|
from pathlib import Path
|
7
7
|
import re
|
8
8
|
|
9
|
-
import
|
9
|
+
from PIL import Image, UnidentifiedImageError
|
10
10
|
|
11
11
|
from esphome import core, external_files
|
12
12
|
import esphome.codegen as cg
|
@@ -29,22 +29,258 @@ _LOGGER = logging.getLogger(__name__)
|
|
29
29
|
|
30
30
|
DOMAIN = "image"
|
31
31
|
DEPENDENCIES = ["display"]
|
32
|
-
MULTI_CONF = True
|
33
|
-
MULTI_CONF_NO_DEFAULT = True
|
34
32
|
|
35
33
|
image_ns = cg.esphome_ns.namespace("image")
|
36
34
|
|
37
35
|
ImageType = image_ns.enum("ImageType")
|
36
|
+
|
37
|
+
CONF_OPAQUE = "opaque"
|
38
|
+
CONF_CHROMA_KEY = "chroma_key"
|
39
|
+
CONF_ALPHA_CHANNEL = "alpha_channel"
|
40
|
+
CONF_INVERT_ALPHA = "invert_alpha"
|
41
|
+
|
42
|
+
TRANSPARENCY_TYPES = (
|
43
|
+
CONF_OPAQUE,
|
44
|
+
CONF_CHROMA_KEY,
|
45
|
+
CONF_ALPHA_CHANNEL,
|
46
|
+
)
|
47
|
+
|
48
|
+
|
49
|
+
def get_image_type_enum(type):
|
50
|
+
return getattr(ImageType, f"IMAGE_TYPE_{type.upper()}")
|
51
|
+
|
52
|
+
|
53
|
+
def get_transparency_enum(transparency):
|
54
|
+
return getattr(TransparencyType, f"TRANSPARENCY_{transparency.upper()}")
|
55
|
+
|
56
|
+
|
57
|
+
class ImageEncoder:
|
58
|
+
"""
|
59
|
+
Superclass of image type encoders
|
60
|
+
"""
|
61
|
+
|
62
|
+
# Control which transparency options are available for a given type
|
63
|
+
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE}
|
64
|
+
|
65
|
+
# All imageencoder types are valid
|
66
|
+
@staticmethod
|
67
|
+
def validate(value):
|
68
|
+
return value
|
69
|
+
|
70
|
+
def __init__(self, width, height, transparency, dither, invert_alpha):
|
71
|
+
"""
|
72
|
+
:param width: The image width in pixels
|
73
|
+
:param height: The image height in pixels
|
74
|
+
:param transparency: Transparency type
|
75
|
+
:param dither: Dither method
|
76
|
+
:param invert_alpha: True if the alpha channel should be inverted; for monochrome formats inverts the colours.
|
77
|
+
"""
|
78
|
+
self.transparency = transparency
|
79
|
+
self.width = width
|
80
|
+
self.height = height
|
81
|
+
self.data = [0 for _ in range(width * height)]
|
82
|
+
self.dither = dither
|
83
|
+
self.index = 0
|
84
|
+
self.invert_alpha = invert_alpha
|
85
|
+
self.path = ""
|
86
|
+
|
87
|
+
def convert(self, image, path):
|
88
|
+
"""
|
89
|
+
Convert the image format
|
90
|
+
:param image: Input image
|
91
|
+
:param path: Path to the image file
|
92
|
+
:return: converted image
|
93
|
+
"""
|
94
|
+
return image
|
95
|
+
|
96
|
+
def encode(self, pixel):
|
97
|
+
"""
|
98
|
+
Encode a single pixel
|
99
|
+
"""
|
100
|
+
|
101
|
+
def end_row(self):
|
102
|
+
"""
|
103
|
+
Marks the end of a pixel row
|
104
|
+
:return:
|
105
|
+
"""
|
106
|
+
|
107
|
+
|
108
|
+
def is_alpha_only(image: Image):
|
109
|
+
"""
|
110
|
+
Check if an image (assumed to be RGBA) is only alpha
|
111
|
+
"""
|
112
|
+
# Any alpha data?
|
113
|
+
if image.split()[-1].getextrema()[0] == 0xFF:
|
114
|
+
return False
|
115
|
+
return all(b.getextrema()[1] == 0 for b in image.split()[:-1])
|
116
|
+
|
117
|
+
|
118
|
+
class ImageBinary(ImageEncoder):
|
119
|
+
allow_config = {CONF_OPAQUE, CONF_INVERT_ALPHA, CONF_CHROMA_KEY}
|
120
|
+
|
121
|
+
def __init__(self, width, height, transparency, dither, invert_alpha):
|
122
|
+
self.width8 = (width + 7) // 8
|
123
|
+
super().__init__(self.width8, height, transparency, dither, invert_alpha)
|
124
|
+
self.bitno = 0
|
125
|
+
|
126
|
+
def convert(self, image, path):
|
127
|
+
if is_alpha_only(image):
|
128
|
+
image = image.split()[-1]
|
129
|
+
return image.convert("1", dither=self.dither)
|
130
|
+
|
131
|
+
def encode(self, pixel):
|
132
|
+
if self.invert_alpha:
|
133
|
+
pixel = not pixel
|
134
|
+
if pixel:
|
135
|
+
self.data[self.index] |= 0x80 >> (self.bitno % 8)
|
136
|
+
self.bitno += 1
|
137
|
+
if self.bitno == 8:
|
138
|
+
self.bitno = 0
|
139
|
+
self.index += 1
|
140
|
+
|
141
|
+
def end_row(self):
|
142
|
+
"""
|
143
|
+
Pad rows to a byte boundary
|
144
|
+
"""
|
145
|
+
if self.bitno != 0:
|
146
|
+
self.bitno = 0
|
147
|
+
self.index += 1
|
148
|
+
|
149
|
+
|
150
|
+
class ImageGrayscale(ImageEncoder):
|
151
|
+
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_INVERT_ALPHA, CONF_OPAQUE}
|
152
|
+
|
153
|
+
def convert(self, image, path):
|
154
|
+
if is_alpha_only(image):
|
155
|
+
if self.transparency != CONF_ALPHA_CHANNEL:
|
156
|
+
_LOGGER.warning(
|
157
|
+
"Grayscale image %s is alpha only, but transparency is set to %s",
|
158
|
+
path,
|
159
|
+
self.transparency,
|
160
|
+
)
|
161
|
+
self.transparency = CONF_ALPHA_CHANNEL
|
162
|
+
image = image.split()[-1]
|
163
|
+
return image.convert("LA")
|
164
|
+
|
165
|
+
def encode(self, pixel):
|
166
|
+
b, a = pixel
|
167
|
+
if self.transparency == CONF_CHROMA_KEY:
|
168
|
+
if b == 1:
|
169
|
+
b = 0
|
170
|
+
if a != 0xFF:
|
171
|
+
b = 1
|
172
|
+
if self.invert_alpha:
|
173
|
+
b ^= 0xFF
|
174
|
+
if self.transparency == CONF_ALPHA_CHANNEL:
|
175
|
+
if a != 0xFF:
|
176
|
+
b = a
|
177
|
+
self.data[self.index] = b
|
178
|
+
self.index += 1
|
179
|
+
|
180
|
+
|
181
|
+
class ImageRGB565(ImageEncoder):
|
182
|
+
def __init__(self, width, height, transparency, dither, invert_alpha):
|
183
|
+
stride = 3 if transparency == CONF_ALPHA_CHANNEL else 2
|
184
|
+
super().__init__(
|
185
|
+
width * stride,
|
186
|
+
height,
|
187
|
+
transparency,
|
188
|
+
dither,
|
189
|
+
invert_alpha,
|
190
|
+
)
|
191
|
+
|
192
|
+
def convert(self, image, path):
|
193
|
+
return image.convert("RGBA")
|
194
|
+
|
195
|
+
def encode(self, pixel):
|
196
|
+
r, g, b, a = pixel
|
197
|
+
r = r >> 3
|
198
|
+
g = g >> 2
|
199
|
+
b = b >> 3
|
200
|
+
if self.transparency == CONF_CHROMA_KEY:
|
201
|
+
if r == 0 and g == 1 and b == 0:
|
202
|
+
g = 0
|
203
|
+
elif a < 128:
|
204
|
+
r = 0
|
205
|
+
g = 1
|
206
|
+
b = 0
|
207
|
+
rgb = (r << 11) | (g << 5) | b
|
208
|
+
self.data[self.index] = rgb >> 8
|
209
|
+
self.index += 1
|
210
|
+
self.data[self.index] = rgb & 0xFF
|
211
|
+
self.index += 1
|
212
|
+
if self.transparency == CONF_ALPHA_CHANNEL:
|
213
|
+
if self.invert_alpha:
|
214
|
+
a ^= 0xFF
|
215
|
+
self.data[self.index] = a
|
216
|
+
self.index += 1
|
217
|
+
|
218
|
+
|
219
|
+
class ImageRGB(ImageEncoder):
|
220
|
+
def __init__(self, width, height, transparency, dither, invert_alpha):
|
221
|
+
stride = 4 if transparency == CONF_ALPHA_CHANNEL else 3
|
222
|
+
super().__init__(
|
223
|
+
width * stride,
|
224
|
+
height,
|
225
|
+
transparency,
|
226
|
+
dither,
|
227
|
+
invert_alpha,
|
228
|
+
)
|
229
|
+
|
230
|
+
def convert(self, image, path):
|
231
|
+
return image.convert("RGBA")
|
232
|
+
|
233
|
+
def encode(self, pixel):
|
234
|
+
r, g, b, a = pixel
|
235
|
+
if self.transparency == CONF_CHROMA_KEY:
|
236
|
+
if r == 0 and g == 1 and b == 0:
|
237
|
+
g = 0
|
238
|
+
elif a < 128:
|
239
|
+
r = 0
|
240
|
+
g = 1
|
241
|
+
b = 0
|
242
|
+
self.data[self.index] = r
|
243
|
+
self.index += 1
|
244
|
+
self.data[self.index] = g
|
245
|
+
self.index += 1
|
246
|
+
self.data[self.index] = b
|
247
|
+
self.index += 1
|
248
|
+
if self.transparency == CONF_ALPHA_CHANNEL:
|
249
|
+
if self.invert_alpha:
|
250
|
+
a ^= 0xFF
|
251
|
+
self.data[self.index] = a
|
252
|
+
self.index += 1
|
253
|
+
|
254
|
+
|
255
|
+
class ReplaceWith:
|
256
|
+
"""
|
257
|
+
Placeholder class to provide feedback on deprecated features
|
258
|
+
"""
|
259
|
+
|
260
|
+
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE}
|
261
|
+
|
262
|
+
def __init__(self, replace_with):
|
263
|
+
self.replace_with = replace_with
|
264
|
+
|
265
|
+
def validate(self, value):
|
266
|
+
raise cv.Invalid(
|
267
|
+
f"Image type {value} is removed; replace with {self.replace_with}"
|
268
|
+
)
|
269
|
+
|
270
|
+
|
38
271
|
IMAGE_TYPE = {
|
39
|
-
"BINARY":
|
40
|
-
"
|
41
|
-
"
|
42
|
-
"
|
43
|
-
"
|
44
|
-
"
|
272
|
+
"BINARY": ImageBinary,
|
273
|
+
"GRAYSCALE": ImageGrayscale,
|
274
|
+
"RGB565": ImageRGB565,
|
275
|
+
"RGB": ImageRGB,
|
276
|
+
"TRANSPARENT_BINARY": ReplaceWith("'type: BINARY' and 'transparency: chroma_key'"),
|
277
|
+
"RGB24": ReplaceWith("'type: RGB'"),
|
278
|
+
"RGBA": ReplaceWith("'type: RGB' and 'transparency: alpha_channel'"),
|
45
279
|
}
|
46
280
|
|
47
|
-
|
281
|
+
TransparencyType = image_ns.enum("TransparencyType")
|
282
|
+
|
283
|
+
CONF_TRANSPARENCY = "transparency"
|
48
284
|
|
49
285
|
# If the MDI file cannot be downloaded within this time, abort.
|
50
286
|
IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds
|
@@ -53,17 +289,11 @@ SOURCE_LOCAL = "local"
|
|
53
289
|
SOURCE_MDI = "mdi"
|
54
290
|
SOURCE_WEB = "web"
|
55
291
|
|
56
|
-
|
57
292
|
Image_ = image_ns.class_("Image")
|
58
293
|
|
59
294
|
|
60
|
-
def
|
61
|
-
|
62
|
-
return base_dir / f"{value[CONF_ICON]}.svg"
|
63
|
-
|
64
|
-
|
65
|
-
def compute_local_image_path(value: dict) -> Path:
|
66
|
-
url = value[CONF_URL]
|
295
|
+
def compute_local_image_path(value) -> Path:
|
296
|
+
url = value[CONF_URL] if isinstance(value, dict) else value
|
67
297
|
h = hashlib.new("sha256")
|
68
298
|
h.update(url.encode())
|
69
299
|
key = h.hexdigest()[:8]
|
@@ -71,30 +301,38 @@ def compute_local_image_path(value: dict) -> Path:
|
|
71
301
|
return base_dir / key
|
72
302
|
|
73
303
|
|
74
|
-
def
|
75
|
-
|
76
|
-
|
77
|
-
mdi_id = value[CONF_ICON]
|
78
|
-
path = _compute_local_icon_path(value)
|
304
|
+
def local_path(value):
|
305
|
+
value = value[CONF_PATH] if isinstance(value, dict) else value
|
306
|
+
return str(CORE.relative_config_path(value))
|
79
307
|
|
80
|
-
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg"
|
81
308
|
|
309
|
+
def download_file(url, path):
|
82
310
|
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
|
311
|
+
return str(path)
|
83
312
|
|
84
|
-
|
313
|
+
|
314
|
+
def download_mdi(value):
|
315
|
+
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
|
316
|
+
base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi"
|
317
|
+
path = base_dir / f"{mdi_id}.svg"
|
318
|
+
|
319
|
+
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg"
|
320
|
+
return download_file(url, path)
|
85
321
|
|
86
322
|
|
87
323
|
def download_image(value):
|
88
|
-
|
89
|
-
|
324
|
+
value = value[CONF_URL] if isinstance(value, dict) else value
|
325
|
+
return download_file(value, compute_local_image_path(value))
|
90
326
|
|
91
|
-
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
|
92
327
|
|
93
|
-
|
328
|
+
def is_svg_file(file):
|
329
|
+
if not file:
|
330
|
+
return False
|
331
|
+
with open(file, "rb") as f:
|
332
|
+
return "<svg" in str(f.read(1024))
|
94
333
|
|
95
334
|
|
96
|
-
def validate_cairosvg_installed(
|
97
|
-
"""Validate that cairosvg is installed"""
|
335
|
+
def validate_cairosvg_installed():
|
98
336
|
try:
|
99
337
|
import cairosvg
|
100
338
|
except ImportError as err:
|
@@ -110,73 +348,28 @@ def validate_cairosvg_installed(value):
|
|
110
348
|
"(pip install -U cairosvg)"
|
111
349
|
)
|
112
350
|
|
113
|
-
return value
|
114
|
-
|
115
|
-
|
116
|
-
def validate_cross_dependencies(config):
|
117
|
-
"""
|
118
|
-
Validate fields whose possible values depend on other fields.
|
119
|
-
For example, validate that explicitly transparent image types
|
120
|
-
have "use_transparency" set to True.
|
121
|
-
Also set the default value for those kind of dependent fields.
|
122
|
-
"""
|
123
|
-
is_mdi = CONF_FILE in config and config[CONF_FILE][CONF_SOURCE] == SOURCE_MDI
|
124
|
-
if CONF_TYPE not in config:
|
125
|
-
if is_mdi:
|
126
|
-
config[CONF_TYPE] = "TRANSPARENT_BINARY"
|
127
|
-
else:
|
128
|
-
config[CONF_TYPE] = "BINARY"
|
129
|
-
|
130
|
-
image_type = config[CONF_TYPE]
|
131
|
-
is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"]
|
132
|
-
|
133
|
-
# If the use_transparency option was not specified, set the default depending on the image type
|
134
|
-
if CONF_USE_TRANSPARENCY not in config:
|
135
|
-
config[CONF_USE_TRANSPARENCY] = is_transparent_type
|
136
|
-
|
137
|
-
if is_transparent_type and not config[CONF_USE_TRANSPARENCY]:
|
138
|
-
raise cv.Invalid(f"Image type {image_type} must always be transparent.")
|
139
|
-
|
140
|
-
if is_mdi and config[CONF_TYPE] not in ["BINARY", "TRANSPARENT_BINARY"]:
|
141
|
-
raise cv.Invalid("MDI images must be binary images.")
|
142
|
-
|
143
|
-
return config
|
144
|
-
|
145
351
|
|
146
352
|
def validate_file_shorthand(value):
|
147
353
|
value = cv.string_strict(value)
|
148
354
|
if value.startswith("mdi:"):
|
149
|
-
validate_cairosvg_installed(value)
|
150
|
-
|
151
355
|
match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value)
|
152
356
|
if match is None:
|
153
357
|
raise cv.Invalid("Could not parse mdi icon name.")
|
154
358
|
icon = match.group(1)
|
155
|
-
return
|
156
|
-
|
157
|
-
CONF_SOURCE: SOURCE_MDI,
|
158
|
-
CONF_ICON: icon,
|
159
|
-
}
|
160
|
-
)
|
359
|
+
return download_mdi(icon)
|
360
|
+
|
161
361
|
if value.startswith("http://") or value.startswith("https://"):
|
162
|
-
return
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
}
|
167
|
-
)
|
168
|
-
return FILE_SCHEMA(
|
169
|
-
{
|
170
|
-
CONF_SOURCE: SOURCE_LOCAL,
|
171
|
-
CONF_PATH: value,
|
172
|
-
}
|
173
|
-
)
|
362
|
+
return download_image(value)
|
363
|
+
|
364
|
+
value = cv.file_(value)
|
365
|
+
return local_path(value)
|
174
366
|
|
175
367
|
|
176
|
-
LOCAL_SCHEMA = cv.
|
368
|
+
LOCAL_SCHEMA = cv.All(
|
177
369
|
{
|
178
370
|
cv.Required(CONF_PATH): cv.file_,
|
179
|
-
}
|
371
|
+
},
|
372
|
+
local_path,
|
180
373
|
)
|
181
374
|
|
182
375
|
MDI_SCHEMA = cv.All(
|
@@ -203,205 +396,200 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
|
|
203
396
|
)
|
204
397
|
|
205
398
|
|
206
|
-
def
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
FILE_SCHEMA = cv.Schema(_file_schema)
|
213
|
-
|
214
|
-
IMAGE_SCHEMA = cv.Schema(
|
215
|
-
cv.All(
|
216
|
-
{
|
217
|
-
cv.Required(CONF_ID): cv.declare_id(Image_),
|
218
|
-
cv.Required(CONF_FILE): FILE_SCHEMA,
|
219
|
-
cv.Optional(CONF_RESIZE): cv.dimensions,
|
220
|
-
# Not setting default here on purpose; the default depends on the source type
|
221
|
-
# (file or mdi), and will be set in the "validate_cross_dependencies" validator.
|
222
|
-
cv.Optional(CONF_TYPE): cv.enum(IMAGE_TYPE, upper=True),
|
223
|
-
# Not setting default here on purpose; the default depends on the image type,
|
224
|
-
# and thus will be set in the "validate_cross_dependencies" validator.
|
225
|
-
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
|
226
|
-
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
227
|
-
"NONE", "FLOYDSTEINBERG", upper=True
|
228
|
-
),
|
229
|
-
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
230
|
-
},
|
231
|
-
validate_cross_dependencies,
|
232
|
-
)
|
233
|
-
)
|
399
|
+
def validate_transparency(choices=TRANSPARENCY_TYPES):
|
400
|
+
def validate(value):
|
401
|
+
if isinstance(value, bool):
|
402
|
+
value = str(value)
|
403
|
+
return cv.one_of(*choices, lower=True)(value)
|
234
404
|
|
235
|
-
|
405
|
+
return validate
|
236
406
|
|
237
407
|
|
238
|
-
def
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
from PIL import Image
|
408
|
+
def validate_type(image_types):
|
409
|
+
def validate(value):
|
410
|
+
value = cv.one_of(*image_types, upper=True)(value)
|
411
|
+
return IMAGE_TYPE[value].validate(value)
|
412
|
+
|
413
|
+
return validate
|
245
414
|
|
246
|
-
if resize:
|
247
|
-
req_width, req_height = resize
|
248
|
-
svg_image = svg2png(
|
249
|
-
file,
|
250
|
-
output_width=req_width,
|
251
|
-
output_height=req_height,
|
252
|
-
)
|
253
|
-
else:
|
254
|
-
svg_image = svg2png(file)
|
255
415
|
|
256
|
-
|
416
|
+
def validate_settings(value):
|
417
|
+
type = value[CONF_TYPE]
|
418
|
+
transparency = value[CONF_TRANSPARENCY].lower()
|
419
|
+
allow_config = IMAGE_TYPE[type].allow_config
|
420
|
+
if transparency not in allow_config:
|
421
|
+
raise cv.Invalid(
|
422
|
+
f"Image format '{type}' cannot have transparency: {transparency}"
|
423
|
+
)
|
424
|
+
invert_alpha = value.get(CONF_INVERT_ALPHA, False)
|
425
|
+
if (
|
426
|
+
invert_alpha
|
427
|
+
and transparency != CONF_ALPHA_CHANNEL
|
428
|
+
and CONF_INVERT_ALPHA not in allow_config
|
429
|
+
):
|
430
|
+
raise cv.Invalid("No alpha channel to invert")
|
431
|
+
if file := value.get(CONF_FILE):
|
432
|
+
file = Path(file)
|
433
|
+
if is_svg_file(file):
|
434
|
+
validate_cairosvg_installed()
|
435
|
+
else:
|
436
|
+
try:
|
437
|
+
Image.open(file)
|
438
|
+
except UnidentifiedImageError as exc:
|
439
|
+
raise cv.Invalid(f"File can't be opened as image: {file}") from exc
|
440
|
+
return value
|
257
441
|
|
258
442
|
|
259
|
-
|
260
|
-
|
261
|
-
|
443
|
+
BASE_SCHEMA = cv.Schema(
|
444
|
+
{
|
445
|
+
cv.Required(CONF_ID): cv.declare_id(Image_),
|
446
|
+
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
|
447
|
+
cv.Optional(CONF_RESIZE): cv.dimensions,
|
448
|
+
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
449
|
+
"NONE", "FLOYDSTEINBERG", upper=True
|
450
|
+
),
|
451
|
+
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
|
452
|
+
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
453
|
+
}
|
454
|
+
).add_extra(validate_settings)
|
262
455
|
|
263
|
-
|
456
|
+
IMAGE_SCHEMA = BASE_SCHEMA.extend(
|
457
|
+
{
|
458
|
+
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
459
|
+
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
|
460
|
+
}
|
461
|
+
)
|
264
462
|
|
265
|
-
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
|
266
|
-
path = CORE.relative_config_path(conf_file[CONF_PATH])
|
267
463
|
|
268
|
-
|
269
|
-
|
464
|
+
def typed_image_schema(image_type):
|
465
|
+
"""
|
466
|
+
Construct a schema for a specific image type, allowing transparency options
|
467
|
+
"""
|
468
|
+
return cv.Any(
|
469
|
+
cv.Schema(
|
470
|
+
{
|
471
|
+
cv.Optional(t.lower()): cv.ensure_list(
|
472
|
+
BASE_SCHEMA.extend(
|
473
|
+
{
|
474
|
+
cv.Optional(
|
475
|
+
CONF_TRANSPARENCY, default=t
|
476
|
+
): validate_transparency((t,)),
|
477
|
+
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
478
|
+
(image_type,)
|
479
|
+
),
|
480
|
+
}
|
481
|
+
)
|
482
|
+
)
|
483
|
+
for t in IMAGE_TYPE[image_type].allow_config.intersection(
|
484
|
+
TRANSPARENCY_TYPES
|
485
|
+
)
|
486
|
+
}
|
487
|
+
),
|
488
|
+
# Allow a default configuration with no transparency preselected
|
489
|
+
cv.ensure_list(
|
490
|
+
BASE_SCHEMA.extend(
|
491
|
+
{
|
492
|
+
cv.Optional(
|
493
|
+
CONF_TRANSPARENCY, default=CONF_OPAQUE
|
494
|
+
): validate_transparency(),
|
495
|
+
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
496
|
+
(image_type,)
|
497
|
+
),
|
498
|
+
}
|
499
|
+
)
|
500
|
+
),
|
501
|
+
)
|
270
502
|
|
271
|
-
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
|
272
|
-
path = compute_local_image_path(conf_file).as_posix()
|
273
503
|
|
274
|
-
|
275
|
-
|
504
|
+
# The config schema can be a (possibly empty) single list of images,
|
505
|
+
# or a dictionary of image types each with a list of images
|
506
|
+
CONFIG_SCHEMA = cv.Any(
|
507
|
+
cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}),
|
508
|
+
cv.ensure_list(IMAGE_SCHEMA),
|
509
|
+
)
|
276
510
|
|
277
|
-
try:
|
278
|
-
with open(path, "rb") as f:
|
279
|
-
file_contents = f.read()
|
280
|
-
except Exception as e:
|
281
|
-
raise core.EsphomeError(f"Could not load image file {path}: {e}")
|
282
511
|
|
283
|
-
|
512
|
+
async def write_image(config, all_frames=False):
|
513
|
+
path = Path(config[CONF_FILE])
|
514
|
+
if not path.is_file():
|
515
|
+
raise core.EsphomeError(f"Could not load image file {path}")
|
284
516
|
|
285
517
|
resize = config.get(CONF_RESIZE)
|
286
|
-
if
|
287
|
-
|
518
|
+
if is_svg_file(path):
|
519
|
+
# Local import so use of non-SVG files needn't require cairosvg installed
|
520
|
+
from cairosvg import svg2png
|
521
|
+
|
522
|
+
if not resize:
|
523
|
+
resize = (None, None)
|
524
|
+
with open(path, "rb") as file:
|
525
|
+
image = svg2png(
|
526
|
+
file_obj=file,
|
527
|
+
output_width=resize[0],
|
528
|
+
output_height=resize[1],
|
529
|
+
)
|
530
|
+
image = Image.open(io.BytesIO(image))
|
531
|
+
width, height = image.size
|
288
532
|
else:
|
289
|
-
image = Image.open(
|
533
|
+
image = Image.open(path)
|
534
|
+
width, height = image.size
|
290
535
|
if resize:
|
291
|
-
|
292
|
-
|
293
|
-
|
536
|
+
# Preserve aspect ratio
|
537
|
+
new_width_max = min(width, resize[0])
|
538
|
+
new_height_max = min(height, resize[1])
|
539
|
+
ratio = min(new_width_max / width, new_height_max / height)
|
540
|
+
width, height = int(width * ratio), int(height * ratio)
|
294
541
|
|
295
|
-
if
|
542
|
+
if not resize and (width > 500 or height > 500):
|
296
543
|
_LOGGER.warning(
|
297
544
|
'The image "%s" you requested is very big. Please consider'
|
298
545
|
" using the resize parameter.",
|
299
546
|
path,
|
300
547
|
)
|
301
548
|
|
302
|
-
transparent = config[CONF_USE_TRANSPARENCY]
|
303
|
-
|
304
549
|
dither = (
|
305
550
|
Image.Dither.NONE
|
306
551
|
if config[CONF_DITHER] == "NONE"
|
307
552
|
else Image.Dither.FLOYDSTEINBERG
|
308
553
|
)
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
for
|
345
|
-
|
346
|
-
if r == 0 and g == 0 and b == 1:
|
347
|
-
b = 0
|
348
|
-
if a < 0x80:
|
349
|
-
r = 0
|
350
|
-
g = 0
|
351
|
-
b = 1
|
352
|
-
|
353
|
-
data[pos] = r
|
354
|
-
pos += 1
|
355
|
-
data[pos] = g
|
356
|
-
pos += 1
|
357
|
-
data[pos] = b
|
358
|
-
pos += 1
|
359
|
-
|
360
|
-
elif config[CONF_TYPE] in ["RGB565"]:
|
361
|
-
image = image.convert("RGBA")
|
362
|
-
pixels = list(image.getdata())
|
363
|
-
bytes_per_pixel = 3 if transparent else 2
|
364
|
-
data = [0 for _ in range(height * width * bytes_per_pixel)]
|
365
|
-
pos = 0
|
366
|
-
for r, g, b, a in pixels:
|
367
|
-
R = r >> 3
|
368
|
-
G = g >> 2
|
369
|
-
B = b >> 3
|
370
|
-
rgb = (R << 11) | (G << 5) | B
|
371
|
-
data[pos] = rgb >> 8
|
372
|
-
pos += 1
|
373
|
-
data[pos] = rgb & 0xFF
|
374
|
-
pos += 1
|
375
|
-
if transparent:
|
376
|
-
data[pos] = a
|
377
|
-
pos += 1
|
378
|
-
|
379
|
-
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
|
380
|
-
if transparent:
|
381
|
-
alpha = image.split()[-1]
|
382
|
-
has_alpha = alpha.getextrema()[0] < 0xFF
|
383
|
-
_LOGGER.debug("%s Has alpha: %s", config[CONF_ID], has_alpha)
|
384
|
-
image = image.convert("1", dither=dither)
|
385
|
-
width8 = ((width + 7) // 8) * 8
|
386
|
-
data = [0 for _ in range(height * width8 // 8)]
|
387
|
-
for y in range(height):
|
388
|
-
for x in range(width):
|
389
|
-
if transparent and has_alpha:
|
390
|
-
a = alpha.getpixel((x, y))
|
391
|
-
if not a:
|
392
|
-
continue
|
393
|
-
elif image.getpixel((x, y)):
|
394
|
-
continue
|
395
|
-
pos = x + y * width8
|
396
|
-
data[pos // 8] |= 0x80 >> (pos % 8)
|
554
|
+
type = config[CONF_TYPE]
|
555
|
+
transparency = config[CONF_TRANSPARENCY]
|
556
|
+
invert_alpha = config[CONF_INVERT_ALPHA]
|
557
|
+
frame_count = 1
|
558
|
+
if all_frames:
|
559
|
+
try:
|
560
|
+
frame_count = image.n_frames
|
561
|
+
except AttributeError:
|
562
|
+
pass
|
563
|
+
if frame_count <= 1:
|
564
|
+
_LOGGER.warning("Image file %s has no animation frames", path)
|
565
|
+
|
566
|
+
total_rows = height * frame_count
|
567
|
+
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
|
568
|
+
for frame_index in range(frame_count):
|
569
|
+
image.seek(frame_index)
|
570
|
+
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
571
|
+
for row in range(height):
|
572
|
+
for col in range(width):
|
573
|
+
encoder.encode(pixels[row * width + col])
|
574
|
+
encoder.end_row()
|
575
|
+
|
576
|
+
rhs = [HexInt(x) for x in encoder.data]
|
577
|
+
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
578
|
+
image_type = get_image_type_enum(type)
|
579
|
+
trans_value = get_transparency_enum(encoder.transparency)
|
580
|
+
|
581
|
+
return prog_arr, width, height, image_type, trans_value, frame_count
|
582
|
+
|
583
|
+
|
584
|
+
async def to_code(config):
|
585
|
+
if isinstance(config, list):
|
586
|
+
for entry in config:
|
587
|
+
await to_code(entry)
|
588
|
+
elif CONF_ID not in config:
|
589
|
+
for entry in config.values():
|
590
|
+
await to_code(entry)
|
397
591
|
else:
|
398
|
-
|
399
|
-
|
592
|
+
prog_arr, width, height, image_type, trans_value, _ = await write_image(config)
|
593
|
+
cg.new_Pvariable(
|
594
|
+
config[CONF_ID], prog_arr, width, height, image_type, trans_value
|
400
595
|
)
|
401
|
-
|
402
|
-
rhs = [HexInt(x) for x in data]
|
403
|
-
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
404
|
-
var = cg.new_Pvariable(
|
405
|
-
config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]]
|
406
|
-
)
|
407
|
-
cg.add(var.set_transparency(transparent))
|