Merge remote-tracking branch 'upstream/dev' into memory

This commit is contained in:
J. Nick Koston 2025-07-11 12:05:55 -10:00
commit e76c40a1e7
No known key found for this signature in database
29 changed files with 1479 additions and 149 deletions

View File

@ -41,7 +41,7 @@ runs:
shell: bash
run: |
python -m venv venv
./venv/Scripts/activate
source ./venv/Scripts/activate
python --version
pip install -r requirements.txt -r requirements_test.txt
pip install -e .

View File

@ -66,6 +66,8 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
@ -87,6 +89,8 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
@ -108,6 +112,8 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
@ -129,6 +135,8 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
@ -214,7 +222,7 @@ jobs:
- name: Run pytest
if: matrix.os == 'windows-latest'
run: |
./venv/Scripts/activate
. ./venv/Scripts/activate.ps1
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
@ -232,11 +240,54 @@ jobs:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
determine-jobs:
name: Determine which jobs to run
runs-on: ubuntu-24.04
needs:
- common
outputs:
integration-tests: ${{ steps.determine.outputs.integration-tests }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-format: ${{ steps.determine.outputs.clang-format }}
python-linters: ${{ steps.determine.outputs.python-linters }}
changed-components: ${{ steps.determine.outputs.changed-components }}
component-test-count: ${{ steps.determine.outputs.component-test-count }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
with:
# Fetch enough history to find the merge base
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Determine which tests to run
id: determine
env:
GH_TOKEN: ${{ github.token }}
run: |
. venv/bin/activate
output=$(python script/determine-jobs.py)
echo "Test determination output:"
echo "$output" | jq
# Extract individual fields
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-format=$(echo "$output" | jq -r '.clang_format')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
integration-tests:
name: Run integration tests
runs-on: ubuntu-latest
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.integration-tests == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
@ -271,6 +322,8 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.clang-format == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
@ -304,6 +357,8 @@ jobs:
- pylint
- pytest
- pyupgrade
- determine-jobs
if: needs.determine-jobs.outputs.clang-tidy == 'true'
env:
GH_TOKEN: ${{ github.token }}
strategy:
@ -344,6 +399,9 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
@ -408,50 +466,18 @@ jobs:
# yamllint disable-line rule:line-length
if: always()
list-components:
runs-on: ubuntu-24.04
needs:
- common
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
outputs:
components: ${{ steps.list-components.outputs.components }}
count: ${{ steps.list-components.outputs.count }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Find changed components
id: list-components
run: |
. venv/bin/activate
components=$(script/list-components.py --changed)
output_components=$(echo "$components" | jq -R -s -c 'split("\n")[:-1] | map(select(length > 0))')
count=$(echo "$output_components" | jq length)
echo "components=$output_components" >> $GITHUB_OUTPUT
echo "count=$count" >> $GITHUB_OUTPUT
echo "$count Components:"
echo "$output_components" | jq
test-build-components:
name: Component test ${{ matrix.file }}
runs-on: ubuntu-24.04
needs:
- common
- list-components
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) > 0 && fromJSON(needs.list-components.outputs.count) < 100
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 && fromJSON(needs.determine-jobs.outputs.component-test-count) < 100
strategy:
fail-fast: false
max-parallel: 2
matrix:
file: ${{ fromJson(needs.list-components.outputs.components) }}
file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }}
steps:
- name: Install dependencies
run: |
@ -479,8 +505,8 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- list-components
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
outputs:
matrix: ${{ steps.split.outputs.components }}
steps:
@ -489,7 +515,7 @@ jobs:
- name: Split components into 20 groups
id: split
run: |
components=$(echo '${{ needs.list-components.outputs.components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
echo "components=$components" >> $GITHUB_OUTPUT
test-build-components-split:
@ -497,9 +523,9 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- list-components
- determine-jobs
- test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
strategy:
fail-fast: false
max-parallel: 4
@ -550,7 +576,7 @@ jobs:
- integration-tests
- pyupgrade
- clang-tidy
- list-components
- determine-jobs
- test-build-components
- test-build-components-splitter
- test-build-components-split

View File

@ -374,6 +374,7 @@ message CoverCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_COVER";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
@ -387,6 +388,7 @@ message CoverCommandRequest {
bool has_tilt = 6;
float tilt = 7;
bool stop = 8;
uint32 device_id = 9;
}
// ==================== FAN ====================
@ -441,6 +443,7 @@ message FanCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_FAN";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
bool has_state = 2;
@ -455,6 +458,7 @@ message FanCommandRequest {
int32 speed_level = 11;
bool has_preset_mode = 12;
string preset_mode = 13;
uint32 device_id = 14;
}
// ==================== LIGHT ====================
@ -523,6 +527,7 @@ message LightCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_LIGHT";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
bool has_state = 2;
@ -551,6 +556,7 @@ message LightCommandRequest {
uint32 flash_length = 17;
bool has_effect = 18;
string effect = 19;
uint32 device_id = 28;
}
// ==================== SENSOR ====================
@ -640,9 +646,11 @@ message SwitchCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SWITCH";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
bool state = 2;
uint32 device_id = 3;
}
// ==================== TEXT SENSOR ====================
@ -850,12 +858,14 @@ message ListEntitiesCameraResponse {
message CameraImageResponse {
option (id) = 44;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CAMERA";
fixed32 key = 1;
bytes data = 2;
bool done = 3;
uint32 device_id = 4;
}
message CameraImageRequest {
option (id) = 45;
@ -980,6 +990,7 @@ message ClimateCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_CLIMATE";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
bool has_mode = 2;
@ -1005,6 +1016,7 @@ message ClimateCommandRequest {
string custom_preset = 21;
bool has_target_humidity = 22;
float target_humidity = 23;
uint32 device_id = 24;
}
// ==================== NUMBER ====================
@ -1054,9 +1066,11 @@ message NumberCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_NUMBER";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
float state = 2;
uint32 device_id = 3;
}
// ==================== SELECT ====================
@ -1096,9 +1110,11 @@ message SelectCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SELECT";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
string state = 2;
uint32 device_id = 3;
}
// ==================== SIREN ====================
@ -1137,6 +1153,7 @@ message SirenCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SIREN";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
bool has_state = 2;
@ -1147,6 +1164,7 @@ message SirenCommandRequest {
uint32 duration = 7;
bool has_volume = 8;
float volume = 9;
uint32 device_id = 10;
}
// ==================== LOCK ====================
@ -1201,12 +1219,14 @@ message LockCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_LOCK";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
LockCommand command = 2;
// Not yet implemented:
bool has_code = 3;
string code = 4;
uint32 device_id = 5;
}
// ==================== BUTTON ====================
@ -1232,8 +1252,10 @@ message ButtonCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_BUTTON";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
uint32 device_id = 2;
}
// ==================== MEDIA PLAYER ====================
@ -1301,6 +1323,7 @@ message MediaPlayerCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_MEDIA_PLAYER";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
@ -1315,6 +1338,7 @@ message MediaPlayerCommandRequest {
bool has_announcement = 8;
bool announcement = 9;
uint32 device_id = 10;
}
// ==================== BLUETOOTH ====================
@ -1843,9 +1867,11 @@ message AlarmControlPanelCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
AlarmControlPanelStateCommand command = 2;
string code = 3;
uint32 device_id = 4;
}
// ===================== TEXT =====================
@ -1892,9 +1918,11 @@ message TextCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_TEXT";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
string state = 2;
uint32 device_id = 3;
}
@ -1936,11 +1964,13 @@ message DateCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_DATE";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
uint32 year = 2;
uint32 month = 3;
uint32 day = 4;
uint32 device_id = 5;
}
// ==================== DATETIME TIME ====================
@ -1981,11 +2011,13 @@ message TimeCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_TIME";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
uint32 hour = 2;
uint32 minute = 3;
uint32 second = 4;
uint32 device_id = 5;
}
// ==================== EVENT ====================
@ -2065,11 +2097,13 @@ message ValveCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VALVE";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
bool has_position = 2;
float position = 3;
bool stop = 4;
uint32 device_id = 5;
}
// ==================== DATETIME DATETIME ====================
@ -2108,9 +2142,11 @@ message DateTimeCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_DATETIME";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 epoch_seconds = 2;
uint32 device_id = 3;
}
// ==================== UPDATE ====================
@ -2160,7 +2196,9 @@ message UpdateCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_UPDATE";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
UpdateCommand command = 2;
uint32 device_id = 3;
}

View File

