mirror of
https://github.com/esphome/esphome.git
synced 2025-10-29 22:24:26 +00:00
Merge remote-tracking branch 'upstream/dev' into ci_impact_analysis
This commit is contained in:
@@ -69,7 +69,7 @@ def run_schema_validation(config: ConfigType) -> None:
|
||||
{
|
||||
"id": "display_id",
|
||||
"model": "custom",
|
||||
"dimensions": {"width": 320, "height": 240},
|
||||
"dimensions": {"width": 260, "height": 260},
|
||||
"draw_rounding": 13,
|
||||
"init_sequence": [[0xA0, 0x01]],
|
||||
},
|
||||
@@ -336,7 +336,7 @@ def test_native_generation(
|
||||
|
||||
main_cpp = generate_main(component_fixture_path("native.yaml"))
|
||||
assert (
|
||||
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, display::DISPLAY_ROTATION_0_DEGREES, 1>()"
|
||||
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, display::DISPLAY_ROTATION_0_DEGREES, 1, 1>()"
|
||||
in main_cpp
|
||||
)
|
||||
assert "set_init_sequence({240, 1, 8, 242" in main_cpp
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
substitutions:
|
||||
irq0_pin: GPIO13
|
||||
irq0_pin: GPIO0
|
||||
irq1_pin: GPIO15
|
||||
reset_pin: GPIO16
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@ sensor:
|
||||
irq_pin: ${irq_pin}
|
||||
voltage:
|
||||
name: ADE7953 Voltage
|
||||
id: ade7953_i2c_voltage
|
||||
current_a:
|
||||
name: ADE7953 Current A
|
||||
id: ade7953_i2c_current_a
|
||||
current_b:
|
||||
name: ADE7953 Current B
|
||||
id: ade7953_i2c_current_b
|
||||
power_factor_a:
|
||||
name: ADE7953 Power Factor A
|
||||
power_factor_b:
|
||||
|
||||
@@ -4,13 +4,13 @@ sensor:
|
||||
irq_pin: ${irq_pin}
|
||||
voltage:
|
||||
name: ADE7953 Voltage
|
||||
id: ade7953_voltage
|
||||
id: ade7953_spi_voltage
|
||||
current_a:
|
||||
name: ADE7953 Current A
|
||||
id: ade7953_current_a
|
||||
id: ade7953_spi_current_a
|
||||
current_b:
|
||||
name: ADE7953 Current B
|
||||
id: ade7953_current_b
|
||||
id: ade7953_spi_current_b
|
||||
power_factor_a:
|
||||
name: ADE7953 Power Factor A
|
||||
power_factor_b:
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
as3935_i2c:
|
||||
id: as3935_i2c_id
|
||||
i2c_id: i2c_bus
|
||||
irq_pin: ${irq_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: as3935
|
||||
as3935_id: as3935_i2c_id
|
||||
name: Storm Alert
|
||||
|
||||
sensor:
|
||||
- platform: as3935
|
||||
as3935_id: as3935_i2c_id
|
||||
lightning_energy:
|
||||
name: Lightning Energy
|
||||
distance:
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
as3935_spi:
|
||||
id: as3935_spi_id
|
||||
cs_pin: ${cs_pin}
|
||||
irq_pin: ${irq_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: as3935
|
||||
as3935_id: as3935_spi_id
|
||||
name: Storm Alert
|
||||
|
||||
sensor:
|
||||
- platform: as3935
|
||||
as3935_id: as3935_spi_id
|
||||
lightning_energy:
|
||||
name: Lightning Energy
|
||||
distance:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
i2c_id: i2c_bus
|
||||
id: ssd1306_display
|
||||
id: ssd1306_i2c_display
|
||||
model: SSD1306_128X64
|
||||
reset_pin: 19
|
||||
pages:
|
||||
@@ -13,6 +13,6 @@ touchscreen:
|
||||
- platform: axs15231
|
||||
i2c_id: i2c_bus
|
||||
id: axs15231_touchscreen
|
||||
display: ssd1306_display
|
||||
display: ssd1306_i2c_display
|
||||
interrupt_pin: 20
|
||||
reset_pin: 18
|
||||
|
||||
6
tests/components/bh1900nux/common.yaml
Normal file
6
tests/components/bh1900nux/common.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
sensor:
|
||||
- platform: bh1900nux
|
||||
i2c_id: i2c_bus
|
||||
name: Temperature Living Room
|
||||
address: 0x48
|
||||
update_interval: 30s
|
||||
4
tests/components/bh1900nux/test.esp32-c3-idf.yaml
Normal file
4
tests/components/bh1900nux/test.esp32-c3-idf.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/bh1900nux/test.esp32-idf.yaml
Normal file
4
tests/components/bh1900nux/test.esp32-idf.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/bh1900nux/test.esp8266-ard.yaml
Normal file
4
tests/components/bh1900nux/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/bh1900nux/test.rp2040-ard.yaml
Normal file
4
tests/components/bh1900nux/test.rp2040-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
19
tests/components/bluetooth_proxy/test.esp32-p4-idf.yaml
Normal file
19
tests/components/bluetooth_proxy/test.esp32-p4-idf.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
<<: !include common.yaml
|
||||
|
||||
esp32_ble_tracker:
|
||||
max_connections: 9
|
||||
|
||||
bluetooth_proxy:
|
||||
active: true
|
||||
connection_slots: 9
|
||||
|
||||
esp32_hosted:
|
||||
active_high: true
|
||||
variant: ESP32C6
|
||||
reset_pin: GPIO54
|
||||
cmd_pin: GPIO19
|
||||
clk_pin: GPIO18
|
||||
d0_pin: GPIO14
|
||||
d1_pin: GPIO15
|
||||
d2_pin: GPIO16
|
||||
d3_pin: GPIO17
|
||||
@@ -3,12 +3,12 @@ sensor:
|
||||
i2c_id: i2c_bus
|
||||
address: 0x76
|
||||
temperature:
|
||||
id: bme280_temperature
|
||||
id: bme280_i2c_temperature
|
||||
name: BME280 Temperature
|
||||
humidity:
|
||||
id: bme280_humidity
|
||||
id: bme280_i2c_humidity
|
||||
name: BME280 Humidity
|
||||
pressure:
|
||||
id: bme280_pressure
|
||||
id: bme280_i2c_pressure
|
||||
name: BME280 Pressure
|
||||
update_interval: 15s
|
||||
|
||||
@@ -2,12 +2,12 @@ sensor:
|
||||
- platform: bme280_spi
|
||||
cs_pin: ${cs_pin}
|
||||
temperature:
|
||||
id: bme280_temperature
|
||||
id: bme280_spi_temperature
|
||||
name: BME280 Temperature
|
||||
humidity:
|
||||
id: bme280_humidity
|
||||
id: bme280_spi_humidity
|
||||
name: BME280 Humidity
|
||||
pressure:
|
||||
id: bme280_pressure
|
||||
id: bme280_spi_pressure
|
||||
name: BME280 Pressure
|
||||
update_interval: 15s
|
||||
|
||||
@@ -3,10 +3,10 @@ sensor:
|
||||
i2c_id: i2c_bus
|
||||
address: 0x77
|
||||
temperature:
|
||||
id: bmp280_temperature
|
||||
id: bmp280_i2c_temperature
|
||||
name: Outside Temperature
|
||||
pressure:
|
||||
name: Outside Pressure
|
||||
id: bmp280_pressure
|
||||
id: bmp280_i2c_pressure
|
||||
iir_filter: 16x
|
||||
update_interval: 15s
|
||||
|
||||
@@ -2,10 +2,10 @@ sensor:
|
||||
- platform: bmp280_spi
|
||||
cs_pin: ${cs_pin}
|
||||
temperature:
|
||||
id: bmp280_temperature
|
||||
id: bmp280_spi_temperature
|
||||
name: Outside Temperature
|
||||
pressure:
|
||||
name: Outside Pressure
|
||||
id: bmp280_pressure
|
||||
id: bmp280_spi_pressure
|
||||
iir_filter: 16x
|
||||
update_interval: 15s
|
||||
|
||||
@@ -3,8 +3,10 @@ sensor:
|
||||
i2c_id: i2c_bus
|
||||
address: 0x77
|
||||
temperature:
|
||||
id: bmp3xx_i2c_temperature
|
||||
name: BMP Temperature
|
||||
oversampling: 16x
|
||||
pressure:
|
||||
id: bmp3xx_i2c_pressure
|
||||
name: BMP Pressure
|
||||
iir_filter: 2X
|
||||
|
||||
@@ -2,8 +2,10 @@ sensor:
|
||||
- platform: bmp3xx_spi
|
||||
cs_pin: ${cs_pin}
|
||||
temperature:
|
||||
id: bmp3xx_spi_temperature
|
||||
name: BMP Temperature
|
||||
oversampling: 16x
|
||||
pressure:
|
||||
id: bmp3xx_spi_pressure
|
||||
name: BMP Pressure
|
||||
iir_filter: 2X
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
packages:
|
||||
camera: !include ../../test_build_components/common/camera/esp32-idf.yaml
|
||||
i2c_camera: !include ../../test_build_components/common/i2c_camera/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
packages:
|
||||
camera: !include ../../test_build_components/common/camera/esp32-idf.yaml
|
||||
i2c_camera: !include ../../test_build_components/common/i2c_camera/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -7,8 +7,8 @@ display:
|
||||
id: ili9xxx_display
|
||||
model: GC9A01A
|
||||
invert_colors: True
|
||||
cs_pin: 10
|
||||
dc_pin: 6
|
||||
cs_pin: 11
|
||||
dc_pin: 7
|
||||
pages:
|
||||
- id: page1
|
||||
lambda: |-
|
||||
|
||||
@@ -4,6 +4,7 @@ packages:
|
||||
|
||||
display:
|
||||
- platform: ili9xxx
|
||||
spi_id: spi_bus
|
||||
id: ili9xxx_display
|
||||
model: GC9A01A
|
||||
invert_colors: True
|
||||
@@ -16,5 +17,6 @@ display:
|
||||
|
||||
touchscreen:
|
||||
- platform: chsc6x
|
||||
i2c_id: i2c_bus
|
||||
display: ili9xxx_display
|
||||
interrupt_pin: 20
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
i2c_id: i2c_bus
|
||||
id: ssd1306_display
|
||||
id: ssd1306_i2c_display
|
||||
model: SSD1306_128X64
|
||||
reset_pin: ${display_reset_pin}
|
||||
pages:
|
||||
@@ -15,7 +15,7 @@ touchscreen:
|
||||
id: ektf2232_touchscreen
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
reset_pin: ${touch_reset_pin}
|
||||
display: ssd1306_display
|
||||
display: ssd1306_i2c_display
|
||||
on_touch:
|
||||
- logger.log:
|
||||
format: Touch at (%d, %d)
|
||||
|
||||
@@ -3,8 +3,11 @@ sensor:
|
||||
i2c_id: i2c_bus
|
||||
address: 0x53
|
||||
eco2:
|
||||
id: ens160_i2c_eco2
|
||||
name: "ENS160 eCO2"
|
||||
tvoc:
|
||||
id: ens160_i2c_tvoc
|
||||
name: "ENS160 Total Volatile Organic Compounds"
|
||||
aqi:
|
||||
id: ens160_i2c_aqi
|
||||
name: "ENS160 Air Quality Index"
|
||||
|
||||
@@ -2,8 +2,11 @@ sensor:
|
||||
- platform: ens160_spi
|
||||
cs_pin: ${cs_pin}
|
||||
eco2:
|
||||
id: ens160_spi_eco2
|
||||
name: "ENS160 eCO2"
|
||||
tvoc:
|
||||
id: ens160_spi_tvoc
|
||||
name: "ENS160 Total Volatile Organic Compounds"
|
||||
aqi:
|
||||
id: ens160_spi_aqi
|
||||
name: "ENS160 Air Quality Index"
|
||||
|
||||
@@ -5,6 +5,7 @@ esp32:
|
||||
advanced:
|
||||
enable_lwip_mdns_queries: true
|
||||
enable_lwip_bridge_interface: true
|
||||
disable_libc_locks_in_iram: false # Test explicit opt-out of RAM optimization
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
|
||||
@@ -4,6 +4,7 @@ esp32:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
execute_from_psram: true
|
||||
disable_libc_locks_in_iram: true # Test default RAM optimization enabled
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
packages:
|
||||
camera: !include ../../test_build_components/common/camera/esp32-idf.yaml
|
||||
i2c_camera: !include ../../test_build_components/common/i2c_camera/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
packages:
|
||||
camera: !include ../../test_build_components/common/camera/esp32-idf.yaml
|
||||
i2c_camera: !include ../../test_build_components/common/i2c_camera/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -49,6 +49,7 @@ font:
|
||||
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
i2c_id: i2c_bus
|
||||
id: ssd1306_display
|
||||
model: SSD1306_128X64
|
||||
reset_pin: ${display_reset_pin}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO12
|
||||
interrupt_pin: GPIO0
|
||||
reset_pin: GPIO16
|
||||
|
||||
packages:
|
||||
|
||||
@@ -11,6 +11,7 @@ graph:
|
||||
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
i2c_id: i2c_bus
|
||||
id: ssd1306_display
|
||||
model: SSD1306_128X64
|
||||
reset_pin: ${reset_pin}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
id: ssd1306_display
|
||||
id: ssd1306_i2c_display
|
||||
model: SSD1306_128X64
|
||||
reset_pin: ${reset_pin}
|
||||
pages:
|
||||
@@ -36,7 +36,7 @@ switch:
|
||||
|
||||
graphical_display_menu:
|
||||
id: test_graphical_display_menu
|
||||
display: ssd1306_display
|
||||
display: ssd1306_i2c_display
|
||||
font: roboto
|
||||
active: false
|
||||
mode: rotary
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
i2c_id: i2c_bus
|
||||
id: ssd1306_display
|
||||
id: ssd1306_i2c_display
|
||||
model: SSD1306_128X64
|
||||
reset_pin: ${display_reset_pin}
|
||||
pages:
|
||||
@@ -13,7 +13,7 @@ touchscreen:
|
||||
- platform: gt911
|
||||
i2c_id: i2c_bus
|
||||
id: gt911_touchscreen
|
||||
display: ssd1306_display
|
||||
display: ssd1306_i2c_display
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
reset_pin: ${reset_pin}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
substitutions:
|
||||
clk_pin: GPIO4
|
||||
dout_pin: GPIO5
|
||||
clk_pin: GPIO0
|
||||
dout_pin: GPIO2
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -7,9 +7,22 @@ sensor:
|
||||
max_current: 40 A
|
||||
adc_range: 1
|
||||
temperature_coefficient: 50
|
||||
shunt_voltage: "INA2xx Shunt Voltage"
|
||||
bus_voltage: "INA2xx Bus Voltage"
|
||||
current: "INA2xx Current"
|
||||
power: "INA2xx Power"
|
||||
energy: "INA2xx Energy"
|
||||
charge: "INA2xx Charge"
|
||||
reset_on_boot: true
|
||||
shunt_voltage:
|
||||
id: ina2xx_i2c_shunt_voltage
|
||||
name: "INA2xx Shunt Voltage"
|
||||
bus_voltage:
|
||||
id: ina2xx_i2c_bus_voltage
|
||||
name: "INA2xx Bus Voltage"
|
||||
current:
|
||||
id: ina2xx_i2c_current
|
||||
name: "INA2xx Current"
|
||||
power:
|
||||
id: ina2xx_i2c_power
|
||||
name: "INA2xx Power"
|
||||
energy:
|
||||
id: ina2xx_i2c_energy
|
||||
name: "INA2xx Energy"
|
||||
charge:
|
||||
id: ina2xx_i2c_charge
|
||||
name: "INA2xx Charge"
|
||||
|
||||
@@ -6,9 +6,21 @@ sensor:
|
||||
max_current: 40 A
|
||||
adc_range: 1
|
||||
temperature_coefficient: 50
|
||||
shunt_voltage: "INA2xx Shunt Voltage"
|
||||
bus_voltage: "INA2xx Bus Voltage"
|
||||
current: "INA2xx Current"
|
||||
power: "INA2xx Power"
|
||||
energy: "INA2xx Energy"
|
||||
charge: "INA2xx Charge"
|
||||
shunt_voltage:
|
||||
id: ina2xx_spi_shunt_voltage
|
||||
name: "INA2xx Shunt Voltage"
|
||||
bus_voltage:
|
||||
id: ina2xx_spi_bus_voltage
|
||||
name: "INA2xx Bus Voltage"
|
||||
current:
|
||||
id: ina2xx_spi_current
|
||||
name: "INA2xx Current"
|
||||
power:
|
||||
id: ina2xx_spi_power
|
||||
name: "INA2xx Power"
|
||||
energy:
|
||||
id: ina2xx_spi_energy
|
||||
name: "INA2xx Energy"
|
||||
charge:
|
||||
id: ina2xx_spi_charge
|
||||
name: "INA2xx Charge"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
i2c_id: i2c_bus
|
||||
id: ssd1306_display
|
||||
id: ssd1306_i2c_display
|
||||
model: SSD1306_128X64
|
||||
reset_pin: ${reset_pin}
|
||||
pages:
|
||||
@@ -14,7 +14,7 @@ touchscreen:
|
||||
i2c_id: i2c_bus
|
||||
id: lilygo_touchscreen
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
display: ssd1306_display
|
||||
display: ssd1306_i2c_display
|
||||
on_touch:
|
||||
- logger.log:
|
||||
format: Touch at (%d, %d)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
output:
|
||||
- platform: mcp4725
|
||||
id: mcp4725_dac_output
|
||||
i2c_id: i2c_bus
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
output:
|
||||
- platform: mcp47a1
|
||||
id: output_mcp47a1
|
||||
i2c_id: i2c_bus
|
||||
|
||||
@@ -10,7 +10,7 @@ display:
|
||||
invert_colors: true
|
||||
show_test_card: true
|
||||
spi_mode: mode0
|
||||
draw_rounding: 8
|
||||
draw_rounding: 4
|
||||
use_axis_flips: true
|
||||
init_sequence:
|
||||
- [0xd0, 1, 2, 3]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
substitutions:
|
||||
dc_pin: GPIO14
|
||||
cs_pin: GPIO13
|
||||
enable_pin: GPIO16
|
||||
enable_pin: GPIO17
|
||||
reset_pin: GPIO20
|
||||
|
||||
packages:
|
||||
|
||||
7
tests/components/nrf52/test.nrf52-xiao-ble.yaml
Normal file
7
tests/components/nrf52/test.nrf52-xiao-ble.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
nrf52:
|
||||
dfu:
|
||||
reset_pin:
|
||||
number: 14
|
||||
inverted: true
|
||||
mode:
|
||||
output: true
|
||||
@@ -1,9 +1,9 @@
|
||||
pn532_i2c:
|
||||
i2c_id: i2c_bus
|
||||
id: pn532_nfcc
|
||||
id: pn532_nfcc_i2c
|
||||
|
||||
binary_sensor:
|
||||
- platform: pn532
|
||||
pn532_id: pn532_nfcc
|
||||
pn532_id: pn532_nfcc_i2c
|
||||
name: PN532 NFC Tag
|
||||
uid: 74-10-37-94
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
pn532_spi:
|
||||
id: pn532_nfcc
|
||||
id: pn532_nfcc_spi
|
||||
cs_pin: ${cs_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: pn532
|
||||
pn532_id: pn532_nfcc
|
||||
pn532_id: pn532_nfcc_spi
|
||||
name: PN532 NFC Tag
|
||||
uid: 74-10-37-94
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- tag.set_clean_mode: nfcc_pn7160
|
||||
- tag.set_format_mode: nfcc_pn7160
|
||||
- tag.set_read_mode: nfcc_pn7160
|
||||
- tag.set_clean_mode: nfcc_pn7160_i2c
|
||||
- tag.set_format_mode: nfcc_pn7160_i2c
|
||||
- tag.set_read_mode: nfcc_pn7160_i2c
|
||||
- tag.set_write_message:
|
||||
message: https://www.home-assistant.io/tag/pulse
|
||||
include_android_app_record: false
|
||||
- tag.set_write_mode: nfcc_pn7160
|
||||
- tag.set_write_mode: nfcc_pn7160_i2c
|
||||
- tag.set_emulation_message:
|
||||
message: https://www.home-assistant.io/tag/pulse
|
||||
include_android_app_record: false
|
||||
- tag.emulation_off: nfcc_pn7160
|
||||
- tag.emulation_on: nfcc_pn7160
|
||||
- tag.polling_off: nfcc_pn7160
|
||||
- tag.polling_on: nfcc_pn7160
|
||||
- tag.emulation_off: nfcc_pn7160_i2c
|
||||
- tag.emulation_on: nfcc_pn7160_i2c
|
||||
- tag.polling_off: nfcc_pn7160_i2c
|
||||
- tag.polling_on: nfcc_pn7160_i2c
|
||||
|
||||
pn7150_i2c:
|
||||
id: nfcc_pn7160
|
||||
id: nfcc_pn7160_i2c
|
||||
i2c_id: i2c_bus
|
||||
irq_pin: ${irq_pin}
|
||||
ven_pin: ${ven_pin}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- tag.set_clean_mode: nfcc_pn7160
|
||||
- tag.set_format_mode: nfcc_pn7160
|
||||
- tag.set_read_mode: nfcc_pn7160
|
||||
- tag.set_clean_mode: nfcc_pn7160_spi
|
||||
- tag.set_format_mode: nfcc_pn7160_spi
|
||||
- tag.set_read_mode: nfcc_pn7160_spi
|
||||
- tag.set_write_message:
|
||||
message: https://www.home-assistant.io/tag/pulse
|
||||
include_android_app_record: false
|
||||
- tag.set_write_mode: nfcc_pn7160
|
||||
- tag.set_write_mode: nfcc_pn7160_spi
|
||||
- tag.set_emulation_message:
|
||||
message: https://www.home-assistant.io/tag/pulse
|
||||
include_android_app_record: false
|
||||
- tag.emulation_off: nfcc_pn7160
|
||||
- tag.emulation_on: nfcc_pn7160
|
||||
- tag.polling_off: nfcc_pn7160
|
||||
- tag.polling_on: nfcc_pn7160
|
||||
- tag.emulation_off: nfcc_pn7160_spi
|
||||
- tag.emulation_on: nfcc_pn7160_spi
|
||||
- tag.polling_off: nfcc_pn7160_spi
|
||||
- tag.polling_on: nfcc_pn7160_spi
|
||||
|
||||
pn7160_spi:
|
||||
id: nfcc_pn7160
|
||||
id: nfcc_pn7160_spi
|
||||
cs_pin: ${cs_pin}
|
||||
irq_pin: ${irq_pin}
|
||||
ven_pin: ${ven_pin}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
rc522_i2c:
|
||||
- id: rc522_nfcc
|
||||
- id: rc522_nfcc_i2c
|
||||
i2c_id: i2c_bus
|
||||
update_interval: 1s
|
||||
on_tag:
|
||||
@@ -8,6 +8,6 @@ rc522_i2c:
|
||||
|
||||
binary_sensor:
|
||||
- platform: rc522
|
||||
rc522_id: rc522_nfcc
|
||||
rc522_id: rc522_nfcc_i2c
|
||||
name: RC522 NFC Tag
|
||||
uid: 74-10-37-94
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
rc522_spi:
|
||||
id: rc522_nfcc
|
||||
id: rc522_nfcc_spi
|
||||
cs_pin: ${cs_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: rc522
|
||||
rc522_id: rc522_nfcc
|
||||
name: PN532 NFC Tag
|
||||
rc522_id: rc522_nfcc_spi
|
||||
name: RC522 NFC Tag
|
||||
uid: 74-10-37-94
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO0
|
||||
rx_pin: GPIO2
|
||||
flow_control_pin: GPIO4
|
||||
flow_control_pin: GPIO15
|
||||
|
||||
packages:
|
||||
modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml
|
||||
|
||||
@@ -2,8 +2,8 @@ packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml
|
||||
|
||||
substitutions:
|
||||
clock_pin: GPIO5
|
||||
data_pin: GPIO4
|
||||
clock_pin: GPIO15
|
||||
data_pin: GPIO16
|
||||
latch_pin1: GPIO2
|
||||
oe_pin1: GPIO0
|
||||
latch_pin2: GPIO3
|
||||
|
||||
@@ -4,7 +4,7 @@ display:
|
||||
model: SSD1306_128X64
|
||||
reset_pin: ${reset_pin}
|
||||
address: 0x3C
|
||||
id: display1
|
||||
id: ssd1306_i2c_display
|
||||
contrast: 60%
|
||||
pages:
|
||||
- id: ssd1306_i2c_page1
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
display:
|
||||
- platform: ssd1306_spi
|
||||
id: ssd1306_spi_display
|
||||
model: SSD1306 128x64
|
||||
cs_pin: ${cs_pin}
|
||||
dc_pin: ${dc_pin}
|
||||
|
||||
@@ -4,7 +4,7 @@ display:
|
||||
model: SSD1327_128x128
|
||||
reset_pin: ${reset_pin}
|
||||
address: 0x3C
|
||||
id: display1
|
||||
id: ssd1327_i2c_display
|
||||
pages:
|
||||
- id: ssd1327_i2c_page1
|
||||
lambda: |-
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
display:
|
||||
- platform: ssd1327_spi
|
||||
id: ssd1327_spi_display
|
||||
model: SSD1327 128x128
|
||||
cs_pin: ${cs_pin}
|
||||
dc_pin: ${dc_pin}
|
||||
|
||||
@@ -3,7 +3,7 @@ display:
|
||||
i2c_id: i2c_bus
|
||||
reset_pin: ${reset_pin}
|
||||
address: 0x3C
|
||||
id: display1
|
||||
id: st7567_i2c_display
|
||||
pages:
|
||||
- id: st7567_i2c_page1
|
||||
lambda: |-
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
display:
|
||||
- platform: st7567_spi
|
||||
id: st7567_spi_display
|
||||
cs_pin: ${cs_pin}
|
||||
dc_pin: ${dc_pin}
|
||||
reset_pin: ${reset_pin}
|
||||
|
||||
@@ -6,7 +6,8 @@ udp:
|
||||
addresses: ["239.0.60.53"]
|
||||
|
||||
time:
|
||||
platform: host
|
||||
- platform: host
|
||||
id: host_time
|
||||
|
||||
syslog:
|
||||
port: 514
|
||||
|
||||
13
tests/components/toshiba/common_ras2819t.yaml
Normal file
13
tests/components/toshiba/common_ras2819t.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
remote_transmitter:
|
||||
pin: ${tx_pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
remote_receiver:
|
||||
id: rcvr
|
||||
pin: ${rx_pin}
|
||||
|
||||
climate:
|
||||
- platform: toshiba
|
||||
name: "RAS-2819T Climate"
|
||||
model: RAS-2819T
|
||||
receiver_id: rcvr
|
||||
5
tests/components/toshiba/test_ras2819t.esp32-ard.yaml
Normal file
5
tests/components/toshiba/test_ras2819t.esp32-ard.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO5
|
||||
rx_pin: GPIO4
|
||||
|
||||
<<: !include common_ras2819t.yaml
|
||||
5
tests/components/toshiba/test_ras2819t.esp32-c3-ard.yaml
Normal file
5
tests/components/toshiba/test_ras2819t.esp32-c3-ard.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO5
|
||||
rx_pin: GPIO4
|
||||
|
||||
<<: !include common_ras2819t.yaml
|
||||
5
tests/components/toshiba/test_ras2819t.esp32-c3-idf.yaml
Normal file
5
tests/components/toshiba/test_ras2819t.esp32-c3-idf.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO5
|
||||
rx_pin: GPIO4
|
||||
|
||||
<<: !include common_ras2819t.yaml
|
||||
5
tests/components/toshiba/test_ras2819t.esp32-idf.yaml
Normal file
5
tests/components/toshiba/test_ras2819t.esp32-idf.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO5
|
||||
rx_pin: GPIO4
|
||||
|
||||
<<: !include common_ras2819t.yaml
|
||||
5
tests/components/toshiba/test_ras2819t.esp8266-ard.yaml
Normal file
5
tests/components/toshiba/test_ras2819t.esp8266-ard.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO5
|
||||
rx_pin: GPIO4
|
||||
|
||||
<<: !include common_ras2819t.yaml
|
||||
@@ -1,7 +1,7 @@
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
i2c_id: i2c_bus
|
||||
id: ssd1306_display
|
||||
id: ssd1306_i2c_display
|
||||
model: SSD1306_128X64
|
||||
reset_pin: ${disp_reset_pin}
|
||||
pages:
|
||||
@@ -13,7 +13,7 @@ touchscreen:
|
||||
- platform: tt21100
|
||||
i2c_id: i2c_bus
|
||||
id: tt21100_touchscreen
|
||||
display: ssd1306_display
|
||||
display: ssd1306_i2c_display
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
reset_pin: ${reset_pin}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -4,5 +4,6 @@ substitutions:
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
||||
uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
wk2132_spi:
|
||||
- id: wk2132_spi_id
|
||||
- id: wk2132_spi_bridge
|
||||
cs_pin: ${cs_pin}
|
||||
crystal: 11059200
|
||||
data_rate: 1MHz
|
||||
uart:
|
||||
- id: wk2132_spi_id0
|
||||
- id: wk2132_spi_uart0
|
||||
channel: 0
|
||||
baud_rate: 115200
|
||||
stop_bits: 1
|
||||
parity: none
|
||||
- id: wk2132_spi_id1
|
||||
- id: wk2132_spi_uart1
|
||||
channel: 1
|
||||
baud_rate: 9600
|
||||
|
||||
# Ensures a sensor doesn't break validation
|
||||
sensor:
|
||||
- platform: a02yyuw
|
||||
uart_id: wk2132_spi_id1
|
||||
uart_id: wk2132_spi_uart1
|
||||
id: distance_sensor
|
||||
|
||||
@@ -3,5 +3,6 @@ substitutions:
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -6,5 +6,6 @@ substitutions:
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
|
||||
uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -4,5 +4,6 @@ substitutions:
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
||||
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -3,5 +3,6 @@ substitutions:
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -6,5 +6,6 @@ substitutions:
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
|
||||
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -4,5 +4,6 @@ substitutions:
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
||||
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
wk2204_spi:
|
||||
- id: wk2204_spi_id
|
||||
- id: wk2204_spi_bridge
|
||||
cs_pin: ${cs_pin}
|
||||
crystal: 11059200
|
||||
data_rate: 1MHz
|
||||
uart:
|
||||
- id: wk2204_spi_id0
|
||||
- id: wk2204_spi_uart0
|
||||
channel: 0
|
||||
baud_rate: 115200
|
||||
stop_bits: 1
|
||||
parity: none
|
||||
- id: wk2204_spi_id1
|
||||
- id: wk2204_spi_uart1
|
||||
channel: 1
|
||||
baud_rate: 921600
|
||||
- id: wk2204_spi_id2
|
||||
- id: wk2204_spi_uart2
|
||||
channel: 2
|
||||
baud_rate: 115200
|
||||
stop_bits: 1
|
||||
parity: none
|
||||
- id: wk2204_spi_id3
|
||||
- id: wk2204_spi_uart3
|
||||
channel: 3
|
||||
baud_rate: 9600
|
||||
|
||||
# Ensures a sensor doesn't break validation
|
||||
sensor:
|
||||
- platform: a02yyuw
|
||||
uart_id: wk2204_spi_id3
|
||||
uart_id: wk2204_spi_uart3
|
||||
id: distance_sensor
|
||||
|
||||
@@ -3,5 +3,6 @@ substitutions:
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -6,5 +6,6 @@ substitutions:
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
|
||||
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -4,5 +4,6 @@ substitutions:
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
||||
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -3,5 +3,6 @@ substitutions:
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -6,5 +6,6 @@ substitutions:
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
|
||||
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.core import CORE
|
||||
from esphome.dashboard.settings import DashboardSettings
|
||||
|
||||
|
||||
@@ -159,3 +161,63 @@ def test_rel_path_with_numeric_args(dashboard_settings: DashboardSettings) -> No
|
||||
result = dashboard_settings.rel_path("123", "456.789")
|
||||
expected = dashboard_settings.config_dir / "123" / "456.789"
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_config_path_parent_resolves_to_config_dir(tmp_path: Path) -> None:
|
||||
"""Test that CORE.config_path.parent resolves to config_dir after parse_args.
|
||||
|
||||
This is a regression test for issue #11280 where binary download failed
|
||||
when using packages with secrets after the Path migration in 2025.10.0.
|
||||
|
||||
The issue was that after switching from os.path to Path:
|
||||
- Before: os.path.dirname("/config/.") → "/config"
|
||||
- After: Path("/config/.").parent → Path("/") (normalized first!)
|
||||
|
||||
The fix uses a sentinel file so .parent returns the correct directory:
|
||||
- Fixed: Path("/config/___DASHBOARD_SENTINEL___.yaml").parent → Path("/config")
|
||||
"""
|
||||
# Create test directory structure with secrets and packages
|
||||
config_dir = tmp_path / "config"
|
||||
config_dir.mkdir()
|
||||
|
||||
# Create secrets.yaml with obviously fake test values
|
||||
secrets_file = config_dir / "secrets.yaml"
|
||||
secrets_file.write_text(
|
||||
"wifi_ssid: TEST-DUMMY-SSID\n"
|
||||
"wifi_password: not-a-real-password-just-for-testing\n"
|
||||
)
|
||||
|
||||
# Create package file that uses secrets
|
||||
package_file = config_dir / "common.yaml"
|
||||
package_file.write_text(
|
||||
"wifi:\n ssid: !secret wifi_ssid\n password: !secret wifi_password\n"
|
||||
)
|
||||
|
||||
# Create main device config that includes the package
|
||||
device_config = config_dir / "test-device.yaml"
|
||||
device_config.write_text(
|
||||
"esphome:\n name: test-device\n\npackages:\n common: !include common.yaml\n"
|
||||
)
|
||||
|
||||
# Set up dashboard settings with our test config directory
|
||||
settings = DashboardSettings()
|
||||
args = Namespace(
|
||||
configuration=str(config_dir),
|
||||
password=None,
|
||||
username=None,
|
||||
ha_addon=False,
|
||||
verbose=False,
|
||||
)
|
||||
settings.parse_args(args)
|
||||
|
||||
# Verify that CORE.config_path.parent correctly points to the config directory
|
||||
# This is critical for secret resolution in yaml_util.py which does:
|
||||
# main_config_dir = CORE.config_path.parent
|
||||
# main_secret_yml = main_config_dir / "secrets.yaml"
|
||||
assert CORE.config_path.parent == config_dir.resolve()
|
||||
assert (CORE.config_path.parent / "secrets.yaml").exists()
|
||||
assert (CORE.config_path.parent / "common.yaml").exists()
|
||||
|
||||
# Verify that CORE.config_path itself uses the sentinel file
|
||||
assert CORE.config_path.name == "___DASHBOARD_SENTINEL___.yaml"
|
||||
assert not CORE.config_path.exists() # Sentinel file doesn't actually exist
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from argparse import Namespace
|
||||
import asyncio
|
||||
from collections.abc import Generator
|
||||
from contextlib import asynccontextmanager
|
||||
@@ -17,6 +18,8 @@ from tornado.ioloop import IOLoop
|
||||
from tornado.testing import bind_unused_port
|
||||
from tornado.websocket import WebSocketClientConnection, websocket_connect
|
||||
|
||||
from esphome import yaml_util
|
||||
from esphome.core import CORE
|
||||
from esphome.dashboard import web_server
|
||||
from esphome.dashboard.const import DashboardEvent
|
||||
from esphome.dashboard.core import DASHBOARD
|
||||
@@ -1302,3 +1305,71 @@ async def test_dashboard_subscriber_refresh_event(
|
||||
|
||||
# Give it a moment to clean up
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dashboard_yaml_loading_with_packages_and_secrets(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test dashboard YAML loading with packages referencing secrets.
|
||||
|
||||
This is a regression test for issue #11280 where binary download failed
|
||||
when using packages with secrets after the Path migration in 2025.10.0.
|
||||
|
||||
This test verifies that CORE.config_path initialization in the dashboard
|
||||
allows yaml_util.load_yaml() to correctly resolve secrets from packages.
|
||||
"""
|
||||
# Create test directory structure with secrets and packages
|
||||
config_dir = tmp_path / "config"
|
||||
config_dir.mkdir()
|
||||
|
||||
# Create secrets.yaml with obviously fake test values
|
||||
secrets_file = config_dir / "secrets.yaml"
|
||||
secrets_file.write_text(
|
||||
"wifi_ssid: TEST-DUMMY-SSID\n"
|
||||
"wifi_password: not-a-real-password-just-for-testing\n"
|
||||
)
|
||||
|
||||
# Create package file that uses secrets
|
||||
package_file = config_dir / "common.yaml"
|
||||
package_file.write_text(
|
||||
"wifi:\n ssid: !secret wifi_ssid\n password: !secret wifi_password\n"
|
||||
)
|
||||
|
||||
# Create main device config that includes the package
|
||||
device_config = config_dir / "test-download-secrets.yaml"
|
||||
device_config.write_text(
|
||||
"esphome:\n name: test-download-secrets\n platform: ESP32\n board: esp32dev\n\n"
|
||||
"packages:\n common: !include common.yaml\n"
|
||||
)
|
||||
|
||||
# Initialize DASHBOARD settings with our test config directory
|
||||
# This is what sets CORE.config_path - the critical code path for the bug
|
||||
args = Namespace(
|
||||
configuration=str(config_dir),
|
||||
password=None,
|
||||
username=None,
|
||||
ha_addon=False,
|
||||
verbose=False,
|
||||
)
|
||||
DASHBOARD.settings.parse_args(args)
|
||||
|
||||
# With the fix: CORE.config_path should be config_dir / "___DASHBOARD_SENTINEL___.yaml"
|
||||
# so CORE.config_path.parent would be config_dir
|
||||
# Without the fix: CORE.config_path is config_dir / "." which normalizes to config_dir
|
||||
# so CORE.config_path.parent would be tmp_path (the parent of config_dir)
|
||||
|
||||
# The fix ensures CORE.config_path.parent points to config_dir
|
||||
assert CORE.config_path.parent == config_dir.resolve(), (
|
||||
f"CORE.config_path.parent should point to config_dir. "
|
||||
f"Got {CORE.config_path.parent}, expected {config_dir.resolve()}. "
|
||||
f"CORE.config_path is {CORE.config_path}"
|
||||
)
|
||||
|
||||
# Now load the YAML with packages that reference secrets
|
||||
# This is where the bug would manifest - yaml_util.load_yaml would fail
|
||||
# to find secrets.yaml because CORE.config_path.parent pointed to the wrong place
|
||||
config = yaml_util.load_yaml(device_config)
|
||||
# If we get here, secret resolution worked!
|
||||
assert "esphome" in config
|
||||
assert config["esphome"]["name"] == "test-download-secrets"
|
||||
|
||||
@@ -87,9 +87,11 @@ def test_main_all_tests_should_run(
|
||||
mock_should_run_clang_format.return_value = True
|
||||
mock_should_run_python_linters.return_value = True
|
||||
|
||||
# Mock list-components.py output
|
||||
# Mock list-components.py output (now returns JSON with --changed-with-deps)
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = "wifi\napi\nsensor\n"
|
||||
mock_result.stdout = json.dumps(
|
||||
{"directly_changed": ["wifi", "api"], "all_changed": ["wifi", "api", "sensor"]}
|
||||
)
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
|
||||
# Run main function with mocked argv
|
||||
@@ -138,7 +140,7 @@ def test_main_no_tests_should_run(
|
||||
|
||||
# Mock empty list-components.py output
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = ""
|
||||
mock_result.stdout = json.dumps({"directly_changed": [], "all_changed": []})
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
|
||||
# Run main function with mocked argv
|
||||
@@ -207,7 +209,9 @@ def test_main_with_branch_argument(
|
||||
|
||||
# Mock list-components.py output
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = "mqtt\n"
|
||||
mock_result.stdout = json.dumps(
|
||||
{"directly_changed": ["mqtt"], "all_changed": ["mqtt"]}
|
||||
)
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
|
||||
with patch("sys.argv", ["script.py", "-b", "main"]):
|
||||
@@ -222,7 +226,7 @@ def test_main_with_branch_argument(
|
||||
# Check that list-components.py was called with branch
|
||||
mock_subprocess_run.assert_called_once()
|
||||
call_args = mock_subprocess_run.call_args[0][0]
|
||||
assert "--changed" in call_args
|
||||
assert "--changed-with-deps" in call_args
|
||||
assert "-b" in call_args
|
||||
assert "main" in call_args
|
||||
|
||||
@@ -449,7 +453,12 @@ def test_main_filters_components_without_tests(
|
||||
# Mock list-components.py output with 3 components
|
||||
# wifi: has tests, sensor: has tests, airthings_ble: no tests
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = "wifi\nsensor\nairthings_ble\n"
|
||||
mock_result.stdout = json.dumps(
|
||||
{
|
||||
"directly_changed": ["wifi", "sensor"],
|
||||
"all_changed": ["wifi", "sensor", "airthings_ble"],
|
||||
}
|
||||
)
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
|
||||
# Create test directory structure
|
||||
@@ -474,6 +483,8 @@ def test_main_filters_components_without_tests(
|
||||
patch.object(determine_jobs, "root_path", str(tmp_path)),
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
):
|
||||
# Clear the cache since we're mocking root_path
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
determine_jobs.main()
|
||||
|
||||
# Check output
|
||||
|
||||
@@ -6,6 +6,9 @@ esp32:
|
||||
board: lolin_c3_mini
|
||||
framework:
|
||||
type: esp-idf
|
||||
# Use custom partition table with larger app partition (3MB)
|
||||
# Default IDF partitions only allow 1.75MB which is too small for grouped tests
|
||||
partitions: ../partitions_testing.csv
|
||||
|
||||
logger:
|
||||
level: VERY_VERBOSE
|
||||
|
||||
@@ -3,9 +3,13 @@ esphome:
|
||||
friendly_name: $component_name
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
# Use board with 8MB flash for testing large component groups
|
||||
board: esp32-pico-devkitm-2
|
||||
framework:
|
||||
type: esp-idf
|
||||
# Use custom partition table with larger app partitions (3MB each)
|
||||
# Default IDF partitions only allow 1.75MB which is too small for grouped tests
|
||||
partitions: ../partitions_testing.csv
|
||||
|
||||
logger:
|
||||
level: VERY_VERBOSE
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: componenttestnrf52
|
||||
friendly_name: $component_name
|
||||
|
||||
nrf52:
|
||||
board: xiao_ble
|
||||
|
||||
logger:
|
||||
level: VERY_VERBOSE
|
||||
|
||||
packages:
|
||||
component_under_test: !include
|
||||
file: $component_test_file
|
||||
vars:
|
||||
component_test_file: $component_test_file
|
||||
@@ -1,3 +1,10 @@
|
||||
# I2C bus for camera sensor
|
||||
i2c:
|
||||
- id: i2c_camera_bus
|
||||
sda: 25
|
||||
scl: 23
|
||||
frequency: 400kHz
|
||||
|
||||
esp32_camera:
|
||||
name: ESP32 Camera
|
||||
data_pins:
|
||||
@@ -15,9 +22,7 @@ esp32_camera:
|
||||
external_clock:
|
||||
pin: 27
|
||||
frequency: 20MHz
|
||||
i2c_pins:
|
||||
sda: 25
|
||||
scl: 23
|
||||
i2c_id: i2c_camera_bus
|
||||
reset_pin: 15
|
||||
power_down_pin: 1
|
||||
resolution: 640x480
|
||||
@@ -0,0 +1,11 @@
|
||||
# Common configuration for 2-channel UART bridge/expander chips
|
||||
# Used by components like wk2132 that create 2 UART channels
|
||||
# Defines standardized UART IDs: uart_id_0, uart_id_1
|
||||
|
||||
substitutions:
|
||||
# These will be overridden by component-specific values
|
||||
uart_bridge_address: "0x70"
|
||||
|
||||
# Note: The actual UART instances are created by the bridge component
|
||||
# This package just ensures all bridge components use the same ID naming convention
|
||||
# so they can be grouped together without conflicts
|
||||
@@ -0,0 +1,11 @@
|
||||
# Common configuration for 2-channel UART bridge/expander chips
|
||||
# Used by components like wk2132 that create 2 UART channels
|
||||
# Defines standardized UART IDs: uart_id_0, uart_id_1
|
||||
|
||||
substitutions:
|
||||
# These will be overridden by component-specific values
|
||||
uart_bridge_address: "0x70"
|
||||
|
||||
# Note: The actual UART instances are created by the bridge component
|
||||
# This package just ensures all bridge components use the same ID naming convention
|
||||
# so they can be grouped together without conflicts
|
||||
@@ -0,0 +1,11 @@
|
||||
# Common configuration for 4-channel UART bridge/expander chips
|
||||
# Used by components like wk2168, wk2204, wk2212 that create 4 UART channels
|
||||
# Defines standardized UART IDs: uart_id_0, uart_id_1, uart_id_2, uart_id_3
|
||||
|
||||
substitutions:
|
||||
# These will be overridden by component-specific values
|
||||
uart_bridge_address: "0x70"
|
||||
|
||||
# Note: The actual UART instances are created by the bridge component
|
||||
# This package just ensures all bridge components use the same ID naming convention
|
||||
# so they can be grouped together without conflicts
|
||||
@@ -0,0 +1,11 @@
|
||||
# Common configuration for 4-channel UART bridge/expander chips
|
||||
# Used by components like wk2168, wk2204, wk2212 that create 4 UART channels
|
||||
# Defines standardized UART IDs: uart_id_0, uart_id_1, uart_id_2, uart_id_3
|
||||
|
||||
substitutions:
|
||||
# These will be overridden by component-specific values
|
||||
uart_bridge_address: "0x70"
|
||||
|
||||
# Note: The actual UART instances are created by the bridge component
|
||||
# This package just ensures all bridge components use the same ID naming convention
|
||||
# so they can be grouped together without conflicts
|
||||
10
tests/test_build_components/partitions_testing.csv
Normal file
10
tests/test_build_components/partitions_testing.csv
Normal file
@@ -0,0 +1,10 @@
|
||||
# ESP-IDF Partition Table for ESPHome Component Testing
|
||||
# Single app partition to maximize space for large component group testing
|
||||
# Fits in 4MB flash
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x4000,
|
||||
otadata, data, ota, , 0x2000,
|
||||
phy_init, data, phy, , 0x1000,
|
||||
factory, app, factory, 0x10000, 0x300000,
|
||||
nvs_key, data, nvs_keys,, 0x1000,
|
||||
coredump, data, coredump,, 0xEB000,
|
||||
|
@@ -96,6 +96,13 @@ def mock_run_git_command() -> Generator[Mock, None, None]:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_subprocess_run() -> Generator[Mock, None, None]:
|
||||
"""Mock subprocess.run for testing."""
|
||||
with patch("subprocess.run") as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_idedata() -> Generator[Mock, None, None]:
|
||||
"""Mock get_idedata for platformio_api."""
|
||||
|
||||
@@ -287,7 +287,7 @@ def test_perform_ota_no_auth(mock_socket: Mock, mock_file: io.BytesIO) -> None:
|
||||
|
||||
mock_socket.recv.side_effect = recv_responses
|
||||
|
||||
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
|
||||
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
|
||||
|
||||
# Should not send any auth-related data
|
||||
auth_calls = [
|
||||
@@ -317,7 +317,7 @@ def test_perform_ota_with_compression(mock_socket: Mock) -> None:
|
||||
|
||||
mock_socket.recv.side_effect = recv_responses
|
||||
|
||||
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
|
||||
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
|
||||
|
||||
# Verify compressed content was sent
|
||||
# Get the binary size that was sent (4 bytes after features)
|
||||
@@ -347,7 +347,7 @@ def test_perform_ota_auth_without_password(mock_socket: Mock) -> None:
|
||||
with pytest.raises(
|
||||
espota2.OTAError, match="ESP requests password, but no password given"
|
||||
):
|
||||
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
|
||||
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_time")
|
||||
@@ -413,7 +413,7 @@ def test_perform_ota_sha256_auth_without_password(mock_socket: Mock) -> None:
|
||||
with pytest.raises(
|
||||
espota2.OTAError, match="ESP requests password, but no password given"
|
||||
):
|
||||
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
|
||||
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
|
||||
|
||||
|
||||
def test_perform_ota_unexpected_auth_response(mock_socket: Mock) -> None:
|
||||
@@ -450,7 +450,7 @@ def test_perform_ota_unsupported_version(mock_socket: Mock) -> None:
|
||||
mock_socket.recv.side_effect = responses
|
||||
|
||||
with pytest.raises(espota2.OTAError, match="Device uses unsupported OTA version"):
|
||||
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
|
||||
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_time")
|
||||
@@ -471,7 +471,7 @@ def test_perform_ota_upload_error(mock_socket: Mock, mock_file: io.BytesIO) -> N
|
||||
mock_socket.recv.side_effect = recv_responses
|
||||
|
||||
with pytest.raises(espota2.OTAError, match="Error receiving acknowledge chunk OK"):
|
||||
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
|
||||
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_socket_constructor", "mock_resolve_ip")
|
||||
@@ -706,7 +706,7 @@ def test_perform_ota_version_differences(
|
||||
]
|
||||
|
||||
mock_socket.recv.side_effect = recv_responses
|
||||
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
|
||||
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
|
||||
|
||||
# For v1.0, verify that we only get the expected number of recv calls
|
||||
# v1.0 doesn't have chunk acknowledgments, so fewer recv calls
|
||||
@@ -732,7 +732,7 @@ def test_perform_ota_version_differences(
|
||||
]
|
||||
|
||||
mock_socket.recv.side_effect = recv_responses_v2
|
||||
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
|
||||
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
|
||||
|
||||
# For v2.0, verify more recv calls due to chunk acknowledgments
|
||||
assert mock_socket.recv.call_count == 9 # v2.0 has 9 recv calls (includes chunk OK)
|
||||
|
||||
@@ -1,13 +1,204 @@
|
||||
"""Tests for git.py module."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import git
|
||||
from esphome.core import CORE, TimePeriodSeconds
|
||||
from esphome.git import GitCommandError
|
||||
|
||||
|
||||
def _compute_repo_dir(url: str, ref: str | None, domain: str) -> Path:
|
||||
"""Helper to compute the expected repo directory path using git module's logic."""
|
||||
key = f"{url}@{ref}"
|
||||
return git._compute_destination_path(key, domain)
|
||||
|
||||
|
||||
def _setup_old_repo(repo_dir: Path, days_old: int = 2) -> None:
|
||||
"""Helper to set up a git repo directory structure with an old timestamp.
|
||||
|
||||
Args:
|
||||
repo_dir: The repository directory path to create.
|
||||
days_old: Number of days old to make the FETCH_HEAD file (default: 2).
|
||||
"""
|
||||
# Create repo directory
|
||||
repo_dir.mkdir(parents=True)
|
||||
git_dir = repo_dir / ".git"
|
||||
git_dir.mkdir()
|
||||
|
||||
# Create FETCH_HEAD file with old timestamp
|
||||
fetch_head = git_dir / "FETCH_HEAD"
|
||||
fetch_head.write_text("test")
|
||||
old_time = datetime.now() - timedelta(days=days_old)
|
||||
fetch_head.touch()
|
||||
os.utime(fetch_head, (old_time.timestamp(), old_time.timestamp()))
|
||||
|
||||
|
||||
def _get_git_command_type(cmd: list[str]) -> str | None:
|
||||
"""Helper to determine the type of git command from a command list.
|
||||
|
||||
Args:
|
||||
cmd: The git command list (e.g., ["git", "rev-parse", "HEAD"]).
|
||||
|
||||
Returns:
|
||||
The command type ("rev-parse", "stash", "fetch", "reset", "clone") or None.
|
||||
"""
|
||||
# Git commands are always in format ["git", "command", ...], so check index 1
|
||||
if len(cmd) > 1:
|
||||
return cmd[1]
|
||||
return None
|
||||
|
||||
|
||||
def test_run_git_command_success(tmp_path: Path) -> None:
|
||||
"""Test that run_git_command returns output on success."""
|
||||
# Create a simple git repo to test with
|
||||
repo_dir = tmp_path / "test_repo"
|
||||
repo_dir.mkdir()
|
||||
|
||||
# Initialize a git repo
|
||||
result = git.run_git_command(["git", "init"], str(repo_dir))
|
||||
assert "Initialized empty Git repository" in result or result == ""
|
||||
|
||||
# Verify we can run a command and get output
|
||||
result = git.run_git_command(["git", "status", "--porcelain"], str(repo_dir))
|
||||
# Empty repo should have empty status
|
||||
assert isinstance(result, str)
|
||||
|
||||
|
||||
def test_run_git_command_with_git_dir_isolation(
|
||||
tmp_path: Path, mock_subprocess_run: Mock
|
||||
) -> None:
|
||||
"""Test that git_dir parameter properly isolates git operations."""
|
||||
repo_dir = tmp_path / "test_repo"
|
||||
repo_dir.mkdir()
|
||||
git_dir = repo_dir / ".git"
|
||||
git_dir.mkdir()
|
||||
|
||||
# Configure mock to return success
|
||||
mock_subprocess_run.return_value = Mock(
|
||||
returncode=0,
|
||||
stdout=b"test output",
|
||||
stderr=b"",
|
||||
)
|
||||
|
||||
result = git.run_git_command(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
git_dir=repo_dir,
|
||||
)
|
||||
|
||||
# Verify subprocess.run was called
|
||||
assert mock_subprocess_run.called
|
||||
call_args = mock_subprocess_run.call_args
|
||||
|
||||
# Verify environment was set
|
||||
env = call_args[1]["env"]
|
||||
assert "GIT_DIR" in env
|
||||
assert "GIT_WORK_TREE" in env
|
||||
assert env["GIT_DIR"] == str(repo_dir / ".git")
|
||||
assert env["GIT_WORK_TREE"] == str(repo_dir)
|
||||
|
||||
assert result == "test output"
|
||||
|
||||
|
||||
def test_run_git_command_raises_git_not_installed_error(
|
||||
tmp_path: Path, mock_subprocess_run: Mock
|
||||
) -> None:
|
||||
"""Test that FileNotFoundError is converted to GitNotInstalledError."""
|
||||
from esphome.git import GitNotInstalledError
|
||||
|
||||
repo_dir = tmp_path / "test_repo"
|
||||
|
||||
# Configure mock to raise FileNotFoundError
|
||||
mock_subprocess_run.side_effect = FileNotFoundError("git not found")
|
||||
|
||||
with pytest.raises(GitNotInstalledError, match="git is not installed"):
|
||||
git.run_git_command(["git", "status"], git_dir=repo_dir)
|
||||
|
||||
|
||||
def test_run_git_command_raises_git_command_error_on_failure(
|
||||
tmp_path: Path, mock_subprocess_run: Mock
|
||||
) -> None:
|
||||
"""Test that failed git commands raise GitCommandError."""
|
||||
repo_dir = tmp_path / "test_repo"
|
||||
|
||||
# Configure mock to return non-zero exit code
|
||||
mock_subprocess_run.return_value = Mock(
|
||||
returncode=1,
|
||||
stdout=b"",
|
||||
stderr=b"fatal: not a git repository",
|
||||
)
|
||||
|
||||
with pytest.raises(GitCommandError, match="not a git repository"):
|
||||
git.run_git_command(["git", "status"], git_dir=repo_dir)
|
||||
|
||||
|
||||
def test_run_git_command_strips_fatal_prefix(
|
||||
tmp_path: Path, mock_subprocess_run: Mock
|
||||
) -> None:
|
||||
"""Test that 'fatal: ' prefix is stripped from error messages."""
|
||||
repo_dir = tmp_path / "test_repo"
|
||||
|
||||
# Configure mock to return error with "fatal: " prefix
|
||||
mock_subprocess_run.return_value = Mock(
|
||||
returncode=128,
|
||||
stdout=b"",
|
||||
stderr=b"fatal: repository not found\n",
|
||||
)
|
||||
|
||||
with pytest.raises(GitCommandError) as exc_info:
|
||||
git.run_git_command(["git", "clone", "invalid-url"], git_dir=repo_dir)
|
||||
|
||||
# Error message should NOT include "fatal: " prefix
|
||||
assert "fatal:" not in str(exc_info.value)
|
||||
assert "repository not found" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_run_git_command_without_git_dir(mock_subprocess_run: Mock) -> None:
|
||||
"""Test that run_git_command works without git_dir (clone case)."""
|
||||
# Configure mock to return success
|
||||
mock_subprocess_run.return_value = Mock(
|
||||
returncode=0,
|
||||
stdout=b"Cloning into 'test_repo'...",
|
||||
stderr=b"",
|
||||
)
|
||||
|
||||
result = git.run_git_command(["git", "clone", "https://github.com/test/repo"])
|
||||
|
||||
# Verify subprocess.run was called
|
||||
assert mock_subprocess_run.called
|
||||
call_args = mock_subprocess_run.call_args
|
||||
|
||||
# Verify environment does NOT have GIT_DIR or GIT_WORK_TREE set
|
||||
# (it should use the default environment or None)
|
||||
env = call_args[1].get("env")
|
||||
if env is not None:
|
||||
assert "GIT_DIR" not in env
|
||||
assert "GIT_WORK_TREE" not in env
|
||||
|
||||
# Verify cwd is None (default)
|
||||
assert call_args[1].get("cwd") is None
|
||||
|
||||
assert result == "Cloning into 'test_repo'..."
|
||||
|
||||
|
||||
def test_run_git_command_without_git_dir_raises_error(
|
||||
mock_subprocess_run: Mock,
|
||||
) -> None:
|
||||
"""Test that run_git_command without git_dir can still raise errors."""
|
||||
# Configure mock to return error
|
||||
mock_subprocess_run.return_value = Mock(
|
||||
returncode=128,
|
||||
stdout=b"",
|
||||
stderr=b"fatal: repository not found\n",
|
||||
)
|
||||
|
||||
with pytest.raises(GitCommandError, match="repository not found"):
|
||||
git.run_git_command(["git", "clone", "https://invalid.url/repo.git"])
|
||||
|
||||
|
||||
def test_clone_or_update_with_never_refresh(
|
||||
@@ -17,16 +208,10 @@ def test_clone_or_update_with_never_refresh(
|
||||
# Set up CORE.config_path so data_dir uses tmp_path
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
|
||||
# Compute the expected repo directory path
|
||||
url = "https://github.com/test/repo"
|
||||
ref = None
|
||||
key = f"{url}@{ref}"
|
||||
domain = "test"
|
||||
|
||||
# Compute hash-based directory name (matching _compute_destination_path logic)
|
||||
h = hashlib.new("sha256")
|
||||
h.update(key.encode())
|
||||
repo_dir = tmp_path / ".esphome" / domain / h.hexdigest()[:8]
|
||||
repo_dir = _compute_repo_dir(url, ref, domain)
|
||||
|
||||
# Create the git repo directory structure
|
||||
repo_dir.mkdir(parents=True)
|
||||
@@ -58,16 +243,10 @@ def test_clone_or_update_with_refresh_updates_old_repo(
|
||||
# Set up CORE.config_path so data_dir uses tmp_path
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
|
||||
# Compute the expected repo directory path
|
||||
url = "https://github.com/test/repo"
|
||||
ref = None
|
||||
key = f"{url}@{ref}"
|
||||
domain = "test"
|
||||
|
||||
# Compute hash-based directory name (matching _compute_destination_path logic)
|
||||
h = hashlib.new("sha256")
|
||||
h.update(key.encode())
|
||||
repo_dir = tmp_path / ".esphome" / domain / h.hexdigest()[:8]
|
||||
repo_dir = _compute_repo_dir(url, ref, domain)
|
||||
|
||||
# Create the git repo directory structure
|
||||
repo_dir.mkdir(parents=True)
|
||||
@@ -112,16 +291,10 @@ def test_clone_or_update_with_refresh_skips_fresh_repo(
|
||||
# Set up CORE.config_path so data_dir uses tmp_path
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
|
||||
# Compute the expected repo directory path
|
||||
url = "https://github.com/test/repo"
|
||||
ref = None
|
||||
key = f"{url}@{ref}"
|
||||
domain = "test"
|
||||
|
||||
# Compute hash-based directory name (matching _compute_destination_path logic)
|
||||
h = hashlib.new("sha256")
|
||||
h.update(key.encode())
|
||||
repo_dir = tmp_path / ".esphome" / domain / h.hexdigest()[:8]
|
||||
repo_dir = _compute_repo_dir(url, ref, domain)
|
||||
|
||||
# Create the git repo directory structure
|
||||
repo_dir.mkdir(parents=True)
|
||||
@@ -158,16 +331,10 @@ def test_clone_or_update_clones_missing_repo(
|
||||
# Set up CORE.config_path so data_dir uses tmp_path
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
|
||||
# Compute the expected repo directory path
|
||||
url = "https://github.com/test/repo"
|
||||
ref = None
|
||||
key = f"{url}@{ref}"
|
||||
domain = "test"
|
||||
|
||||
# Compute hash-based directory name (matching _compute_destination_path logic)
|
||||
h = hashlib.new("sha256")
|
||||
h.update(key.encode())
|
||||
repo_dir = tmp_path / ".esphome" / domain / h.hexdigest()[:8]
|
||||
repo_dir = _compute_repo_dir(url, ref, domain)
|
||||
|
||||
# Create base directory but NOT the repo itself
|
||||
base_dir = tmp_path / ".esphome" / domain
|
||||
@@ -200,16 +367,10 @@ def test_clone_or_update_with_none_refresh_always_updates(
|
||||
# Set up CORE.config_path so data_dir uses tmp_path
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
|
||||
# Compute the expected repo directory path
|
||||
url = "https://github.com/test/repo"
|
||||
ref = None
|
||||
key = f"{url}@{ref}"
|
||||
domain = "test"
|
||||
|
||||
# Compute hash-based directory name (matching _compute_destination_path logic)
|
||||
h = hashlib.new("sha256")
|
||||
h.update(key.encode())
|
||||
repo_dir = tmp_path / ".esphome" / domain / h.hexdigest()[:8]
|
||||
repo_dir = _compute_repo_dir(url, ref, domain)
|
||||
|
||||
# Create the git repo directory structure
|
||||
repo_dir.mkdir(parents=True)
|
||||
@@ -244,3 +405,269 @@ def test_clone_or_update_with_none_refresh_always_updates(
|
||||
if len(call[0]) > 0 and "fetch" in call[0][0]
|
||||
]
|
||||
assert len(fetch_calls) > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fail_command", "error_message"),
|
||||
[
|
||||
(
|
||||
"rev-parse",
|
||||
"ambiguous argument 'HEAD': unknown revision or path not in the working tree.",
|
||||
),
|
||||
("stash", "fatal: unable to write new index file"),
|
||||
(
|
||||
"fetch",
|
||||
"fatal: unable to access 'https://github.com/test/repo/': Could not resolve host",
|
||||
),
|
||||
("reset", "fatal: Could not reset index file to revision 'FETCH_HEAD'"),
|
||||
],
|
||||
)
|
||||
def test_clone_or_update_recovers_from_git_failures(
|
||||
tmp_path: Path, mock_run_git_command: Mock, fail_command: str, error_message: str
|
||||
) -> None:
|
||||
"""Test that repos are re-cloned when various git commands fail."""
|
||||
# Set up CORE.config_path so data_dir uses tmp_path
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
|
||||
url = "https://github.com/test/repo"
|
||||
ref = "main"
|
||||
domain = "test"
|
||||
repo_dir = _compute_repo_dir(url, ref, domain)
|
||||
|
||||
# Use helper to set up old repo
|
||||
_setup_old_repo(repo_dir)
|
||||
|
||||
# Track command call counts to make first call fail, subsequent calls succeed
|
||||
call_counts: dict[str, int] = {}
|
||||
|
||||
def git_command_side_effect(
|
||||
cmd: list[str], cwd: str | None = None, **kwargs: Any
|
||||
) -> str:
|
||||
# Determine which command this is
|
||||
cmd_type = _get_git_command_type(cmd)
|
||||
|
||||
# Track call count for this command type
|
||||
if cmd_type:
|
||||
call_counts[cmd_type] = call_counts.get(cmd_type, 0) + 1
|
||||
|
||||
# Fail on first call to the specified command, succeed on subsequent calls
|
||||
if cmd_type == fail_command and call_counts[cmd_type] == 1:
|
||||
raise GitCommandError(error_message)
|
||||
|
||||
# Default successful responses
|
||||
if cmd_type == "rev-parse":
|
||||
return "abc123"
|
||||
return ""
|
||||
|
||||
mock_run_git_command.side_effect = git_command_side_effect
|
||||
|
||||
refresh = TimePeriodSeconds(days=1)
|
||||
result_dir, revert = git.clone_or_update(
|
||||
url=url,
|
||||
ref=ref,
|
||||
refresh=refresh,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
# Verify recovery happened
|
||||
call_list = mock_run_git_command.call_args_list
|
||||
|
||||
# Should have attempted the failing command
|
||||
assert any(fail_command in str(c) for c in call_list)
|
||||
|
||||
# Should have called clone for recovery
|
||||
assert any("clone" in str(c) for c in call_list)
|
||||
|
||||
# Verify the repo directory path is returned
|
||||
assert result_dir == repo_dir
|
||||
|
||||
|
||||
def test_clone_or_update_fails_when_recovery_also_fails(
|
||||
tmp_path: Path, mock_run_git_command: Mock
|
||||
) -> None:
|
||||
"""Test that we don't infinitely recurse when recovery also fails."""
|
||||
# Set up CORE.config_path so data_dir uses tmp_path
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
|
||||
url = "https://github.com/test/repo"
|
||||
ref = "main"
|
||||
domain = "test"
|
||||
repo_dir = _compute_repo_dir(url, ref, domain)
|
||||
|
||||
# Use helper to set up old repo
|
||||
_setup_old_repo(repo_dir)
|
||||
|
||||
# Mock git command to fail on clone (simulating network failure during recovery)
|
||||
def git_command_side_effect(
|
||||
cmd: list[str], cwd: str | None = None, **kwargs: Any
|
||||
) -> str:
|
||||
cmd_type = _get_git_command_type(cmd)
|
||||
if cmd_type == "rev-parse":
|
||||
# First time fails (broken repo)
|
||||
raise GitCommandError(
|
||||
"ambiguous argument 'HEAD': unknown revision or path not in the working tree."
|
||||
)
|
||||
if cmd_type == "clone":
|
||||
# Clone also fails (recovery fails)
|
||||
raise GitCommandError("fatal: unable to access repository")
|
||||
return ""
|
||||
|
||||
mock_run_git_command.side_effect = git_command_side_effect
|
||||
|
||||
refresh = TimePeriodSeconds(days=1)
|
||||
|
||||
# Should raise after one recovery attempt fails
|
||||
with pytest.raises(GitCommandError, match="fatal: unable to access repository"):
|
||||
git.clone_or_update(
|
||||
url=url,
|
||||
ref=ref,
|
||||
refresh=refresh,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
# Verify we only tried to clone once (no infinite recursion)
|
||||
call_list = mock_run_git_command.call_args_list
|
||||
clone_calls = [c for c in call_list if "clone" in c[0][0]]
|
||||
# Should have exactly one clone call (the recovery attempt that failed)
|
||||
assert len(clone_calls) == 1
|
||||
# Should have tried rev-parse once (which failed and triggered recovery)
|
||||
rev_parse_calls = [c for c in call_list if "rev-parse" in c[0][0]]
|
||||
assert len(rev_parse_calls) == 1
|
||||
|
||||
|
||||
def test_clone_or_update_recover_broken_flag_prevents_second_recovery(
|
||||
tmp_path: Path, mock_run_git_command: Mock
|
||||
) -> None:
|
||||
"""Test that _recover_broken=False prevents a second recovery attempt (tests the raise path)."""
|
||||
# Set up CORE.config_path so data_dir uses tmp_path
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
|
||||
url = "https://github.com/test/repo"
|
||||
ref = "main"
|
||||
domain = "test"
|
||||
repo_dir = _compute_repo_dir(url, ref, domain)
|
||||
|
||||
# Use helper to set up old repo
|
||||
_setup_old_repo(repo_dir)
|
||||
|
||||
# Track fetch calls to differentiate between first (in clone) and second (in recovery update)
|
||||
call_counts: dict[str, int] = {}
|
||||
|
||||
# Mock git command to fail on fetch during recovery's ref checkout
|
||||
def git_command_side_effect(
|
||||
cmd: list[str], cwd: str | None = None, **kwargs: Any
|
||||
) -> str:
|
||||
cmd_type = _get_git_command_type(cmd)
|
||||
|
||||
if cmd_type:
|
||||
call_counts[cmd_type] = call_counts.get(cmd_type, 0) + 1
|
||||
|
||||
# First attempt: rev-parse fails (broken repo)
|
||||
if cmd_type == "rev-parse" and call_counts[cmd_type] == 1:
|
||||
raise GitCommandError(
|
||||
"ambiguous argument 'HEAD': unknown revision or path not in the working tree."
|
||||
)
|
||||
|
||||
# Recovery: clone succeeds
|
||||
if cmd_type == "clone":
|
||||
return ""
|
||||
|
||||
# Recovery: fetch for ref checkout fails
|
||||
# This happens in the clone path when ref is not None (line 80 in git.py)
|
||||
if cmd_type == "fetch" and call_counts[cmd_type] == 1:
|
||||
raise GitCommandError("fatal: couldn't find remote ref main")
|
||||
|
||||
# Default success
|
||||
return "abc123" if cmd_type == "rev-parse" else ""
|
||||
|
||||
mock_run_git_command.side_effect = git_command_side_effect
|
||||
|
||||
refresh = TimePeriodSeconds(days=1)
|
||||
|
||||
# Should raise on the fetch during recovery (when _recover_broken=False)
|
||||
# This tests the critical "if not _recover_broken: raise" path
|
||||
with pytest.raises(GitCommandError, match="fatal: couldn't find remote ref main"):
|
||||
git.clone_or_update(
|
||||
url=url,
|
||||
ref=ref,
|
||||
refresh=refresh,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
# Verify the sequence of events
|
||||
call_list = mock_run_git_command.call_args_list
|
||||
|
||||
# Should have: rev-parse (fail, triggers recovery), clone (success),
|
||||
# fetch (fail during ref checkout, raises because _recover_broken=False)
|
||||
rev_parse_calls = [c for c in call_list if "rev-parse" in c[0][0]]
|
||||
# Should have exactly one rev-parse call that failed
|
||||
assert len(rev_parse_calls) == 1
|
||||
|
||||
clone_calls = [c for c in call_list if "clone" in c[0][0]]
|
||||
# Should have exactly one clone call (the recovery attempt)
|
||||
assert len(clone_calls) == 1
|
||||
|
||||
fetch_calls = [c for c in call_list if "fetch" in c[0][0]]
|
||||
# Should have exactly one fetch call that failed (during ref checkout in recovery)
|
||||
assert len(fetch_calls) == 1
|
||||
|
||||
|
||||
def test_clone_or_update_recover_broken_flag_prevents_infinite_loop(
|
||||
tmp_path: Path, mock_run_git_command: Mock
|
||||
) -> None:
|
||||
"""Test that _recover_broken=False prevents infinite recursion when repo persists."""
|
||||
# This tests the critical "if not _recover_broken: raise" path at line 124-125
|
||||
# Set up CORE.config_path so data_dir uses tmp_path
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
|
||||
url = "https://github.com/test/repo"
|
||||
ref = "main"
|
||||
domain = "test"
|
||||
repo_dir = _compute_repo_dir(url, ref, domain)
|
||||
|
||||
# Use helper to set up old repo
|
||||
_setup_old_repo(repo_dir)
|
||||
|
||||
# Mock shutil.rmtree to NOT actually delete the directory
|
||||
# This simulates a scenario where deletion fails (permissions, etc.)
|
||||
import unittest.mock
|
||||
|
||||
def mock_rmtree(path, *args, **kwargs):
|
||||
# Don't actually delete - this causes the recursive call to still see the repo
|
||||
pass
|
||||
|
||||
# Mock git commands to always fail on stash
|
||||
def git_command_side_effect(
|
||||
cmd: list[str], cwd: str | None = None, **kwargs: Any
|
||||
) -> str:
|
||||
cmd_type = _get_git_command_type(cmd)
|
||||
if cmd_type == "rev-parse":
|
||||
return "abc123"
|
||||
if cmd_type == "stash":
|
||||
# Always fails
|
||||
raise GitCommandError("fatal: unable to write new index file")
|
||||
return ""
|
||||
|
||||
mock_run_git_command.side_effect = git_command_side_effect
|
||||
|
||||
refresh = TimePeriodSeconds(days=1)
|
||||
|
||||
# Mock shutil.rmtree and test
|
||||
# Should raise on the second attempt when _recover_broken=False
|
||||
# This hits the "if not _recover_broken: raise" path
|
||||
with (
|
||||
unittest.mock.patch("esphome.git.shutil.rmtree", side_effect=mock_rmtree),
|
||||
pytest.raises(GitCommandError, match="fatal: unable to write new index file"),
|
||||
):
|
||||
git.clone_or_update(
|
||||
url=url,
|
||||
ref=ref,
|
||||
refresh=refresh,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
# Verify the sequence: stash fails twice (once triggering recovery, once raising)
|
||||
call_list = mock_run_git_command.call_args_list
|
||||
stash_calls = [c for c in call_list if "stash" in c[0][0]]
|
||||
# Should have exactly two stash calls
|
||||
assert len(stash_calls) == 2
|
||||
|
||||
@@ -1062,7 +1062,7 @@ def test_upload_program_ota_with_file_arg(
|
||||
assert exit_code == 0
|
||||
assert host == "192.168.1.100"
|
||||
mock_run_ota.assert_called_once_with(
|
||||
["192.168.1.100"], 3232, "", Path("custom.bin")
|
||||
["192.168.1.100"], 3232, None, Path("custom.bin")
|
||||
)
|
||||
|
||||
|
||||
@@ -1119,7 +1119,9 @@ def test_upload_program_ota_with_mqtt_resolution(
|
||||
expected_firmware = (
|
||||
tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
|
||||
)
|
||||
mock_run_ota.assert_called_once_with(["192.168.1.100"], 3232, "", expected_firmware)
|
||||
mock_run_ota.assert_called_once_with(
|
||||
["192.168.1.100"], 3232, None, expected_firmware
|
||||
)
|
||||
|
||||
|
||||
@patch("esphome.__main__.importlib.import_module")
|
||||
@@ -1976,3 +1978,292 @@ def test_command_clean_all_args_used() -> None:
|
||||
# Verify the correct configuration paths were passed
|
||||
mock_clean_all.assert_any_call(["/path/to/config1"])
|
||||
mock_clean_all.assert_any_call(["/path/to/config2", "/path/to/config3"])
|
||||
|
||||
|
||||
def test_upload_program_ota_static_ip_with_mqttip(
|
||||
mock_mqtt_get_ip: Mock,
|
||||
mock_run_ota: Mock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test upload_program with static IP and MQTTIP (issue #11260).
|
||||
|
||||
This tests the scenario where a device has manual_ip (static IP) configured
|
||||
and MQTT is also configured. The devices list contains both the static IP
|
||||
and "MQTTIP" magic string. This previously failed because only the first
|
||||
device was checked for MQTT resolution.
|
||||
"""
|
||||
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path)
|
||||
|
||||
mock_mqtt_get_ip.return_value = ["192.168.2.50"] # Different subnet
|
||||
mock_run_ota.return_value = (0, "192.168.1.100")
|
||||
|
||||
config = {
|
||||
CONF_OTA: [
|
||||
{
|
||||
CONF_PLATFORM: CONF_ESPHOME,
|
||||
CONF_PORT: 3232,
|
||||
}
|
||||
],
|
||||
CONF_MQTT: {
|
||||
CONF_BROKER: "mqtt.local",
|
||||
},
|
||||
}
|
||||
args = MockArgs(username="user", password="pass", client_id="client")
|
||||
# Simulates choose_upload_log_host returning static IP + MQTTIP
|
||||
devices = ["192.168.1.100", "MQTTIP"]
|
||||
|
||||
exit_code, host = upload_program(config, args, devices)
|
||||
|
||||
assert exit_code == 0
|
||||
assert host == "192.168.1.100"
|
||||
|
||||
# Verify MQTT was resolved
|
||||
mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client")
|
||||
|
||||
# Verify espota2.run_ota was called with both IPs
|
||||
expected_firmware = (
|
||||
tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
|
||||
)
|
||||
mock_run_ota.assert_called_once_with(
|
||||
["192.168.1.100", "192.168.2.50"], 3232, None, expected_firmware
|
||||
)
|
||||
|
||||
|
||||
def test_upload_program_ota_multiple_mqttip_resolves_once(
|
||||
mock_mqtt_get_ip: Mock,
|
||||
mock_run_ota: Mock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that MQTT resolution only happens once even with multiple MQTT magic strings."""
|
||||
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path)
|
||||
|
||||
mock_mqtt_get_ip.return_value = ["192.168.2.50", "192.168.2.51"]
|
||||
mock_run_ota.return_value = (0, "192.168.2.50")
|
||||
|
||||
config = {
|
||||
CONF_OTA: [
|
||||
{
|
||||
CONF_PLATFORM: CONF_ESPHOME,
|
||||
CONF_PORT: 3232,
|
||||
}
|
||||
],
|
||||
CONF_MQTT: {
|
||||
CONF_BROKER: "mqtt.local",
|
||||
},
|
||||
}
|
||||
args = MockArgs(username="user", password="pass", client_id="client")
|
||||
# Multiple MQTT magic strings in the list
|
||||
devices = ["MQTTIP", "MQTT", "192.168.1.100"]
|
||||
|
||||
exit_code, host = upload_program(config, args, devices)
|
||||
|
||||
assert exit_code == 0
|
||||
assert host == "192.168.2.50"
|
||||
|
||||
# Verify MQTT was only resolved once despite multiple MQTT magic strings
|
||||
mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client")
|
||||
|
||||
# Verify espota2.run_ota was called with all unique IPs
|
||||
expected_firmware = (
|
||||
tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
|
||||
)
|
||||
mock_run_ota.assert_called_once_with(
|
||||
["192.168.2.50", "192.168.2.51", "192.168.1.100"], 3232, None, expected_firmware
|
||||
)
|
||||
|
||||
|
||||
def test_upload_program_ota_mqttip_deduplication(
|
||||
mock_mqtt_get_ip: Mock,
|
||||
mock_run_ota: Mock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that duplicate IPs are filtered when MQTT returns same IP as static IP."""
|
||||
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path)
|
||||
|
||||
# MQTT returns the same IP as the static IP
|
||||
mock_mqtt_get_ip.return_value = ["192.168.1.100"]
|
||||
mock_run_ota.return_value = (0, "192.168.1.100")
|
||||
|
||||
config = {
|
||||
CONF_OTA: [
|
||||
{
|
||||
CONF_PLATFORM: CONF_ESPHOME,
|
||||
CONF_PORT: 3232,
|
||||
}
|
||||
],
|
||||
CONF_MQTT: {
|
||||
CONF_BROKER: "mqtt.local",
|
||||
},
|
||||
}
|
||||
args = MockArgs(username="user", password="pass", client_id="client")
|
||||
devices = ["192.168.1.100", "MQTTIP"]
|
||||
|
||||
exit_code, host = upload_program(config, args, devices)
|
||||
|
||||
assert exit_code == 0
|
||||
assert host == "192.168.1.100"
|
||||
|
||||
# Verify MQTT was resolved
|
||||
mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client")
|
||||
|
||||
# Verify espota2.run_ota was called with deduplicated IPs (only one instance of 192.168.1.100)
|
||||
# Note: Current implementation doesn't dedupe, so we'll get the IP twice
|
||||
# This test documents current behavior - deduplication could be future enhancement
|
||||
mock_run_ota.assert_called_once()
|
||||
call_args = mock_run_ota.call_args[0]
|
||||
# Should contain both the original IP and MQTT-resolved IP (even if duplicate)
|
||||
assert "192.168.1.100" in call_args[0]
|
||||
|
||||
|
||||
@patch("esphome.components.api.client.run_logs")
|
||||
def test_show_logs_api_static_ip_with_mqttip(
|
||||
mock_run_logs: Mock,
|
||||
mock_mqtt_get_ip: Mock,
|
||||
) -> None:
|
||||
"""Test show_logs with static IP and MQTTIP (issue #11260).
|
||||
|
||||
This tests the scenario where a device has manual_ip (static IP) configured
|
||||
and MQTT is also configured. The devices list contains both the static IP
|
||||
and "MQTTIP" magic string.
|
||||
"""
|
||||
setup_core(
|
||||
config={
|
||||
"logger": {},
|
||||
CONF_API: {},
|
||||
CONF_MQTT: {CONF_BROKER: "mqtt.local"},
|
||||
},
|
||||
platform=PLATFORM_ESP32,
|
||||
)
|
||||
mock_run_logs.return_value = 0
|
||||
mock_mqtt_get_ip.return_value = ["192.168.2.50"]
|
||||
|
||||
args = MockArgs(username="user", password="pass", client_id="client")
|
||||
# Simulates choose_upload_log_host returning static IP + MQTTIP
|
||||
devices = ["192.168.1.100", "MQTTIP"]
|
||||
|
||||
result = show_logs(CORE.config, args, devices)
|
||||
|
||||
assert result == 0
|
||||
|
||||
# Verify MQTT was resolved
|
||||
mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client")
|
||||
|
||||
# Verify run_logs was called with both IPs
|
||||
mock_run_logs.assert_called_once_with(
|
||||
CORE.config, ["192.168.1.100", "192.168.2.50"]
|
||||
)
|
||||
|
||||
|
||||
@patch("esphome.components.api.client.run_logs")
|
||||
def test_show_logs_api_multiple_mqttip_resolves_once(
|
||||
mock_run_logs: Mock,
|
||||
mock_mqtt_get_ip: Mock,
|
||||
) -> None:
|
||||
"""Test that MQTT resolution only happens once for show_logs with multiple MQTT magic strings."""
|
||||
setup_core(
|
||||
config={
|
||||
"logger": {},
|
||||
CONF_API: {},
|
||||
CONF_MQTT: {CONF_BROKER: "mqtt.local"},
|
||||
},
|
||||
platform=PLATFORM_ESP32,
|
||||
)
|
||||
mock_run_logs.return_value = 0
|
||||
mock_mqtt_get_ip.return_value = ["192.168.2.50", "192.168.2.51"]
|
||||
|
||||
args = MockArgs(username="user", password="pass", client_id="client")
|
||||
# Multiple MQTT magic strings in the list
|
||||
devices = ["MQTTIP", "192.168.1.100", "MQTT"]
|
||||
|
||||
result = show_logs(CORE.config, args, devices)
|
||||
|
||||
assert result == 0
|
||||
|
||||
# Verify MQTT was only resolved once despite multiple MQTT magic strings
|
||||
mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client")
|
||||
|
||||
# Verify run_logs was called with all unique IPs (MQTT strings replaced with IPs)
|
||||
# Note: "MQTT" is a different magic string from "MQTTIP", but both trigger MQTT resolution
|
||||
# The _resolve_network_devices helper filters out both after first resolution
|
||||
mock_run_logs.assert_called_once_with(
|
||||
CORE.config, ["192.168.2.50", "192.168.2.51", "192.168.1.100"]
|
||||
)
|
||||
|
||||
|
||||
def test_upload_program_ota_mqtt_timeout_fallback(
|
||||
mock_mqtt_get_ip: Mock,
|
||||
mock_run_ota: Mock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test upload_program falls back to other devices when MQTT times out."""
|
||||
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path)
|
||||
|
||||
# MQTT times out
|
||||
mock_mqtt_get_ip.side_effect = EsphomeError("Failed to find IP via MQTT")
|
||||
mock_run_ota.return_value = (0, "192.168.1.100")
|
||||
|
||||
config = {
|
||||
CONF_OTA: [
|
||||
{
|
||||
CONF_PLATFORM: CONF_ESPHOME,
|
||||
CONF_PORT: 3232,
|
||||
}
|
||||
],
|
||||
CONF_MQTT: {
|
||||
CONF_BROKER: "mqtt.local",
|
||||
},
|
||||
}
|
||||
args = MockArgs(username="user", password="pass", client_id="client")
|
||||
# Static IP first, MQTTIP second
|
||||
devices = ["192.168.1.100", "MQTTIP"]
|
||||
|
||||
exit_code, host = upload_program(config, args, devices)
|
||||
|
||||
# Should succeed using the static IP even though MQTT failed
|
||||
assert exit_code == 0
|
||||
assert host == "192.168.1.100"
|
||||
|
||||
# Verify MQTT was attempted
|
||||
mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client")
|
||||
|
||||
# Verify espota2.run_ota was called with only the static IP (MQTT failed)
|
||||
expected_firmware = (
|
||||
tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
|
||||
)
|
||||
mock_run_ota.assert_called_once_with(
|
||||
["192.168.1.100"], 3232, None, expected_firmware
|
||||
)
|
||||
|
||||
|
||||
@patch("esphome.components.api.client.run_logs")
|
||||
def test_show_logs_api_mqtt_timeout_fallback(
|
||||
mock_run_logs: Mock,
|
||||
mock_mqtt_get_ip: Mock,
|
||||
) -> None:
|
||||
"""Test show_logs falls back to other devices when MQTT times out."""
|
||||
setup_core(
|
||||
config={
|
||||
"logger": {},
|
||||
CONF_API: {},
|
||||
CONF_MQTT: {CONF_BROKER: "mqtt.local"},
|
||||
},
|
||||
platform=PLATFORM_ESP32,
|
||||
)
|
||||
mock_run_logs.return_value = 0
|
||||
# MQTT times out
|
||||
mock_mqtt_get_ip.side_effect = EsphomeError("Failed to find IP via MQTT")
|
||||
|
||||
args = MockArgs(username="user", password="pass", client_id="client")
|
||||
# Static IP first, MQTTIP second
|
||||
devices = ["192.168.1.100", "MQTTIP"]
|
||||
|
||||
result = show_logs(CORE.config, args, devices)
|
||||
|
||||
# Should succeed using the static IP even though MQTT failed
|
||||
assert result == 0
|
||||
|
||||
# Verify MQTT was attempted
|
||||
mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client")
|
||||
|
||||
# Verify run_logs was called with only the static IP (MQTT failed)
|
||||
mock_run_logs.assert_called_once_with(CORE.config, ["192.168.1.100"])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user