mirror of
https://github.com/esphome/esphome.git
synced 2025-10-22 11:43:51 +01:00
Merge branch 'dev' into light_bitmask
This commit is contained in:
@@ -1058,7 +1058,8 @@ class DownloadBinaryRequestHandler(BaseHandler):
|
|||||||
"download",
|
"download",
|
||||||
f"{storage_json.name}-{file_name}",
|
f"{storage_json.name}-{file_name}",
|
||||||
)
|
)
|
||||||
path = storage_json.firmware_bin_path.with_name(file_name)
|
|
||||||
|
path = storage_json.firmware_bin_path.parent.joinpath(file_name)
|
||||||
|
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
args = ["esphome", "idedata", settings.rel_path(configuration)]
|
args = ["esphome", "idedata", settings.rel_path(configuration)]
|
||||||
|
@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
|||||||
esptool==5.1.0
|
esptool==5.1.0
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
esphome-dashboard==20251013.0
|
esphome-dashboard==20251013.0
|
||||||
aioesphomeapi==42.0.0
|
aioesphomeapi==42.1.0
|
||||||
zeroconf==0.148.0
|
zeroconf==0.148.0
|
||||||
puremagic==1.30
|
puremagic==1.30
|
||||||
ruamel.yaml==0.18.15 # dashboard_import
|
ruamel.yaml==0.18.15 # dashboard_import
|
||||||
|
@@ -8,14 +8,12 @@ sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return 0.6;
|
return 0.6;
|
||||||
} else {
|
|
||||||
return 0.0;
|
|
||||||
}
|
}
|
||||||
|
return 0.0;
|
||||||
- platform: template
|
- platform: template
|
||||||
id: template_temperature
|
id: template_temperature
|
||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return 42.0;
|
return 42.0;
|
||||||
} else {
|
|
||||||
return 0.0;
|
|
||||||
}
|
}
|
||||||
|
return 0.0;
|
||||||
|
@@ -5,9 +5,8 @@ sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return 42.0;
|
return 42.0;
|
||||||
} else {
|
|
||||||
return 0.0;
|
|
||||||
}
|
}
|
||||||
|
return 0.0;
|
||||||
update_interval: 15s
|
update_interval: 15s
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
|
@@ -23,9 +23,8 @@ binary_sensor:
|
|||||||
- lambda: |-
|
- lambda: |-
|
||||||
if (id(some_binary_sensor).state) {
|
if (id(some_binary_sensor).state) {
|
||||||
return x;
|
return x;
|
||||||
} else {
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
- settle: 100ms
|
- settle: 100ms
|
||||||
- timeout: 10s
|
- timeout: 10s
|
||||||
|
|
||||||
|
@@ -4,25 +4,22 @@ binary_sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
- platform: template
|
- platform: template
|
||||||
id: bin2
|
id: bin2
|
||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 20000) {
|
if (millis() > 20000) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
- platform: template
|
- platform: template
|
||||||
id: bin3
|
id: bin3
|
||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 30000) {
|
if (millis() > 30000) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
|
||||||
sensor:
|
sensor:
|
||||||
- platform: binary_sensor_map
|
- platform: binary_sensor_map
|
||||||
|
@@ -4,17 +4,15 @@ sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return 0.6;
|
return 0.6;
|
||||||
} else {
|
|
||||||
return 0.0;
|
|
||||||
}
|
}
|
||||||
|
return 0.0;
|
||||||
- platform: template
|
- platform: template
|
||||||
id: template_temperature2
|
id: template_temperature2
|
||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 20000) {
|
if (millis() > 20000) {
|
||||||
return 0.8;
|
return 0.8;
|
||||||
} else {
|
|
||||||
return 0.0;
|
|
||||||
}
|
}
|
||||||
|
return 0.0;
|
||||||
- platform: combination
|
- platform: combination
|
||||||
type: kalman
|
type: kalman
|
||||||
name: Kalman-filtered temperature
|
name: Kalman-filtered temperature
|
||||||
|
@@ -4,9 +4,8 @@ binary_sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
|
||||||
sensor:
|
sensor:
|
||||||
- platform: duty_time
|
- platform: duty_time
|
||||||
|
@@ -4,9 +4,8 @@ binary_sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
|
||||||
switch:
|
switch:
|
||||||
- platform: template
|
- platform: template
|
||||||
|
@@ -17,9 +17,8 @@ lock:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return LOCK_STATE_LOCKED;
|
return LOCK_STATE_LOCKED;
|
||||||
} else {
|
|
||||||
return LOCK_STATE_UNLOCKED;
|
|
||||||
}
|
}
|
||||||
|
return LOCK_STATE_UNLOCKED;
|
||||||
optimistic: true
|
optimistic: true
|
||||||
assumed_state: false
|
assumed_state: false
|
||||||
on_unlock:
|
on_unlock:
|
||||||
|
@@ -72,10 +72,9 @@ binary_sensor:
|
|||||||
if (id(template_sens).state > 30) {
|
if (id(template_sens).state > 30) {
|
||||||
// Garage Door is open.
|
// Garage Door is open.
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
// Garage Door is closed.
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
// Garage Door is closed.
|
||||||
|
return false;
|
||||||
on_state:
|
on_state:
|
||||||
- mqtt.publish:
|
- mqtt.publish:
|
||||||
topic: some/topic/binary_sensor
|
topic: some/topic/binary_sensor
|
||||||
@@ -217,9 +216,8 @@ cover:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (id(some_binary_sensor).state) {
|
if (id(some_binary_sensor).state) {
|
||||||
return COVER_OPEN;
|
return COVER_OPEN;
|
||||||
} else {
|
|
||||||
return COVER_CLOSED;
|
|
||||||
}
|
}
|
||||||
|
return COVER_CLOSED;
|
||||||
open_action:
|
open_action:
|
||||||
- logger.log: open_action
|
- logger.log: open_action
|
||||||
close_action:
|
close_action:
|
||||||
@@ -321,9 +319,8 @@ lock:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (id(some_binary_sensor).state) {
|
if (id(some_binary_sensor).state) {
|
||||||
return LOCK_STATE_LOCKED;
|
return LOCK_STATE_LOCKED;
|
||||||
} else {
|
|
||||||
return LOCK_STATE_UNLOCKED;
|
|
||||||
}
|
}
|
||||||
|
return LOCK_STATE_UNLOCKED;
|
||||||
lock_action:
|
lock_action:
|
||||||
- logger.log: lock_action
|
- logger.log: lock_action
|
||||||
unlock_action:
|
unlock_action:
|
||||||
@@ -360,9 +357,8 @@ sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (id(some_binary_sensor).state) {
|
if (id(some_binary_sensor).state) {
|
||||||
return 42.0;
|
return 42.0;
|
||||||
} else {
|
|
||||||
return 0.0;
|
|
||||||
}
|
}
|
||||||
|
return 0.0;
|
||||||
update_interval: 60s
|
update_interval: 60s
|
||||||
on_value:
|
on_value:
|
||||||
- mqtt.publish:
|
- mqtt.publish:
|
||||||
@@ -390,9 +386,8 @@ switch:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (id(some_binary_sensor).state) {
|
if (id(some_binary_sensor).state) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
turn_on_action:
|
turn_on_action:
|
||||||
- logger.log: turn_on_action
|
- logger.log: turn_on_action
|
||||||
turn_off_action:
|
turn_off_action:
|
||||||
@@ -436,9 +431,8 @@ valve:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (id(some_binary_sensor).state) {
|
if (id(some_binary_sensor).state) {
|
||||||
return VALVE_OPEN;
|
return VALVE_OPEN;
|
||||||
} else {
|
|
||||||
return VALVE_CLOSED;
|
|
||||||
}
|
}
|
||||||
|
return VALVE_CLOSED;
|
||||||
|
|
||||||
alarm_control_panel:
|
alarm_control_panel:
|
||||||
- platform: template
|
- platform: template
|
||||||
|
@@ -27,9 +27,8 @@ sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return 42.0;
|
return 42.0;
|
||||||
} else {
|
|
||||||
return 0.0;
|
|
||||||
}
|
}
|
||||||
|
return 0.0;
|
||||||
update_interval: 60s
|
update_interval: 60s
|
||||||
|
|
||||||
climate:
|
climate:
|
||||||
|
@@ -35,9 +35,8 @@ sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return 42.0;
|
return 42.0;
|
||||||
} else {
|
|
||||||
return 0.0;
|
|
||||||
}
|
}
|
||||||
|
return 0.0;
|
||||||
update_interval: 60s
|
update_interval: 60s
|
||||||
|
|
||||||
text_sensor:
|
text_sensor:
|
||||||
@@ -49,9 +48,8 @@ text_sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return {"Hello World"};
|
return {"Hello World"};
|
||||||
} else {
|
|
||||||
return {"Goodbye (cruel) World"};
|
|
||||||
}
|
}
|
||||||
|
return {"Goodbye (cruel) World"};
|
||||||
update_interval: 60s
|
update_interval: 60s
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
@@ -60,9 +58,8 @@ binary_sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
|
||||||
switch:
|
switch:
|
||||||
- platform: template
|
- platform: template
|
||||||
@@ -70,9 +67,8 @@ switch:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
optimistic: true
|
optimistic: true
|
||||||
|
|
||||||
fan:
|
fan:
|
||||||
@@ -85,9 +81,8 @@ cover:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return COVER_OPEN;
|
return COVER_OPEN;
|
||||||
} else {
|
|
||||||
return COVER_CLOSED;
|
|
||||||
}
|
}
|
||||||
|
return COVER_CLOSED;
|
||||||
|
|
||||||
lock:
|
lock:
|
||||||
- platform: template
|
- platform: template
|
||||||
@@ -95,9 +90,8 @@ lock:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (millis() > 10000) {
|
if (millis() > 10000) {
|
||||||
return LOCK_STATE_LOCKED;
|
return LOCK_STATE_LOCKED;
|
||||||
} else {
|
|
||||||
return LOCK_STATE_UNLOCKED;
|
|
||||||
}
|
}
|
||||||
|
return LOCK_STATE_UNLOCKED;
|
||||||
optimistic: true
|
optimistic: true
|
||||||
|
|
||||||
select:
|
select:
|
||||||
|
@@ -59,9 +59,8 @@ binary_sensor:
|
|||||||
- lambda: |-
|
- lambda: |-
|
||||||
if (id(other_binary_sensor).state) {
|
if (id(other_binary_sensor).state) {
|
||||||
return x;
|
return x;
|
||||||
} else {
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
- settle: 500ms
|
- settle: 500ms
|
||||||
- timeout: 5s
|
- timeout: 5s
|
||||||
|
|
||||||
@@ -72,9 +71,8 @@ sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (id(some_binary_sensor).state) {
|
if (id(some_binary_sensor).state) {
|
||||||
return 42.0;
|
return 42.0;
|
||||||
} else {
|
|
||||||
return 0.0;
|
|
||||||
}
|
}
|
||||||
|
return 0.0;
|
||||||
update_interval: 60s
|
update_interval: 60s
|
||||||
filters:
|
filters:
|
||||||
- calibrate_linear:
|
- calibrate_linear:
|
||||||
@@ -183,9 +181,8 @@ switch:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (id(some_binary_sensor).state) {
|
if (id(some_binary_sensor).state) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
turn_on_action:
|
turn_on_action:
|
||||||
- logger.log: "turn_on_action"
|
- logger.log: "turn_on_action"
|
||||||
turn_off_action:
|
turn_off_action:
|
||||||
@@ -203,9 +200,8 @@ cover:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (id(some_binary_sensor).state) {
|
if (id(some_binary_sensor).state) {
|
||||||
return COVER_OPEN;
|
return COVER_OPEN;
|
||||||
} else {
|
|
||||||
return COVER_CLOSED;
|
|
||||||
}
|
}
|
||||||
|
return COVER_CLOSED;
|
||||||
open_action:
|
open_action:
|
||||||
- logger.log: open_action
|
- logger.log: open_action
|
||||||
close_action:
|
close_action:
|
||||||
@@ -238,9 +234,8 @@ lock:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (id(some_binary_sensor).state) {
|
if (id(some_binary_sensor).state) {
|
||||||
return LOCK_STATE_LOCKED;
|
return LOCK_STATE_LOCKED;
|
||||||
} else {
|
|
||||||
return LOCK_STATE_UNLOCKED;
|
|
||||||
}
|
}
|
||||||
|
return LOCK_STATE_UNLOCKED;
|
||||||
lock_action:
|
lock_action:
|
||||||
- logger.log: lock_action
|
- logger.log: lock_action
|
||||||
unlock_action:
|
unlock_action:
|
||||||
@@ -255,9 +250,8 @@ valve:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (id(some_binary_sensor).state) {
|
if (id(some_binary_sensor).state) {
|
||||||
return VALVE_OPEN;
|
return VALVE_OPEN;
|
||||||
} else {
|
|
||||||
return VALVE_CLOSED;
|
|
||||||
}
|
}
|
||||||
|
return VALVE_CLOSED;
|
||||||
open_action:
|
open_action:
|
||||||
- logger.log: open_action
|
- logger.log: open_action
|
||||||
close_action:
|
close_action:
|
||||||
|
@@ -17,10 +17,10 @@ sensor:
|
|||||||
name: HLW8012 Voltage
|
name: HLW8012 Voltage
|
||||||
power:
|
power:
|
||||||
name: HLW8012 Power
|
name: HLW8012 Power
|
||||||
id: hlw8012_power
|
id: total_daily_energy_hlw8012_power
|
||||||
energy:
|
energy:
|
||||||
name: HLW8012 Energy
|
name: HLW8012 Energy
|
||||||
id: hlw8012_energy
|
id: total_daily_energy_hlw8012_energy
|
||||||
update_interval: 15s
|
update_interval: 15s
|
||||||
current_resistor: 0.001 ohm
|
current_resistor: 0.001 ohm
|
||||||
voltage_divider: 2351
|
voltage_divider: 2351
|
||||||
@@ -29,4 +29,4 @@ sensor:
|
|||||||
model: hlw8012
|
model: hlw8012
|
||||||
- platform: total_daily_energy
|
- platform: total_daily_energy
|
||||||
name: HLW8012 Total Daily Energy
|
name: HLW8012 Total Daily Energy
|
||||||
power_id: hlw8012_power
|
power_id: total_daily_energy_hlw8012_power
|
||||||
|
@@ -35,6 +35,26 @@ from esphome.zeroconf import DiscoveredImport
|
|||||||
from .common import get_fixture_path
|
from .common import get_fixture_path
|
||||||
|
|
||||||
|
|
||||||
|
def get_build_path(base_path: Path, device_name: str) -> Path:
|
||||||
|
"""Get the build directory path for a device.
|
||||||
|
|
||||||
|
This is a test helper that constructs the standard ESPHome build directory
|
||||||
|
structure. Note: This helper does NOT perform path traversal sanitization
|
||||||
|
because it's only used in tests where we control the inputs. The actual
|
||||||
|
web_server.py code handles sanitization in DownloadBinaryRequestHandler.get()
|
||||||
|
via file_name.replace("..", "").lstrip("/").
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_path: The base temporary path (typically tmp_path from pytest)
|
||||||
|
device_name: The name of the device (should not contain path separators
|
||||||
|
in production use, but tests may use it for specific scenarios)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to the build directory (.esphome/build/device_name)
|
||||||
|
"""
|
||||||
|
return base_path / ".esphome" / "build" / device_name
|
||||||
|
|
||||||
|
|
||||||
class DashboardTestHelper:
|
class DashboardTestHelper:
|
||||||
def __init__(self, io_loop: IOLoop, client: AsyncHTTPClient, port: int) -> None:
|
def __init__(self, io_loop: IOLoop, client: AsyncHTTPClient, port: int) -> None:
|
||||||
self.io_loop = io_loop
|
self.io_loop = io_loop
|
||||||
@@ -417,6 +437,180 @@ async def test_download_binary_handler_idedata_fallback(
|
|||||||
assert response.body == b"bootloader content"
|
assert response.body == b"bootloader content"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.usefixtures("mock_ext_storage_path")
|
||||||
|
async def test_download_binary_handler_subdirectory_file(
|
||||||
|
dashboard: DashboardTestHelper,
|
||||||
|
tmp_path: Path,
|
||||||
|
mock_storage_json: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test the DownloadBinaryRequestHandler.get with file in subdirectory (nRF52 case).
|
||||||
|
|
||||||
|
This is a regression test for issue #11343 where the Path migration broke
|
||||||
|
downloads for nRF52 firmware files in subdirectories like 'zephyr/zephyr.uf2'.
|
||||||
|
|
||||||
|
The issue was that with_name() doesn't accept path separators:
|
||||||
|
- Before: path = storage_json.firmware_bin_path.with_name(file_name)
|
||||||
|
ValueError: Invalid name 'zephyr/zephyr.uf2'
|
||||||
|
- After: path = storage_json.firmware_bin_path.parent.joinpath(file_name)
|
||||||
|
Works correctly with subdirectory paths
|
||||||
|
"""
|
||||||
|
# Create a fake nRF52 build structure with firmware in subdirectory
|
||||||
|
build_dir = get_build_path(tmp_path, "nrf52-device")
|
||||||
|
zephyr_dir = build_dir / "zephyr"
|
||||||
|
zephyr_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
# Create the main firmware binary (would be in build root)
|
||||||
|
firmware_file = build_dir / "firmware.bin"
|
||||||
|
firmware_file.write_bytes(b"main firmware")
|
||||||
|
|
||||||
|
# Create the UF2 file in zephyr subdirectory (nRF52 specific)
|
||||||
|
uf2_file = zephyr_dir / "zephyr.uf2"
|
||||||
|
uf2_file.write_bytes(b"nRF52 UF2 firmware content")
|
||||||
|
|
||||||
|
# Mock storage JSON
|
||||||
|
mock_storage = Mock()
|
||||||
|
mock_storage.name = "nrf52-device"
|
||||||
|
mock_storage.firmware_bin_path = firmware_file
|
||||||
|
mock_storage_json.load.return_value = mock_storage
|
||||||
|
|
||||||
|
# Request the UF2 file with subdirectory path
|
||||||
|
response = await dashboard.fetch(
|
||||||
|
"/download.bin?configuration=nrf52-device.yaml&file=zephyr/zephyr.uf2",
|
||||||
|
method="GET",
|
||||||
|
)
|
||||||
|
assert response.code == 200
|
||||||
|
assert response.body == b"nRF52 UF2 firmware content"
|
||||||
|
assert response.headers["Content-Type"] == "application/octet-stream"
|
||||||
|
assert "attachment" in response.headers["Content-Disposition"]
|
||||||
|
# Download name should be device-name + full file path
|
||||||
|
assert "nrf52-device-zephyr/zephyr.uf2" in response.headers["Content-Disposition"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.usefixtures("mock_ext_storage_path")
|
||||||
|
async def test_download_binary_handler_subdirectory_file_url_encoded(
|
||||||
|
dashboard: DashboardTestHelper,
|
||||||
|
tmp_path: Path,
|
||||||
|
mock_storage_json: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test the DownloadBinaryRequestHandler.get with URL-encoded subdirectory path.
|
||||||
|
|
||||||
|
Verifies that URL-encoded paths (e.g., zephyr%2Fzephyr.uf2) are correctly
|
||||||
|
decoded and handled, and that custom download names work with subdirectories.
|
||||||
|
"""
|
||||||
|
# Create a fake build structure with firmware in subdirectory
|
||||||
|
build_dir = get_build_path(tmp_path, "test")
|
||||||
|
zephyr_dir = build_dir / "zephyr"
|
||||||
|
zephyr_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
firmware_file = build_dir / "firmware.bin"
|
||||||
|
firmware_file.write_bytes(b"content")
|
||||||
|
|
||||||
|
uf2_file = zephyr_dir / "zephyr.uf2"
|
||||||
|
uf2_file.write_bytes(b"content")
|
||||||
|
|
||||||
|
# Mock storage JSON
|
||||||
|
mock_storage = Mock()
|
||||||
|
mock_storage.name = "test_device"
|
||||||
|
mock_storage.firmware_bin_path = firmware_file
|
||||||
|
mock_storage_json.load.return_value = mock_storage
|
||||||
|
|
||||||
|
# Request with URL-encoded path and custom download name
|
||||||
|
response = await dashboard.fetch(
|
||||||
|
"/download.bin?configuration=test.yaml&file=zephyr%2Fzephyr.uf2&download=custom_name.bin",
|
||||||
|
method="GET",
|
||||||
|
)
|
||||||
|
assert response.code == 200
|
||||||
|
assert "custom_name.bin" in response.headers["Content-Disposition"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.usefixtures("mock_ext_storage_path")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"attack_path",
|
||||||
|
[
|
||||||
|
pytest.param("../../../secrets.yaml", id="basic_traversal"),
|
||||||
|
pytest.param("..%2F..%2F..%2Fsecrets.yaml", id="url_encoded"),
|
||||||
|
pytest.param("zephyr/../../../secrets.yaml", id="traversal_with_prefix"),
|
||||||
|
pytest.param("/etc/passwd", id="absolute_path"),
|
||||||
|
pytest.param("//etc/passwd", id="double_slash_absolute"),
|
||||||
|
pytest.param("....//secrets.yaml", id="multiple_dots"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_download_binary_handler_path_traversal_protection(
|
||||||
|
dashboard: DashboardTestHelper,
|
||||||
|
tmp_path: Path,
|
||||||
|
mock_storage_json: MagicMock,
|
||||||
|
attack_path: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test that DownloadBinaryRequestHandler prevents path traversal attacks.
|
||||||
|
|
||||||
|
Verifies that attempts to use '..' in file paths are sanitized to prevent
|
||||||
|
accessing files outside the build directory. Tests multiple attack vectors.
|
||||||
|
"""
|
||||||
|
# Create build structure
|
||||||
|
build_dir = get_build_path(tmp_path, "test")
|
||||||
|
build_dir.mkdir(parents=True)
|
||||||
|
firmware_file = build_dir / "firmware.bin"
|
||||||
|
firmware_file.write_bytes(b"firmware content")
|
||||||
|
|
||||||
|
# Create a sensitive file outside the build directory that should NOT be accessible
|
||||||
|
sensitive_file = tmp_path / "secrets.yaml"
|
||||||
|
sensitive_file.write_bytes(b"secret: my_secret_password")
|
||||||
|
|
||||||
|
# Mock storage JSON
|
||||||
|
mock_storage = Mock()
|
||||||
|
mock_storage.name = "test_device"
|
||||||
|
mock_storage.firmware_bin_path = firmware_file
|
||||||
|
mock_storage_json.load.return_value = mock_storage
|
||||||
|
|
||||||
|
# Attempt path traversal attack - should be blocked
|
||||||
|
with pytest.raises(HTTPClientError) as exc_info:
|
||||||
|
await dashboard.fetch(
|
||||||
|
f"/download.bin?configuration=test.yaml&file={attack_path}",
|
||||||
|
method="GET",
|
||||||
|
)
|
||||||
|
# Should get 404 (file not found after sanitization) or 500 (idedata fails)
|
||||||
|
assert exc_info.value.code in (404, 500)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.usefixtures("mock_ext_storage_path")
|
||||||
|
async def test_download_binary_handler_multiple_subdirectory_levels(
|
||||||
|
dashboard: DashboardTestHelper,
|
||||||
|
tmp_path: Path,
|
||||||
|
mock_storage_json: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test downloading files from multiple subdirectory levels.
|
||||||
|
|
||||||
|
Verifies that joinpath correctly handles multi-level paths like 'build/output/firmware.bin'.
|
||||||
|
"""
|
||||||
|
# Create nested directory structure
|
||||||
|
build_dir = get_build_path(tmp_path, "test")
|
||||||
|
nested_dir = build_dir / "build" / "output"
|
||||||
|
nested_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
firmware_file = build_dir / "firmware.bin"
|
||||||
|
firmware_file.write_bytes(b"main")
|
||||||
|
|
||||||
|
nested_file = nested_dir / "firmware.bin"
|
||||||
|
nested_file.write_bytes(b"nested firmware content")
|
||||||
|
|
||||||
|
# Mock storage JSON
|
||||||
|
mock_storage = Mock()
|
||||||
|
mock_storage.name = "test_device"
|
||||||
|
mock_storage.firmware_bin_path = firmware_file
|
||||||
|
mock_storage_json.load.return_value = mock_storage
|
||||||
|
|
||||||
|
response = await dashboard.fetch(
|
||||||
|
"/download.bin?configuration=test.yaml&file=build/output/firmware.bin",
|
||||||
|
method="GET",
|
||||||
|
)
|
||||||
|
assert response.code == 200
|
||||||
|
assert response.body == b"nested firmware content"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_edit_request_handler_post_invalid_file(
|
async def test_edit_request_handler_post_invalid_file(
|
||||||
dashboard: DashboardTestHelper,
|
dashboard: DashboardTestHelper,
|
||||||
|
@@ -34,10 +34,9 @@ binary_sensor:
|
|||||||
ESP_LOGD("test", "Button ON at %u", now);
|
ESP_LOGD("test", "Button ON at %u", now);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
// Only log state change
|
|
||||||
if (id(ir_remote_button).state) {
|
|
||||||
ESP_LOGD("test", "Button OFF at %u", now);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
// Only log state change
|
||||||
|
if (id(ir_remote_button).state) {
|
||||||
|
ESP_LOGD("test", "Button OFF at %u", now);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
Reference in New Issue
Block a user