@ -1920,7 +1920,7 @@ uint16_t APIConnection::get_estimated_message_size(uint16_t message_type) {
case ListEntitiesClimateResponse::MESSAGE_TYPE:
return ListEntitiesClimateResponse::ESTIMATED_SIZE;
#endif
#ifdef USE_ESP32_CAMERA
#ifdef USE_CAMERA
case ListEntitiesCameraResponse::MESSAGE_TYPE:
return ListEntitiesCameraResponse::ESTIMATED_SIZE;
#endif

View File

@ -623,6 +623,10 @@ bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
this->stop = value.as_bool();
return true;
}
case 9: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -654,6 +658,7 @@ void CoverCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(6, this->has_tilt);
buffer.encode_float(7, this->tilt);
buffer.encode_bool(8, this->stop);
buffer.encode_uint32(9, this->device_id);
}
void CoverCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
@ -664,6 +669,7 @@ void CoverCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->has_tilt, false);
ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f, false);
ProtoSize::add_bool_field(total_size, 1, this->stop, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_FAN
@ -889,6 +895,10 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
this->has_preset_mode = value.as_bool();
return true;
}
case 14: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -927,6 +937,7 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_int32(11, this->speed_level);
buffer.encode_bool(12, this->has_preset_mode);
buffer.encode_string(13, this->preset_mode);
buffer.encode_uint32(14, this->device_id);
}
void FanCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
@ -942,6 +953,7 @@ void FanCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_int32_field(total_size, 1, this->speed_level, false);
ProtoSize::add_bool_field(total_size, 1, this->has_preset_mode, false);
ProtoSize::add_string_field(total_size, 1, this->preset_mode, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_LIGHT
@ -1247,6 +1259,10 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
this->has_effect = value.as_bool();
return true;
}
case 28: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -1335,6 +1351,7 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(17, this->flash_length);
buffer.encode_bool(18, this->has_effect);
buffer.encode_string(19, this->effect);
buffer.encode_uint32(28, this->device_id);
}
void LightCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
@ -1364,6 +1381,7 @@ void LightCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_uint32_field(total_size, 2, this->flash_length, false);
ProtoSize::add_bool_field(total_size, 2, this->has_effect, false);
ProtoSize::add_string_field(total_size, 2, this->effect, false);
ProtoSize::add_uint32_field(total_size, 2, this->device_id, false);
}
#endif
#ifdef USE_SENSOR
@ -1637,6 +1655,10 @@ bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
this->state = value.as_bool();
return true;
}
case 3: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -1654,10 +1676,12 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
void SwitchCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_bool(2, this->state);
buffer.encode_uint32(3, this->device_id);
}
void SwitchCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
ProtoSize::add_bool_field(total_size, 1, this->state, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_TEXT_SENSOR
@ -2293,6 +2317,10 @@ bool CameraImageResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
this->done = value.as_bool();
return true;
}
case 4: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -2321,11 +2349,13 @@ void CameraImageResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_bytes(2, reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size());
buffer.encode_bool(3, this->done);
buffer.encode_uint32(4, this->device_id);
}
void CameraImageResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
ProtoSize::add_string_field(total_size, 1, this->data, false);
ProtoSize::add_bool_field(total_size, 1, this->done, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
@ -2749,6 +2779,10 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
this->has_target_humidity = value.as_bool();
return true;
}
case 24: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -2817,6 +2851,7 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(21, this->custom_preset);
buffer.encode_bool(22, this->has_target_humidity);
buffer.encode_float(23, this->target_humidity);
buffer.encode_uint32(24, this->device_id);
}
void ClimateCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
@ -2842,6 +2877,7 @@ void ClimateCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 2, this->custom_preset, false);
ProtoSize::add_bool_field(total_size, 2, this->has_target_humidity, false);
ProtoSize::add_fixed_field<4>(total_size, 2, this->target_humidity != 0.0f, false);
ProtoSize::add_uint32_field(total_size, 2, this->device_id, false);
}
#endif
#ifdef USE_NUMBER
@ -2991,6 +3027,16 @@ void NumberStateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
bool NumberCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 3: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
}
bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
@ -3008,10 +3054,12 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
void NumberCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_float(2, this->state);
buffer.encode_uint32(3, this->device_id);
}
void NumberCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_SELECT
@ -3143,6 +3191,16 @@ void SelectStateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 3: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
}
bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2: {
@ -3166,10 +3224,12 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
void SelectCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_string(2, this->state);
buffer.encode_uint32(3, this->device_id);
}
void SelectCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
ProtoSize::add_string_field(total_size, 1, this->state, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_SIREN
@ -3327,6 +3387,10 @@ bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
this->has_volume = value.as_bool();
return true;
}
case 10: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -3365,6 +3429,7 @@ void SirenCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(7, this->duration);
buffer.encode_bool(8, this->has_volume);
buffer.encode_float(9, this->volume);
buffer.encode_uint32(10, this->device_id);
}
void SirenCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
@ -3376,6 +3441,7 @@ void SirenCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_uint32_field(total_size, 1, this->duration, false);
ProtoSize::add_bool_field(total_size, 1, this->has_volume, false);
ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_LOCK
@ -3517,6 +3583,10 @@ bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
this->has_code = value.as_bool();
return true;
}
case 5: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -3546,12 +3616,14 @@ void LockCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_enum<enums::LockCommand>(2, this->command);
buffer.encode_bool(3, this->has_code);
buffer.encode_string(4, this->code);
buffer.encode_uint32(5, this->device_id);
}
void LockCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->command), false);
ProtoSize::add_bool_field(total_size, 1, this->has_code, false);
ProtoSize::add_string_field(total_size, 1, this->code, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_BUTTON
@ -3631,6 +3703,16 @@ void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->device_class, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
bool ButtonCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
}
bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
@ -3641,9 +3723,13 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
return false;
}
}
void ButtonCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); }
void ButtonCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_uint32(2, this->device_id);
}
void ButtonCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_MEDIA_PLAYER
@ -3849,6 +3935,10 @@ bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt val
this->announcement = value.as_bool();
return true;
}
case 10: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -3887,6 +3977,7 @@ void MediaPlayerCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(7, this->media_url);
buffer.encode_bool(8, this->has_announcement);
buffer.encode_bool(9, this->announcement);
buffer.encode_uint32(10, this->device_id);
}
void MediaPlayerCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
@ -3898,6 +3989,7 @@ void MediaPlayerCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->media_url, false);
ProtoSize::add_bool_field(total_size, 1, this->has_announcement, false);
ProtoSize::add_bool_field(total_size, 1, this->announcement, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
@ -5311,6 +5403,10 @@ bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarI
this->command = value.as_enum<enums::AlarmControlPanelStateCommand>();
return true;
}
case 4: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -5339,11 +5435,13 @@ void AlarmControlPanelCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_enum<enums::AlarmControlPanelStateCommand>(2, this->command);
buffer.encode_string(3, this->code);
buffer.encode_uint32(4, this->device_id);
}
void AlarmControlPanelCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->command), false);
ProtoSize::add_string_field(total_size, 1, this->code, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_TEXT
@ -5487,6 +5585,16 @@ void TextStateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 3: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
}
bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2: {
@ -5510,10 +5618,12 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
void TextCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_string(2, this->state);
buffer.encode_uint32(3, this->device_id);
}
void TextCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
ProtoSize::add_string_field(total_size, 1, this->state, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_DATETIME_DATE
@ -5653,6 +5763,10 @@ bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
this->day = value.as_uint32();
return true;
}
case 5: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -5672,12 +5786,14 @@ void DateCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(2, this->year);
buffer.encode_uint32(3, this->month);
buffer.encode_uint32(4, this->day);
buffer.encode_uint32(5, this->device_id);
}
void DateCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
ProtoSize::add_uint32_field(total_size, 1, this->year, false);
ProtoSize::add_uint32_field(total_size, 1, this->month, false);
ProtoSize::add_uint32_field(total_size, 1, this->day, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_DATETIME_TIME
@ -5817,6 +5933,10 @@ bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
this->second = value.as_uint32();
return true;
}
case 5: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -5836,12 +5956,14 @@ void TimeCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(2, this->hour);
buffer.encode_uint32(3, this->minute);
buffer.encode_uint32(4, this->second);
buffer.encode_uint32(5, this->device_id);
}
void TimeCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
ProtoSize::add_uint32_field(total_size, 1, this->hour, false);
ProtoSize::add_uint32_field(total_size, 1, this->minute, false);
ProtoSize::add_uint32_field(total_size, 1, this->second, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_EVENT
@ -6119,6 +6241,10 @@ bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
this->stop = value.as_bool();
return true;
}
case 5: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -6142,12 +6268,14 @@ void ValveCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(2, this->has_position);
buffer.encode_float(3, this->position);
buffer.encode_bool(4, this->stop);
buffer.encode_uint32(5, this->device_id);
}
void ValveCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
ProtoSize::add_bool_field(total_size, 1, this->has_position, false);
ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false);
ProtoSize::add_bool_field(total_size, 1, this->stop, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_DATETIME_DATETIME
@ -6261,6 +6389,16 @@ void DateTimeStateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
bool DateTimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 3: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
}
bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
@ -6278,10 +6416,12 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
void DateTimeCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_fixed32(2, this->epoch_seconds);
buffer.encode_uint32(3, this->device_id);
}
void DateTimeCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif
#ifdef USE_UPDATE
@ -6455,6 +6595,10 @@ bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
this->command = value.as_enum<enums::UpdateCommand>();
return true;
}
case 3: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@ -6472,10 +6616,12 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
void UpdateCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_enum<enums::UpdateCommand>(2, this->command);
buffer.encode_uint32(3, this->device_id);
}
void UpdateCommandRequest::calculate_size(uint32_t &total_size) const {
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->command), false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#endif

View File

@ -307,6 +307,15 @@ class StateResponseProtoMessage : public ProtoMessage {
protected:
};
class CommandProtoMessage : public ProtoMessage {
public:
~CommandProtoMessage() override = default;
uint32_t key{0};
uint32_t device_id{0};
protected:
};
class HelloRequest : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 1;
@ -640,14 +649,13 @@ class CoverStateResponse : public StateResponseProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class CoverCommandRequest : public ProtoMessage {
class CoverCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 30;
static constexpr uint16_t ESTIMATED_SIZE = 25;
static constexpr uint16_t ESTIMATED_SIZE = 29;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "cover_command_request"; }
#endif
uint32_t key{0};
bool has_legacy_command{false};
enums::LegacyCoverCommand legacy_command{};
bool has_position{false};
@ -714,14 +722,13 @@ class FanStateResponse : public StateResponseProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class FanCommandRequest : public ProtoMessage {
class FanCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 31;
static constexpr uint16_t ESTIMATED_SIZE = 38;
static constexpr uint16_t ESTIMATED_SIZE = 42;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "fan_command_request"; }
#endif
uint32_t key{0};
bool has_state{false};
bool state{false};
bool has_speed{false};
@ -803,14 +810,13 @@ class LightStateResponse : public StateResponseProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class LightCommandRequest : public ProtoMessage {
class LightCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 32;
static constexpr uint16_t ESTIMATED_SIZE = 107;
static constexpr uint16_t ESTIMATED_SIZE = 112;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "light_command_request"; }
#endif
uint32_t key{0};
bool has_state{false};
bool state{false};
bool has_brightness{false};
@ -933,14 +939,13 @@ class SwitchStateResponse : public StateResponseProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class SwitchCommandRequest : public ProtoMessage {
class SwitchCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 33;
static constexpr uint16_t ESTIMATED_SIZE = 7;
static constexpr uint16_t ESTIMATED_SIZE = 11;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "switch_command_request"; }
#endif
uint32_t key{0};
bool state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@ -1292,14 +1297,13 @@ class ListEntitiesCameraResponse : public InfoResponseProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class CameraImageResponse : public ProtoMessage {
class CameraImageResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 44;
static constexpr uint16_t ESTIMATED_SIZE = 16;
static constexpr uint16_t ESTIMATED_SIZE = 20;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "camera_image_response"; }
#endif
uint32_t key{0};
std::string data{};
bool done{false};
void encode(ProtoWriteBuffer buffer) const override;
@ -1401,14 +1405,13 @@ class ClimateStateResponse : public StateResponseProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ClimateCommandRequest : public ProtoMessage {
class ClimateCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 48;
static constexpr uint16_t ESTIMATED_SIZE = 83;
static constexpr uint16_t ESTIMATED_SIZE = 88;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "climate_command_request"; }
#endif
uint32_t key{0};
bool has_mode{false};
enums::ClimateMode mode{};
bool has_target_temperature{false};
@ -1487,14 +1490,13 @@ class NumberStateResponse : public StateResponseProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class NumberCommandRequest : public ProtoMessage {
class NumberCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 51;
static constexpr uint16_t ESTIMATED_SIZE = 10;
static constexpr uint16_t ESTIMATED_SIZE = 14;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "number_command_request"; }
#endif
uint32_t key{0};
float state{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@ -1504,6 +1506,7 @@ class NumberCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_SELECT
@ -1546,14 +1549,13 @@ class SelectStateResponse : public StateResponseProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class SelectCommandRequest : public ProtoMessage {
class SelectCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 54;
static constexpr uint16_t ESTIMATED_SIZE = 14;
static constexpr uint16_t ESTIMATED_SIZE = 18;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "select_command_request"; }
#endif
uint32_t key{0};
std::string state{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@ -1564,6 +1566,7 @@ class SelectCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_SIREN
@ -1606,14 +1609,13 @@ class SirenStateResponse : public StateResponseProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class SirenCommandRequest : public ProtoMessage {
class SirenCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 57;
static constexpr uint16_t ESTIMATED_SIZE = 33;
static constexpr uint16_t ESTIMATED_SIZE = 37;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "siren_command_request"; }
#endif
uint32_t key{0};
bool has_state{false};
bool state{false};
bool has_tone{false};
@ -1675,14 +1677,13 @@ class LockStateResponse : public StateResponseProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class LockCommandRequest : public ProtoMessage {
class LockCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 60;
static constexpr uint16_t ESTIMATED_SIZE = 18;
static constexpr uint16_t ESTIMATED_SIZE = 22;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "lock_command_request"; }
#endif
uint32_t key{0};
enums::LockCommand command{};
bool has_code{false};
std::string code{};
@ -1718,14 +1719,13 @@ class ListEntitiesButtonResponse : public InfoResponseProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ButtonCommandRequest : public ProtoMessage {
class ButtonCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 62;
static constexpr uint16_t ESTIMATED_SIZE = 5;
static constexpr uint16_t ESTIMATED_SIZE = 9;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "button_command_request"; }
#endif
uint32_t key{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@ -1734,6 +1734,7 @@ class ButtonCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_MEDIA_PLAYER
@ -1794,14 +1795,13 @@ class MediaPlayerStateResponse : public StateResponseProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class MediaPlayerCommandRequest : public ProtoMessage {
class MediaPlayerCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 65;
static constexpr uint16_t ESTIMATED_SIZE = 31;
static constexpr uint16_t ESTIMATED_SIZE = 35;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "media_player_command_request"; }
#endif
uint32_t key{0};
bool has_command{false};
enums::MediaPlayerCommand command{};
bool has_volume{false};
@ -2669,14 +2669,13 @@ class AlarmControlPanelStateResponse : public StateResponseProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class AlarmControlPanelCommandRequest : public ProtoMessage {
class AlarmControlPanelCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 96;
static constexpr uint16_t ESTIMATED_SIZE = 16;
static constexpr uint16_t ESTIMATED_SIZE = 20;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "alarm_control_panel_command_request"; }
#endif
uint32_t key{0};
enums::AlarmControlPanelStateCommand command{};
std::string code{};
void encode(ProtoWriteBuffer buffer) const override;
@ -2734,14 +2733,13 @@ class TextStateResponse : public StateResponseProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class TextCommandRequest : public ProtoMessage {
class TextCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 99;
static constexpr uint16_t ESTIMATED_SIZE = 14;
static constexpr uint16_t ESTIMATED_SIZE = 18;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "text_command_request"; }
#endif
uint32_t key{0};
std::string state{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@ -2752,6 +2750,7 @@ class TextCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_DATETIME_DATE
@ -2794,14 +2793,13 @@ class DateStateResponse : public StateResponseProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class DateCommandRequest : public ProtoMessage {
class DateCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 102;
static constexpr uint16_t ESTIMATED_SIZE = 17;
static constexpr uint16_t ESTIMATED_SIZE = 21;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "date_command_request"; }
#endif
uint32_t key{0};
uint32_t year{0};
uint32_t month{0};
uint32_t day{0};
@ -2856,14 +2854,13 @@ class TimeStateResponse : public StateResponseProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class TimeCommandRequest : public ProtoMessage {
class TimeCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 105;
static constexpr uint16_t ESTIMATED_SIZE = 17;
static constexpr uint16_t ESTIMATED_SIZE = 21;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "time_command_request"; }
#endif
uint32_t key{0};
uint32_t hour{0};
uint32_t minute{0};
uint32_t second{0};
@ -2961,14 +2958,13 @@ class ValveStateResponse : public StateResponseProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ValveCommandRequest : public ProtoMessage {
class ValveCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 111;
static constexpr uint16_t ESTIMATED_SIZE = 14;
static constexpr uint16_t ESTIMATED_SIZE = 18;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "valve_command_request"; }
#endif
uint32_t key{0};
bool has_position{false};
float position{0.0f};
bool stop{false};
@ -3021,14 +3017,13 @@ class DateTimeStateResponse : public StateResponseProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class DateTimeCommandRequest : public ProtoMessage {
class DateTimeCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 114;
static constexpr uint16_t ESTIMATED_SIZE = 10;
static constexpr uint16_t ESTIMATED_SIZE = 14;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "date_time_command_request"; }
#endif
uint32_t key{0};
uint32_t epoch_seconds{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@ -3038,6 +3033,7 @@ class DateTimeCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_UPDATE
@ -3087,14 +3083,13 @@ class UpdateStateResponse : public StateResponseProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class UpdateCommandRequest : public ProtoMessage {
class UpdateCommandRequest : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 118;
static constexpr uint16_t ESTIMATED_SIZE = 7;
static constexpr uint16_t ESTIMATED_SIZE = 11;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "update_command_request"; }
#endif
uint32_t key{0};
enums::UpdateCommand command{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;

View File

@ -986,6 +986,11 @@ void CoverCommandRequest::dump_to(std::string &out) const {
out.append(" stop: ");
out.append(YESNO(this->stop));
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -1146,6 +1151,11 @@ void FanCommandRequest::dump_to(std::string &out) const {
out.append(" preset_mode: ");
out.append("'").append(this->preset_mode).append("'");
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -1419,6 +1429,11 @@ void LightCommandRequest::dump_to(std::string &out) const {
out.append(" effect: ");
out.append("'").append(this->effect).append("'");
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -1586,6 +1601,11 @@ void SwitchCommandRequest::dump_to(std::string &out) const {
out.append(" state: ");
out.append(YESNO(this->state));
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -1944,6 +1964,11 @@ void CameraImageResponse::dump_to(std::string &out) const {
out.append(" done: ");
out.append(YESNO(this->done));
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
void CameraImageRequest::dump_to(std::string &out) const {
@ -2263,6 +2288,11 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%g", this->target_humidity);
out.append(buffer);
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -2367,6 +2397,11 @@ void NumberCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%g", this->state);
out.append(buffer);
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -2448,6 +2483,11 @@ void SelectCommandRequest::dump_to(std::string &out) const {
out.append(" state: ");
out.append("'").append(this->state).append("'");
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -2563,6 +2603,11 @@ void SirenCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%g", this->volume);
out.append(buffer);
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -2658,6 +2703,11 @@ void LockCommandRequest::dump_to(std::string &out) const {
out.append(" code: ");
out.append("'").append(this->code).append("'");
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -2711,6 +2761,11 @@ void ButtonCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
out.append(buffer);
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -2857,6 +2912,11 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const {
out.append(" announcement: ");
out.append(YESNO(this->announcement));
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -3682,6 +3742,11 @@ void AlarmControlPanelCommandRequest::dump_to(std::string &out) const {
out.append(" code: ");
out.append("'").append(this->code).append("'");
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -3775,6 +3840,11 @@ void TextCommandRequest::dump_to(std::string &out) const {
out.append(" state: ");
out.append("'").append(this->state).append("'");
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -3872,6 +3942,11 @@ void DateCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day);
out.append(buffer);
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -3969,6 +4044,11 @@ void TimeCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second);
out.append(buffer);
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -4138,6 +4218,11 @@ void ValveCommandRequest::dump_to(std::string &out) const {
out.append(" stop: ");
out.append(YESNO(this->stop));
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -4215,6 +4300,11 @@ void DateTimeCommandRequest::dump_to(std::string &out) const {
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds);
out.append(buffer);
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -4323,6 +4413,11 @@ void UpdateCommandRequest::dump_to(std::string &out) const {
out.append(" command: ");
out.append(proto_enum_to_string<enums::UpdateCommand>(this->command));
out.append("\n");
out.append(" device_id: ");
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif

View File

@ -53,6 +53,7 @@ void DebugComponent::on_shutdown() {
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
if (component != nullptr) {
strncpy(buffer, component->get_component_source(), REBOOT_MAX_LEN - 1);
buffer[REBOOT_MAX_LEN - 1] = '\0';
}
ESP_LOGD(TAG, "Storing reboot source: %s", buffer);
pref.save(&buffer);
@ -68,6 +69,7 @@ std::string DebugComponent::get_reset_reason_() {
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
char buffer[REBOOT_MAX_LEN]{};
if (pref.load(&buffer)) {
buffer[REBOOT_MAX_LEN - 1] = '\0';
reset_reason = "Reboot request from " + std::string(buffer);
}
}

View File

@ -707,6 +707,7 @@ async def to_code(config):
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]

View File

@ -114,7 +114,6 @@ void ESP32InternalGPIOPin::setup() {
if (flags_ & gpio::FLAG_OUTPUT) {
gpio_set_drive_capability(pin_, drive_strength_);
}
ESP_LOGD(TAG, "rtc: %d", SOC_GPIO_SUPPORT_RTC_INDEPENDENT);
}
void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {

View File

@ -308,7 +308,7 @@ async def to_code(config):
cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT]))
cg.add(var.set_frame_size(config[CONF_RESOLUTION]))
cg.add_define("USE_ESP32_CAMERA")
cg.add_define("USE_CAMERA")
if CORE.using_esp_idf:
add_idf_component(name="espressif/esp32-camera", ref="2.0.15")

View File

@ -180,6 +180,7 @@ async def to_code(config):
cg.add(esp8266_ns.setup_preferences())
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_ESP8266")

View File

@ -45,3 +45,4 @@ async def to_code(config):
cg.add_define("ESPHOME_BOARD", "host")
cg.add_platformio_option("platform", "platformio/native")
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")

View File

@ -268,6 +268,7 @@ async def component_to_code(config):
# disable library compatibility checks
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
# include <Arduino.h> in every file
cg.add_platformio_option("build_src_flags", "-include Arduino.h")
# dummy version code

View File

@ -153,11 +153,15 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) {
case MQTT_EVENT_DATA: {
static std::string topic;
if (!event.topic.empty()) {
// When a single message arrives as multiple chunks, the topic will be empty
// on any but the first message, leading to event.topic being an empty string.
// To ensure handlers get the correct topic, cache the last seen topic to
// simulate always receiving the topic from underlying library
topic = event.topic;
}
ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str());
this->on_message_.call(!event.topic.empty() ? topic.c_str() : nullptr, event.data.data(), event.data.size(),
event.current_data_offset, event.total_data_len);
this->on_message_.call(topic.c_str(), event.data.data(), event.data.size(), event.current_data_offset,
event.total_data_len);
} break;
case MQTT_EVENT_ERROR:
ESP_LOGE(TAG, "MQTT_EVENT_ERROR");

View File

@ -165,6 +165,7 @@ async def to_code(config):
# Allow LDF to properly discover dependency including those in preprocessor
# conditionals
cg.add_platformio_option("lib_ldf_mode", "chain+")
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_RP2040")
cg.set_cpp_standard("gnu++20")

View File

@ -1055,6 +1055,7 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False):
return validator
bps = float_with_unit("bits per second", "(bps|bits/s|bit/s)?")
frequency = float_with_unit("frequency", "(Hz|HZ|hz)?")
resistance = float_with_unit("resistance", "(Ω|Ω|ohm|Ohm|OHM)?")
current = float_with_unit("current", "(a|A|amp|Amp|amps|Amps|ampere|Ampere)?")

View File

@ -187,6 +187,12 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
# No name to validate
return config
# Skip validation for internal entities
# Internal entities are not exposed to Home Assistant and don't use the hash-based
# entity state tracking system, so name collisions don't matter for them
if config.get(CONF_INTERNAL, False):
return config
# Get the entity name
entity_name = config[CONF_NAME]

View File

@ -66,10 +66,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
if (delay == SCHEDULER_DONT_RUN) {
// Still need to cancel existing timer if name is not empty
if (this->is_name_valid_(name_cstr)) {
LockGuard guard{this->lock_};
this->cancel_item_locked_(component, name_cstr, type);
}
LockGuard guard{this->lock_};
this->cancel_item_locked_(component, name_cstr, type);
return;
}
@ -125,10 +123,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
LockGuard guard{this->lock_};
// If name is provided, do atomic cancel-and-add
if (this->is_name_valid_(name_cstr)) {
// Cancel existing items
this->cancel_item_locked_(component, name_cstr, type);
}
// Cancel existing items
this->cancel_item_locked_(component, name_cstr, type);
// Add new item directly to to_add_
// since we have the lock held
this->to_add_.push_back(std::move(item));
@ -442,10 +438,6 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co
// Get the name as const char*
const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
// Handle null or empty names
if (!this->is_name_valid_(name_cstr))
return false;
// obtain lock because this function iterates and can be called from non-loop task context
LockGuard guard{this->lock_};
return this->cancel_item_locked_(component, name_cstr, type);
@ -453,6 +445,11 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co
// Helper to cancel items by name - must be called with lock held
bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) {
// Early return if name is invalid - no items to cancel
if (name_cstr == nullptr || name_cstr[0] == '\0') {
return false;
}
size_t total_cancelled = 0;
// Check all containers for matching items

View File

@ -150,9 +150,6 @@ class Scheduler {
return is_static_string ? static_cast<const char *>(name_ptr) : static_cast<const std::string *>(name_ptr)->c_str();
}
// Helper to check if a name is valid (not null and not empty)
inline bool is_name_valid_(const char *name) { return name != nullptr && name[0] != '\0'; }
// Common implementation for cancel operations
bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);

View File

@ -61,6 +61,7 @@ src_filter =
+<../tests/dummy_main.cpp>
+<../.temp/all-include.cpp>
lib_ldf_mode = off
lib_compat_mode = strict
; This are common settings for all Arduino-framework based environments.
[common:arduino]

245
script/determine-jobs.py Executable file
View File

@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Determine which CI jobs should run based on changed files.
This script is a centralized way to determine which CI jobs need to run based on
what files have changed. It outputs JSON with the following structure:
{
"integration_tests": true/false,
"clang_tidy": true/false,
"clang_format": true/false,
"python_linters": true/false,
"changed_components": ["component1", "component2", ...],
"component_test_count": 5
}
The CI workflow uses this information to:
- Skip or run integration tests
- Skip or run clang-tidy (and whether to do a full scan)
- Skip or run clang-format
- Skip or run Python linters (ruff, flake8, pylint, pyupgrade)
- Determine which components to test individually
- Decide how to split component tests (if there are many)
Usage:
python script/determine-jobs.py [-b BRANCH]
Options:
-b, --branch BRANCH Branch to compare against (default: dev)
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import subprocess
import sys
from typing import Any
from helpers import (
CPP_FILE_EXTENSIONS,
ESPHOME_COMPONENTS_PATH,
PYTHON_FILE_EXTENSIONS,
changed_files,
get_all_dependencies,
get_components_from_integration_fixtures,
parse_list_components_output,
root_path,
)
def should_run_integration_tests(branch: str | None = None) -> bool:
"""Determine if integration tests should run based on changed files.
This function is used by the CI workflow to intelligently skip integration tests when they're
not needed, saving significant CI time and resources.
Integration tests will run when ANY of the following conditions are met:
1. Core C++ files changed (esphome/core/*)
- Any .cpp, .h, .tcc files in the core directory
- These files contain fundamental functionality used throughout ESPHome
- Examples: esphome/core/component.cpp, esphome/core/application.h
2. Core Python files changed (esphome/core/*.py)
- Only .py files in the esphome/core/ directory
- These are core Python files that affect the entire system
- Examples: esphome/core/config.py, esphome/core/__init__.py
- NOT included: esphome/*.py, esphome/dashboard/*.py, esphome/components/*/*.py
3. Integration test files changed
- Any file in tests/integration/ directory
- This includes test files themselves and fixture YAML files
- Examples: tests/integration/test_api.py, tests/integration/fixtures/api.yaml
4. Components used by integration tests (or their dependencies) changed
- The function parses all YAML files in tests/integration/fixtures/
- Extracts which components are used in integration tests
- Recursively finds all dependencies of those components
- If any of these components have changes, tests must run
- Example: If api.yaml uses 'sensor' and 'api' components, and 'api' depends on 'socket',
then changes to sensor/, api/, or socket/ components trigger tests
Args:
branch: Branch to compare against. If None, uses default.
Returns:
True if integration tests should run, False otherwise.
"""
files = changed_files(branch)
# Check if any core files changed (esphome/core/*)
for file in files:
if file.startswith("esphome/core/"):
return True
# Check if any integration test files changed
if any("tests/integration" in file for file in files):
return True
# Get all components used in integration tests and their dependencies
fixture_components = get_components_from_integration_fixtures()
all_required_components = get_all_dependencies(fixture_components)
# Check if any required components changed
for file in files:
if file.startswith(ESPHOME_COMPONENTS_PATH):
parts = file.split("/")
if len(parts) >= 3:
component = parts[2]
if component in all_required_components:
return True
return False
def should_run_clang_tidy(branch: str | None = None) -> bool:
"""Determine if clang-tidy should run based on changed files.
This function is used by the CI workflow to intelligently skip clang-tidy checks when they're
not needed, saving significant CI time and resources.
Clang-tidy will run when ANY of the following conditions are met:
1. Clang-tidy configuration changed
- The hash of .clang-tidy configuration file has changed
- The hash includes the .clang-tidy file, clang-tidy version from requirements_dev.txt,
and relevant platformio.ini sections
- When configuration changes, a full scan is needed to ensure all code complies
with the new rules
- Detected by script/clang_tidy_hash.py --check returning exit code 0
2. Any C++ source files changed
- Any file with C++ extensions: .cpp, .h, .hpp, .cc, .cxx, .c, .tcc
- Includes files anywhere in the repository, not just in esphome/
- This ensures all C++ code is checked, including tests, examples, etc.
- Examples: esphome/core/component.cpp, tests/custom/my_component.h
If the hash check fails for any reason, clang-tidy runs as a safety measure to ensure
code quality is maintained.
Args:
branch: Branch to compare against. If None, uses default.
Returns:
True if clang-tidy should run, False otherwise.
"""
# First check if clang-tidy configuration changed (full scan needed)
try:
result = subprocess.run(
[os.path.join(root_path, "script", "clang_tidy_hash.py"), "--check"],
capture_output=True,
check=False,
)
# Exit 0 means hash changed (full scan needed)
if result.returncode == 0:
return True
except Exception:
# If hash check fails, run clang-tidy to be safe
return True
return _any_changed_file_endswith(branch, CPP_FILE_EXTENSIONS)
def should_run_clang_format(branch: str | None = None) -> bool:
"""Determine if clang-format should run based on changed files.
This function is used by the CI workflow to skip clang-format checks when no C++ files
have changed, saving CI time and resources.
Clang-format will run when any C++ source files have changed.
Args:
branch: Branch to compare against. If None, uses default.
Returns:
True if clang-format should run, False otherwise.
"""
return _any_changed_file_endswith(branch, CPP_FILE_EXTENSIONS)
def should_run_python_linters(branch: str | None = None) -> bool:
"""Determine if Python linters (ruff, flake8, pylint, pyupgrade) should run based on changed files.
This function is used by the CI workflow to skip Python linting checks when no Python files
have changed, saving CI time and resources.
Python linters will run when any Python source files have changed.
Args:
branch: Branch to compare against. If None, uses default.
Returns:
True if Python linters should run, False otherwise.
"""
return _any_changed_file_endswith(branch, PYTHON_FILE_EXTENSIONS)
def _any_changed_file_endswith(branch: str | None, extensions: tuple[str, ...]) -> bool:
"""Check if a changed file ends with any of the specified extensions."""
return any(file.endswith(extensions) for file in changed_files(branch))
def main() -> None:
"""Main function that determines which CI jobs to run."""
parser = argparse.ArgumentParser(
description="Determine which CI jobs should run based on changed files"
)
parser.add_argument(
"-b", "--branch", help="Branch to compare changed files against"
)
args = parser.parse_args()
# Determine what should run
run_integration = should_run_integration_tests(args.branch)
run_clang_tidy = should_run_clang_tidy(args.branch)
run_clang_format = should_run_clang_format(args.branch)
run_python_linters = should_run_python_linters(args.branch)
# Get changed components using list-components.py for exact compatibility
script_path = Path(__file__).parent / "list-components.py"
cmd = [sys.executable, str(script_path), "--changed"]
if args.branch:
cmd.extend(["-b", args.branch])
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
changed_components = parse_list_components_output(result.stdout)
# Build output
output: dict[str, Any] = {
"integration_tests": run_integration,
"clang_tidy": run_clang_tidy,
"clang_format": run_clang_format,
"python_linters": run_python_linters,
"changed_components": changed_components,
"component_test_count": len(changed_components),
}
# Output as JSON
print(json.dumps(output))
if __name__ == "__main__":
main()

View File

@ -1,5 +1,6 @@
from __future__ import annotations
from functools import cache
import json
import os
import os.path
@ -7,6 +8,7 @@ from pathlib import Path
import re
import subprocess
import time
from typing import Any
import colorama
@ -15,6 +17,34 @@ basepath = os.path.join(root_path, "esphome")
temp_folder = os.path.join(root_path, ".temp")
temp_header_file = os.path.join(temp_folder, "all-include.cpp")
# C++ file extensions used for clang-tidy and clang-format checks
CPP_FILE_EXTENSIONS = (".cpp", ".h", ".hpp", ".cc", ".cxx", ".c", ".tcc")
# Python file extensions
PYTHON_FILE_EXTENSIONS = (".py", ".pyi")
# YAML file extensions
YAML_FILE_EXTENSIONS = (".yaml", ".yml")
# Component path prefix
ESPHOME_COMPONENTS_PATH = "esphome/components/"
def parse_list_components_output(output: str) -> list[str]:
"""Parse the output from list-components.py script.
The script outputs one component name per line.
Args:
output: The stdout from list-components.py
Returns:
List of component names, or empty list if no output
"""
if not output or not output.strip():
return []
return [c.strip() for c in output.strip().split("\n") if c.strip()]
def styled(color: str | tuple[str, ...], msg: str, reset: bool = True) -> str:
prefix = "".join(color) if isinstance(color, tuple) else color
@ -96,6 +126,7 @@ def _get_pr_number_from_github_env() -> str | None:
return None
@cache
def _get_changed_files_github_actions() -> list[str] | None:
"""Get changed files in GitHub Actions environment.
@ -135,7 +166,7 @@ def changed_files(branch: str | None = None) -> list[str]:
return github_files
# Original implementation for local development
if branch is None:
if not branch: # Treat None and empty string the same
branch = "dev"
check_remotes = ["upstream", "origin"]
check_remotes.extend(splitlines_no_ends(get_output("git", "remote")))
@ -183,7 +214,7 @@ def get_changed_components() -> list[str] | None:
changed = changed_files()
core_cpp_changed = any(
f.startswith("esphome/core/")
and f.endswith((".cpp", ".h", ".hpp", ".cc", ".cxx", ".c"))
and f.endswith(CPP_FILE_EXTENSIONS[:-1]) # Exclude .tcc for core files
for f in changed
)
if core_cpp_changed:
@ -198,8 +229,7 @@ def get_changed_components() -> list[str] | None:
result = subprocess.run(
cmd, capture_output=True, text=True, check=True, close_fds=False
)
components = [c.strip() for c in result.stdout.strip().split("\n") if c.strip()]
return components
return parse_list_components_output(result.stdout)
except subprocess.CalledProcessError:
# If the script fails, fall back to full scan
print("Could not determine changed components - will run full clang-tidy scan")
@ -249,7 +279,9 @@ def _filter_changed_ci(files: list[str]) -> list[str]:
# Action: Check only the specific non-component files that changed
changed = changed_files()
files = [
f for f in files if f in changed and not f.startswith("esphome/components/")
f
for f in files
if f in changed and not f.startswith(ESPHOME_COMPONENTS_PATH)
]
if not files:
print("No files changed")
@ -267,7 +299,7 @@ def _filter_changed_ci(files: list[str]) -> list[str]:
# because changes in one file can affect other files in the same component.
filtered_files = []
for f in files:
if f.startswith("esphome/components/"):
if f.startswith(ESPHOME_COMPONENTS_PATH):
# Check if file belongs to any of the changed components
parts = f.split("/")
if len(parts) >= 3 and parts[2] in component_set:
@ -326,7 +358,7 @@ def git_ls_files(patterns: list[str] | None = None) -> dict[str, int]:
return {s[3].strip(): int(s[0]) for s in lines}
def load_idedata(environment):
def load_idedata(environment: str) -> dict[str, Any]:
start_time = time.time()
print(f"Loading IDE data for environment '{environment}'...")
@ -442,3 +474,83 @@ def get_usable_cpu_count() -> int:
return (
os.process_cpu_count() if hasattr(os, "process_cpu_count") else os.cpu_count()
)
def get_all_dependencies(component_names: set[str]) -> set[str]:
"""Get all dependencies for a set of components.
Args:
component_names: Set of component names to get dependencies for
Returns:
Set of all components including dependencies and auto-loaded components
"""
from esphome.const import KEY_CORE
from esphome.core import CORE
from esphome.loader import get_component
all_components: set[str] = set(component_names)
# Reset CORE to ensure clean state
CORE.reset()
# Set up fake config path for component loading
root = Path(__file__).parent.parent
CORE.config_path = str(root)
CORE.data[KEY_CORE] = {}
# Keep finding dependencies until no new ones are found
while True:
new_components: set[str] = set()
for comp_name in all_components:
comp = get_component(comp_name)
if not comp:
continue
# Add dependencies (extract component name before '.')
new_components.update(dep.split(".")[0] for dep in comp.dependencies)
# Add auto_load components
new_components.update(comp.auto_load)
# Check if we found any new components
new_components -= all_components
if not new_components:
break
all_components.update(new_components)
return all_components
def get_components_from_integration_fixtures() -> set[str]:
"""Extract all components used in integration test fixtures.
Returns:
Set of component names used in integration test fixtures
"""
import yaml
components: set[str] = set()
fixtures_dir = Path(__file__).parent.parent / "tests" / "integration" / "fixtures"
for yaml_file in fixtures_dir.glob("*.yaml"):
with open(yaml_file) as f:
config: dict[str, any] | None = yaml.safe_load(f)
if not config:
continue
# Add all top-level component keys
components.update(config.keys())
# Add platform components (e.g., output.template)
for value in config.values():
if not isinstance(value, list):
continue
for item in value:
if isinstance(item, dict) and "platform" in item:
components.add(item["platform"])
return components

View File

@ -20,6 +20,12 @@ def filter_component_files(str):
return str.startswith("esphome/components/") | str.startswith("tests/components/")
def get_all_component_files() -> list[str]:
"""Get all component files from git."""
files = git_ls_files()
return list(filter(filter_component_files, files))
def extract_component_names_array_from_files_array(files):
components = []
for file in files:
@ -165,17 +171,20 @@ def main():
if args.branch and not args.changed:
parser.error("--branch requires --changed")
files = git_ls_files()
files = filter(filter_component_files, files)
if args.changed:
if args.branch:
changed = changed_files(args.branch)
else:
changed = changed_files()
# When --changed is passed, only get the changed files
changed = changed_files(args.branch)
# If any base test file(s) changed, there's no need to filter out components
if not any("tests/test_build_components" in file for file in changed):
files = [f for f in files if f in changed]
if any("tests/test_build_components" in file for file in changed):
# Need to get all component files
files = get_all_component_files()
else:
# Only look at changed component files
files = [f for f in changed if filter_component_files(f)]
else:
# Get all component files
files = get_all_component_files()
for c in get_components(files, args.changed):
print(c)

View File

@ -0,0 +1,43 @@
esphome:
name: scheduler-null-name
host:
logger:
level: DEBUG
api:
services:
- service: test_null_name
then:
- lambda: |-
// First, create a scenario that would trigger the crash
// The crash happens when defer() is called with a name that would be cancelled
// Test 1: Create a defer with a valid name
App.scheduler.set_timeout(nullptr, "test_defer", 0, []() {
ESP_LOGI("TEST", "First defer should be cancelled");
});
// Test 2: Create another defer with the same name - this triggers cancel_item_locked_
// In the unfixed code, this would crash if the name was NULL
App.scheduler.set_timeout(nullptr, "test_defer", 0, []() {
ESP_LOGI("TEST", "Second defer executed");
});
// Test 3: Now test with nullptr - this is the actual crash scenario
// Create a defer item without a name (like voice assistant does)
const char* null_name = nullptr;
App.scheduler.set_timeout(nullptr, null_name, 0, []() {
ESP_LOGI("TEST", "Defer with null name executed");
});
// Test 4: Create another defer with null name - this would trigger the crash
App.scheduler.set_timeout(nullptr, null_name, 0, []() {
ESP_LOGI("TEST", "Second null defer executed");
});
// Test 5: Verify scheduler still works
App.scheduler.set_timeout(nullptr, "valid_timeout", 50, []() {
ESP_LOGI("TEST", "Test completed successfully");
});

View File

@ -0,0 +1,59 @@
"""Test that scheduler handles NULL names safely without crashing."""
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_scheduler_null_name(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that scheduler handles NULL names safely without crashing."""
loop = asyncio.get_running_loop()
test_complete_future: asyncio.Future[bool] = loop.create_future()
# Pattern to match test completion
test_complete_pattern = re.compile(r"Test completed successfully")
def check_output(line: str) -> None:
"""Check log output for test completion."""
if not test_complete_future.done() and test_complete_pattern.search(line):
test_complete_future.set_result(True)
async with run_compiled(yaml_config, line_callback=check_output):
async with api_client_connected() as client:
# Verify we can connect
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "scheduler-null-name"
# List services
_, services = await asyncio.wait_for(
client.list_entities_services(), timeout=5.0
)
# Find our test service
test_null_name_service = next(
(s for s in services if s.name == "test_null_name"), None
)
assert test_null_name_service is not None, (
"test_null_name service not found"
)
# Execute the test
client.execute_service(test_null_name_service, {})
# Wait for test completion
try:
await asyncio.wait_for(test_complete_future, timeout=10.0)
except asyncio.TimeoutError:
pytest.fail(
"Test did not complete within timeout - likely crashed due to NULL name"
)

View File

@ -0,0 +1,352 @@
"""Unit tests for script/determine-jobs.py module."""
from collections.abc import Generator
import importlib.util
import json
import os
import subprocess
import sys
from unittest.mock import Mock, patch
import pytest
# Add the script directory to Python path so we can import the module
script_dir = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "script")
)
sys.path.insert(0, script_dir)
spec = importlib.util.spec_from_file_location(
"determine_jobs", os.path.join(script_dir, "determine-jobs.py")
)
determine_jobs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(determine_jobs)
@pytest.fixture
def mock_should_run_integration_tests() -> Generator[Mock, None, None]:
"""Mock should_run_integration_tests from helpers."""
with patch.object(determine_jobs, "should_run_integration_tests") as mock:
yield mock
@pytest.fixture
def mock_should_run_clang_tidy() -> Generator[Mock, None, None]:
"""Mock should_run_clang_tidy from helpers."""
with patch.object(determine_jobs, "should_run_clang_tidy") as mock:
yield mock
@pytest.fixture
def mock_should_run_clang_format() -> Generator[Mock, None, None]:
"""Mock should_run_clang_format from helpers."""
with patch.object(determine_jobs, "should_run_clang_format") as mock:
yield mock
@pytest.fixture
def mock_should_run_python_linters() -> Generator[Mock, None, None]:
"""Mock should_run_python_linters from helpers."""
with patch.object(determine_jobs, "should_run_python_linters") as mock:
yield mock
@pytest.fixture
def mock_subprocess_run() -> Generator[Mock, None, None]:
"""Mock subprocess.run for list-components.py calls."""
with patch.object(determine_jobs.subprocess, "run") as mock:
yield mock
def test_main_all_tests_should_run(
mock_should_run_integration_tests: Mock,
mock_should_run_clang_tidy: Mock,
mock_should_run_clang_format: Mock,
mock_should_run_python_linters: Mock,
mock_subprocess_run: Mock,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Test when all tests should run."""
mock_should_run_integration_tests.return_value = True
mock_should_run_clang_tidy.return_value = True
mock_should_run_clang_format.return_value = True
mock_should_run_python_linters.return_value = True
# Mock list-components.py output
mock_result = Mock()
mock_result.stdout = "wifi\napi\nsensor\n"
mock_subprocess_run.return_value = mock_result
# Run main function with mocked argv
with patch("sys.argv", ["determine-jobs.py"]):
determine_jobs.main()
# Check output
captured = capsys.readouterr()
output = json.loads(captured.out)
assert output["integration_tests"] is True
assert output["clang_tidy"] is True
assert output["clang_format"] is True
assert output["python_linters"] is True
assert output["changed_components"] == ["wifi", "api", "sensor"]
assert output["component_test_count"] == 3
def test_main_no_tests_should_run(
mock_should_run_integration_tests: Mock,
mock_should_run_clang_tidy: Mock,
mock_should_run_clang_format: Mock,
mock_should_run_python_linters: Mock,
mock_subprocess_run: Mock,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Test when no tests should run."""
mock_should_run_integration_tests.return_value = False
mock_should_run_clang_tidy.return_value = False
mock_should_run_clang_format.return_value = False
mock_should_run_python_linters.return_value = False
# Mock empty list-components.py output
mock_result = Mock()
mock_result.stdout = ""
mock_subprocess_run.return_value = mock_result
# Run main function with mocked argv
with patch("sys.argv", ["determine-jobs.py"]):
determine_jobs.main()
# Check output
captured = capsys.readouterr()
output = json.loads(captured.out)
assert output["integration_tests"] is False
assert output["clang_tidy"] is False
assert output["clang_format"] is False
assert output["python_linters"] is False
assert output["changed_components"] == []
assert output["component_test_count"] == 0
def test_main_list_components_fails(
mock_should_run_integration_tests: Mock,
mock_should_run_clang_tidy: Mock,
mock_should_run_clang_format: Mock,
mock_should_run_python_linters: Mock,
mock_subprocess_run: Mock,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Test when list-components.py fails."""
mock_should_run_integration_tests.return_value = True
mock_should_run_clang_tidy.return_value = True
mock_should_run_clang_format.return_value = True
mock_should_run_python_linters.return_value = True
# Mock list-components.py failure
mock_subprocess_run.side_effect = subprocess.CalledProcessError(1, "cmd")
# Run main function with mocked argv - should raise
with patch("sys.argv", ["determine-jobs.py"]):
with pytest.raises(subprocess.CalledProcessError):
determine_jobs.main()
def test_main_with_branch_argument(
mock_should_run_integration_tests: Mock,
mock_should_run_clang_tidy: Mock,
mock_should_run_clang_format: Mock,
mock_should_run_python_linters: Mock,
mock_subprocess_run: Mock,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Test with branch argument."""
mock_should_run_integration_tests.return_value = False
mock_should_run_clang_tidy.return_value = True
mock_should_run_clang_format.return_value = False
mock_should_run_python_linters.return_value = True
# Mock list-components.py output
mock_result = Mock()
mock_result.stdout = "mqtt\n"
mock_subprocess_run.return_value = mock_result
with patch("sys.argv", ["script.py", "-b", "main"]):
determine_jobs.main()
# Check that functions were called with branch
mock_should_run_integration_tests.assert_called_once_with("main")
mock_should_run_clang_tidy.assert_called_once_with("main")
mock_should_run_clang_format.assert_called_once_with("main")
mock_should_run_python_linters.assert_called_once_with("main")
# 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 "-b" in call_args
assert "main" in call_args
# Check output
captured = capsys.readouterr()
output = json.loads(captured.out)
assert output["integration_tests"] is False
assert output["clang_tidy"] is True
assert output["clang_format"] is False
assert output["python_linters"] is True
assert output["changed_components"] == ["mqtt"]
assert output["component_test_count"] == 1
def test_should_run_integration_tests(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test should_run_integration_tests function."""
# Core C++ files trigger tests
with patch.object(
determine_jobs, "changed_files", return_value=["esphome/core/component.cpp"]
):
result = determine_jobs.should_run_integration_tests()
assert result is True
# Core Python files trigger tests
with patch.object(
determine_jobs, "changed_files", return_value=["esphome/core/config.py"]
):
result = determine_jobs.should_run_integration_tests()
assert result is True
# Python files directly in esphome/ do NOT trigger tests
with patch.object(
determine_jobs, "changed_files", return_value=["esphome/config.py"]
):
result = determine_jobs.should_run_integration_tests()
assert result is False
# Python files in subdirectories (not core) do NOT trigger tests
with patch.object(
determine_jobs,
"changed_files",
return_value=["esphome/dashboard/web_server.py"],
):
result = determine_jobs.should_run_integration_tests()
assert result is False
def test_should_run_integration_tests_with_branch() -> None:
"""Test should_run_integration_tests with branch argument."""
with patch.object(determine_jobs, "changed_files") as mock_changed:
mock_changed.return_value = []
determine_jobs.should_run_integration_tests("release")
mock_changed.assert_called_once_with("release")
def test_should_run_integration_tests_component_dependency() -> None:
"""Test that integration tests run when components used in fixtures change."""
with patch.object(
determine_jobs, "changed_files", return_value=["esphome/components/api/api.cpp"]
):
with patch.object(
determine_jobs, "get_components_from_integration_fixtures"
) as mock_fixtures:
mock_fixtures.return_value = {"api", "sensor"}
with patch.object(determine_jobs, "get_all_dependencies") as mock_deps:
mock_deps.return_value = {"api", "sensor", "network"}
result = determine_jobs.should_run_integration_tests()
assert result is True
@pytest.mark.parametrize(
("check_returncode", "changed_files", "expected_result"),
[
(0, [], True), # Hash changed - need full scan
(1, ["esphome/core.cpp"], True), # C++ file changed
(1, ["README.md"], False), # No C++ files changed
],
)
def test_should_run_clang_tidy(
check_returncode: int,
changed_files: list[str],
expected_result: bool,
) -> None:
"""Test should_run_clang_tidy function."""
with patch.object(determine_jobs, "changed_files", return_value=changed_files):
# Test with hash check returning specific code
with patch("subprocess.run") as mock_run:
mock_run.return_value = Mock(returncode=check_returncode)
result = determine_jobs.should_run_clang_tidy()
assert result == expected_result
# Test with hash check failing (exception)
if check_returncode != 0:
with patch("subprocess.run", side_effect=Exception("Failed")):
result = determine_jobs.should_run_clang_tidy()
assert result is True # Fail safe - run clang-tidy
def test_should_run_clang_tidy_with_branch() -> None:
"""Test should_run_clang_tidy with branch argument."""
with patch.object(determine_jobs, "changed_files") as mock_changed:
mock_changed.return_value = []
with patch("subprocess.run") as mock_run:
mock_run.return_value = Mock(returncode=1) # Hash unchanged
determine_jobs.should_run_clang_tidy("release")
mock_changed.assert_called_once_with("release")
@pytest.mark.parametrize(
("changed_files", "expected_result"),
[
(["esphome/core.py"], True),
(["script/test.py"], True),
(["esphome/test.pyi"], True), # .pyi files should trigger
(["README.md"], False),
([], False),
],
)
def test_should_run_python_linters(
changed_files: list[str], expected_result: bool
) -> None:
"""Test should_run_python_linters function."""
with patch.object(determine_jobs, "changed_files", return_value=changed_files):
result = determine_jobs.should_run_python_linters()
assert result == expected_result
def test_should_run_python_linters_with_branch() -> None:
"""Test should_run_python_linters with branch argument."""
with patch.object(determine_jobs, "changed_files") as mock_changed:
mock_changed.return_value = []
determine_jobs.should_run_python_linters("release")
mock_changed.assert_called_once_with("release")
@pytest.mark.parametrize(
("changed_files", "expected_result"),
[
(["esphome/core.cpp"], True),
(["esphome/core.h"], True),
(["test.hpp"], True),
(["test.cc"], True),
(["test.cxx"], True),
(["test.c"], True),
(["test.tcc"], True),
(["README.md"], False),
([], False),
],
)
def test_should_run_clang_format(
changed_files: list[str], expected_result: bool
) -> None:
"""Test should_run_clang_format function."""
with patch.object(determine_jobs, "changed_files", return_value=changed_files):
result = determine_jobs.should_run_clang_format()
assert result == expected_result
def test_should_run_clang_format_with_branch() -> None:
"""Test should_run_clang_format with branch argument."""
with patch.object(determine_jobs, "changed_files") as mock_changed:
mock_changed.return_value = []
determine_jobs.should_run_clang_format("release")
mock_changed.assert_called_once_with("release")

View File

@ -27,6 +27,7 @@ _filter_changed_ci = helpers._filter_changed_ci
_filter_changed_local = helpers._filter_changed_local
build_all_include = helpers.build_all_include
print_file_list = helpers.print_file_list
get_all_dependencies = helpers.get_all_dependencies
@pytest.mark.parametrize(
@ -154,6 +155,14 @@ def test_github_actions_push_event(monkeypatch: MonkeyPatch) -> None:
assert result == expected_files
@pytest.fixture(autouse=True)
def clear_caches():
"""Clear function caches before each test."""
# Clear the cache for _get_changed_files_github_actions
_get_changed_files_github_actions.cache_clear()
yield
def test_get_changed_files_github_actions_pull_request(
monkeypatch: MonkeyPatch,
) -> None:
@ -847,3 +856,159 @@ def test_print_file_list_default_title(capsys: pytest.CaptureFixture[str]) -> No
assert "Files:" in captured.out
assert " test.cpp" in captured.out
@pytest.mark.parametrize(
("component_configs", "initial_components", "expected_components"),
[
# No dependencies
(
{"sensor": ([], [])}, # (dependencies, auto_load)
{"sensor"},
{"sensor"},
),
# Simple dependencies
(
{
"sensor": (["esp32"], []),
"esp32": ([], []),
},
{"sensor"},
{"sensor", "esp32"},
),
# Auto-load components
(
{
"light": ([], ["output", "power_supply"]),
"output": ([], []),
"power_supply": ([], []),
},
{"light"},
{"light", "output", "power_supply"},
),
# Transitive dependencies
(
{
"comp_a": (["comp_b"], []),
"comp_b": (["comp_c"], []),
"comp_c": ([], []),
},
{"comp_a"},
{"comp_a", "comp_b", "comp_c"},
),
# Dependencies with dots (sensor.base)
(
{
"my_comp": (["sensor.base", "binary_sensor.base"], []),
"sensor": ([], []),
"binary_sensor": ([], []),
},
{"my_comp"},
{"my_comp", "sensor", "binary_sensor"},
),
# Circular dependencies (should not cause infinite loop)
(
{
"comp_a": (["comp_b"], []),
"comp_b": (["comp_a"], []),
},
{"comp_a"},
{"comp_a", "comp_b"},
),
],
)
def test_get_all_dependencies(
component_configs: dict[str, tuple[list[str], list[str]]],
initial_components: set[str],
expected_components: set[str],
) -> None:
"""Test dependency resolution for components."""
with patch("esphome.loader.get_component") as mock_get_component:
def get_component_side_effect(name: str):
if name in component_configs:
deps, auto_load = component_configs[name]
comp = Mock()
comp.dependencies = deps
comp.auto_load = auto_load
return comp
return None
mock_get_component.side_effect = get_component_side_effect
result = helpers.get_all_dependencies(initial_components)
assert result == expected_components
def test_get_all_dependencies_handles_missing_components() -> None:
"""Test handling of components that can't be loaded."""
with patch("esphome.loader.get_component") as mock_get_component:
# First component exists, its dependency doesn't
comp = Mock()
comp.dependencies = ["missing_comp"]
comp.auto_load = []
mock_get_component.side_effect = (
lambda name: comp if name == "existing" else None
)
result = helpers.get_all_dependencies({"existing", "nonexistent"})
# Should still include all components, even if some can't be loaded
assert result == {"existing", "nonexistent", "missing_comp"}
def test_get_all_dependencies_empty_set() -> None:
"""Test with empty initial component set."""
result = helpers.get_all_dependencies(set())
assert result == set()
def test_get_components_from_integration_fixtures() -> None:
"""Test extraction of components from fixture YAML files."""
yaml_content = {
"sensor": [{"platform": "template", "name": "test"}],
"binary_sensor": [{"platform": "gpio", "pin": 5}],
"esphome": {"name": "test"},
"api": {},
}
expected_components = {
"sensor",
"binary_sensor",
"esphome",
"api",
"template",
"gpio",
}
mock_yaml_file = Mock()
with (
patch("pathlib.Path.glob") as mock_glob,
patch("builtins.open", create=True),
patch("yaml.safe_load", return_value=yaml_content),
):
mock_glob.return_value = [mock_yaml_file]
components = helpers.get_components_from_integration_fixtures()
assert components == expected_components
@pytest.mark.parametrize(
"output,expected",
[
("wifi\napi\nsensor\n", ["wifi", "api", "sensor"]),
("wifi\n", ["wifi"]),
("", []),
(" \n \n", []),
("\n\n", []),
(" wifi \n api \n", ["wifi", "api"]),
("wifi\n\napi\n\nsensor", ["wifi", "api", "sensor"]),
],
)
def test_parse_list_components_output(output: str, expected: list[str]) -> None:
"""Test parse_list_components_output function."""
result = helpers.parse_list_components_output(output)
assert result == expected

View File

@ -8,9 +8,19 @@ from typing import Any
import pytest
from esphome.config_validation import Invalid
from esphome.const import CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ICON, CONF_NAME
from esphome.const import (
CONF_DEVICE_ID,
CONF_DISABLED_BY_DEFAULT,
CONF_ICON,
CONF_INTERNAL,
CONF_NAME,
)
from esphome.core import CORE, ID, entity_helpers
from esphome.core.entity_helpers import get_base_entity_object_id, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
get_base_entity_object_id,
setup_entity,
)
from esphome.cpp_generator import MockObj
from esphome.helpers import sanitize, snake_case
@ -493,11 +503,6 @@ async def test_setup_entity_disabled_by_default(
def test_entity_duplicate_validator() -> None:
"""Test the entity_duplicate_validator function."""
from esphome.core.entity_helpers import entity_duplicate_validator
# Reset CORE unique_ids for clean test
CORE.unique_ids.clear()
# Create validator for sensor platform
validator = entity_duplicate_validator("sensor")
@ -523,11 +528,6 @@ def test_entity_duplicate_validator() -> None:
def test_entity_duplicate_validator_with_devices() -> None:
"""Test entity_duplicate_validator with devices."""
from esphome.core.entity_helpers import entity_duplicate_validator
# Reset CORE unique_ids for clean test
CORE.unique_ids.clear()
# Create validator for sensor platform
validator = entity_duplicate_validator("sensor")
@ -605,3 +605,36 @@ def test_entity_different_platforms_yaml_validation(
)
# This should succeed
assert result is not None
def test_entity_duplicate_validator_internal_entities() -> None:
"""Test that internal entities are excluded from duplicate name validation."""
# Create validator for sensor platform
validator = entity_duplicate_validator("sensor")
# First entity should pass
config1 = {CONF_NAME: "Temperature"}
validated1 = validator(config1)
assert validated1 == config1
assert ("sensor", "temperature") in CORE.unique_ids
# Internal entity with same name should pass (not added to unique_ids)
config2 = {CONF_NAME: "Temperature", CONF_INTERNAL: True}
validated2 = validator(config2)
assert validated2 == config2
# Internal entity should not be added to unique_ids
assert len([k for k in CORE.unique_ids if k == ("sensor", "temperature")]) == 1
# Another internal entity with same name should also pass
config3 = {CONF_NAME: "Temperature", CONF_INTERNAL: True}
validated3 = validator(config3)
assert validated3 == config3
# Still only one entry in unique_ids (from the non-internal entity)
assert len([k for k in CORE.unique_ids if k == ("sensor", "temperature")]) == 1
# Non-internal entity with same name should fail
config4 = {CONF_NAME: "Temperature"}
with pytest.raises(
Invalid, match=r"Duplicate sensor entity with name 'Temperature' found"
):
validator(config4)