diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a2ba086394..f5d291b49f 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,7 +18,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v7 + - uses: actions/stale@v8 with: days-before-pr-stale: 90 days-before-pr-close: 7 @@ -38,7 +38,7 @@ jobs: close-issues: runs-on: ubuntu-latest steps: - - uses: actions/stale@v7 + - uses: actions/stale@v8 with: days-before-pr-stale: -1 days-before-pr-close: -1 diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 1cebdd0cbe..a64aac52e2 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1156,6 +1156,7 @@ enum BluetoothDeviceRequestType { BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR = 3; BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE = 4; BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE = 5; + BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE = 6; } message BluetoothDeviceRequest { @@ -1359,3 +1360,13 @@ message BluetoothDeviceUnpairingResponse { bool success = 2; int32 error = 3; } + +message BluetoothDeviceClearCacheResponse { + option (id) = 88; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + bool success = 2; + int32 error = 3; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 40a5a230a5..77ba96291a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -180,7 +180,8 @@ bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_ ListEntitiesBinarySensorResponse msg; msg.object_id = binary_sensor->get_object_id(); msg.key = binary_sensor->get_object_id_hash(); - msg.name = binary_sensor->get_name(); + if (binary_sensor->has_own_name()) + msg.name = binary_sensor->get_name(); msg.unique_id = get_default_unique_id("binary_sensor", binary_sensor); msg.device_class = binary_sensor->get_device_class(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); @@ -212,7 +213,8 @@ bool APIConnection::send_cover_info(cover::Cover *cover) { ListEntitiesCoverResponse msg; msg.key = cover->get_object_id_hash(); msg.object_id = cover->get_object_id(); - msg.name = cover->get_name(); + if (cover->has_own_name()) + msg.name = cover->get_name(); msg.unique_id = get_default_unique_id("cover", cover); msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); @@ -275,7 +277,8 @@ bool APIConnection::send_fan_info(fan::Fan *fan) { ListEntitiesFanResponse msg; msg.key = fan->get_object_id_hash(); msg.object_id = fan->get_object_id(); - msg.name = fan->get_name(); + if (fan->has_own_name()) + msg.name = fan->get_name(); msg.unique_id = get_default_unique_id("fan", fan); msg.supports_oscillation = traits.supports_oscillation(); msg.supports_speed = traits.supports_speed(); @@ -337,7 +340,8 @@ bool APIConnection::send_light_info(light::LightState *light) { ListEntitiesLightResponse msg; msg.key = light->get_object_id_hash(); msg.object_id = light->get_object_id(); - msg.name = light->get_name(); + if (light->has_own_name()) + msg.name = light->get_name(); msg.unique_id = get_default_unique_id("light", light); msg.disabled_by_default = light->is_disabled_by_default(); @@ -418,7 +422,8 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { ListEntitiesSensorResponse msg; msg.key = sensor->get_object_id_hash(); msg.object_id = sensor->get_object_id(); - msg.name = sensor->get_name(); + if (sensor->has_own_name()) + msg.name = sensor->get_name(); msg.unique_id = sensor->unique_id(); if (msg.unique_id.empty()) msg.unique_id = get_default_unique_id("sensor", sensor); @@ -448,7 +453,8 @@ bool APIConnection::send_switch_info(switch_::Switch *a_switch) { ListEntitiesSwitchResponse msg; msg.key = a_switch->get_object_id_hash(); msg.object_id = a_switch->get_object_id(); - msg.name = a_switch->get_name(); + if (a_switch->has_own_name()) + msg.name = a_switch->get_name(); msg.unique_id = get_default_unique_id("switch", a_switch); msg.icon = a_switch->get_icon(); msg.assumed_state = a_switch->assumed_state(); @@ -533,7 +539,8 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { ListEntitiesClimateResponse msg; msg.key = climate->get_object_id_hash(); msg.object_id = climate->get_object_id(); - msg.name = climate->get_name(); + if (climate->has_own_name()) + msg.name = climate->get_name(); msg.unique_id = get_default_unique_id("climate", climate); msg.disabled_by_default = climate->is_disabled_by_default(); @@ -611,7 +618,8 @@ bool APIConnection::send_number_info(number::Number *number) { ListEntitiesNumberResponse msg; msg.key = number->get_object_id_hash(); msg.object_id = number->get_object_id(); - msg.name = number->get_name(); + if (number->has_own_name()) + msg.name = number->get_name(); msg.unique_id = get_default_unique_id("number", number); msg.icon = number->get_icon(); msg.disabled_by_default = number->is_disabled_by_default(); @@ -652,7 +660,8 @@ bool APIConnection::send_select_info(select::Select *select) { ListEntitiesSelectResponse msg; msg.key = select->get_object_id_hash(); msg.object_id = select->get_object_id(); - msg.name = select->get_name(); + if (select->has_own_name()) + msg.name = select->get_name(); msg.unique_id = get_default_unique_id("select", select); msg.icon = select->get_icon(); msg.disabled_by_default = select->is_disabled_by_default(); @@ -679,7 +688,8 @@ bool APIConnection::send_button_info(button::Button *button) { ListEntitiesButtonResponse msg; msg.key = button->get_object_id_hash(); msg.object_id = button->get_object_id(); - msg.name = button->get_name(); + if (button->has_own_name()) + msg.name = button->get_name(); msg.unique_id = get_default_unique_id("button", button); msg.icon = button->get_icon(); msg.disabled_by_default = button->is_disabled_by_default(); @@ -710,7 +720,8 @@ bool APIConnection::send_lock_info(lock::Lock *a_lock) { ListEntitiesLockResponse msg; msg.key = a_lock->get_object_id_hash(); msg.object_id = a_lock->get_object_id(); - msg.name = a_lock->get_name(); + if (a_lock->has_own_name()) + msg.name = a_lock->get_name(); msg.unique_id = get_default_unique_id("lock", a_lock); msg.icon = a_lock->get_icon(); msg.assumed_state = a_lock->traits.get_assumed_state(); @@ -755,7 +766,8 @@ bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_play ListEntitiesMediaPlayerResponse msg; msg.key = media_player->get_object_id_hash(); msg.object_id = media_player->get_object_id(); - msg.name = media_player->get_name(); + if (media_player->has_own_name()) + msg.name = media_player->get_name(); msg.unique_id = get_default_unique_id("media_player", media_player); msg.icon = media_player->get_icon(); msg.disabled_by_default = media_player->is_disabled_by_default(); @@ -799,7 +811,8 @@ bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { ListEntitiesCameraResponse msg; msg.key = camera->get_object_id_hash(); msg.object_id = camera->get_object_id(); - msg.name = camera->get_name(); + if (camera->has_own_name()) + msg.name = camera->get_name(); msg.unique_id = get_default_unique_id("camera", camera); msg.disabled_by_default = camera->is_disabled_by_default(); msg.icon = camera->get_icon(); @@ -953,7 +966,9 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.webserver_port = USE_WEBSERVER_PORT; #endif #ifdef USE_BLUETOOTH_PROXY - resp.bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->has_active() ? 4 : 1; + resp.bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->has_active() + ? bluetooth_proxy::ACTIVE_CONNECTIONS_VERSION + : bluetooth_proxy::PASSIVE_ONLY_VERSION; #endif return resp; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 381f8b3c46..43587469af 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -400,6 +400,8 @@ const char *proto_enum_to_string(enums::Bluet return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE"; case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE"; + case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: + return "BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE"; default: return "UNKNOWN"; } @@ -6060,6 +6062,49 @@ void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { out.append("}"); } #endif +bool BluetoothDeviceClearCacheResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->success = value.as_bool(); + return true; + } + case 3: { + this->error = value.as_int32(); + return true; + } + default: + return false; + } +} +void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_bool(2, this->success); + buffer.encode_int32(3, this->error); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothDeviceClearCacheResponse {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" success: "); + out.append(YESNO(this->success)); + out.append("\n"); + + out.append(" error: "); + sprintf(buffer, "%d", this->error); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index e9025142e9..ff581cac6f 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -163,6 +163,7 @@ enum BluetoothDeviceRequestType : uint32_t { BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR = 3, BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE = 4, BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE = 5, + BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE = 6, }; } // namespace enums @@ -1554,6 +1555,19 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class BluetoothDeviceClearCacheResponse : public ProtoMessage { + public: + uint64_t address{0}; + bool success{false}; + int32_t error{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 7ee9e56192..73015fa914 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -441,6 +441,14 @@ bool APIServerConnectionBase::send_bluetooth_device_unpairing_response(const Blu return this->send_message_(msg, 86); } #endif +#ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_device_clear_cache_response(const BluetoothDeviceClearCacheResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_device_clear_cache_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 88); +} +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index f1879b2dba..7f19292ff3 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -215,6 +215,9 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_BLUETOOTH_PROXY bool send_bluetooth_device_unpairing_response(const BluetoothDeviceUnpairingResponse &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_device_clear_cache_response(const BluetoothDeviceClearCacheResponse &msg); #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index acde0966ba..c60766b364 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -331,6 +331,17 @@ void APIServer::send_bluetooth_device_unpairing(uint64_t address, bool success, } } +void APIServer::send_bluetooth_device_clear_cache(uint64_t address, bool success, esp_err_t error) { + BluetoothDeviceClearCacheResponse call; + call.address = address; + call.success = success; + call.error = error; + + for (auto &client : this->clients_) { + client->send_bluetooth_device_clear_cache_response(call); + } +} + void APIServer::send_bluetooth_connections_free(uint8_t free, uint8_t limit) { BluetoothConnectionsFreeResponse call; call.free = free; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 5f92e6b058..db87affdb8 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -80,6 +80,7 @@ class APIServer : public Component, public Controller { void send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK); void send_bluetooth_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK); void send_bluetooth_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK); + void send_bluetooth_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK); void send_bluetooth_connections_free(uint8_t free, uint8_t limit); void send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call); void send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 55fabf05ef..76950c944e 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -306,6 +306,13 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest api::global_api_server->send_bluetooth_device_unpairing(msg.address, ret == ESP_OK, ret); break; } + case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: { + esp_bd_addr_t address; + uint64_to_bd_addr(msg.address, address); + esp_err_t ret = esp_ble_gattc_cache_clean(address); + api::global_api_server->send_bluetooth_device_clear_cache(msg.address, ret == ESP_OK, ret); + break; + } } } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index b99e9a8527..a582abc8a3 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -68,6 +68,14 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +// Version 1: Initial version without active connections +// Version 2: Support for active connections +// Version 3: New connection API +// Version 4: Pairing support +// Version 5: Cache clear support +static const uint32_t ACTIVE_CONNECTIONS_VERSION = 5; +static const uint32_t PASSIVE_ONLY_VERSION = 1; + } // namespace bluetooth_proxy } // namespace esphome diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index c9813c4974..085d2a574b 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c +from esphome.components import i2c, esp32 from esphome.const import CONF_ID CODEOWNERS = ["@trvrnrth"] @@ -32,22 +32,31 @@ BME680BSECComponent = bme680_bsec_ns.class_( "BME680BSECComponent", cg.Component, i2c.I2CDevice ) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(BME680BSECComponent), - cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature, - cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum( - IAQ_MODE_OPTIONS, upper=True - ), - cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum( - SAMPLE_RATE_OPTIONS, upper=True - ), - cv.Optional( - CONF_STATE_SAVE_INTERVAL, default="6hours" - ): cv.positive_time_period_minutes, - }, +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BME680BSECComponent), + cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature, + cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum( + IAQ_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum( + SAMPLE_RATE_OPTIONS, upper=True + ), + cv.Optional( + CONF_STATE_SAVE_INTERVAL, default="6hours" + ): cv.positive_time_period_minutes, + } + ).extend(i2c.i2c_device_schema(0x76)), cv.only_with_arduino, -).extend(i2c.i2c_device_schema(0x76)) + cv.Any( + cv.only_on_esp8266, + cv.All( + cv.only_on_esp32, + esp32.only_on_variant(supported=[esp32.const.VARIANT_ESP32]), + ), + ), +) async def to_code(config): diff --git a/esphome/components/copy/select/__init__.py b/esphome/components/copy/select/__init__.py index 7d4c1c7705..6254ed03e1 100644 --- a/esphome/components/copy/select/__init__.py +++ b/esphome/components/copy/select/__init__.py @@ -14,12 +14,15 @@ from .. import copy_ns CopySelect = copy_ns.class_("CopySelect", select.Select, cg.Component) -CONFIG_SCHEMA = select.SELECT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(CopySelect), - cv.Required(CONF_SOURCE_ID): cv.use_id(select.Select), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + select.select_schema(CopySelect) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(select.Select), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = cv.All( inherit_property_from(CONF_ICON, CONF_SOURCE_ID), diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index d6a744d24d..4cbdf7ca5c 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -156,7 +156,7 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( - cv.frequency, cv.Range(min=10e6, max=20e6) + cv.frequency, cv.Range(min=8e6, max=20e6) ), } ), diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 7170a6dabf..ae7f0b6427 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -22,20 +22,12 @@ ESP32ImprovComponent = esp32_improv_ns.class_( ) -def validate_none_(value): - if value in ("none", "None"): - return None - if cv.boolean(value) is False: - return None - raise cv.Invalid("Must be none") - - CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), cv.GenerateID(CONF_BLE_SERVER_ID): cv.use_id(esp32_ble_server.BLEServer), cv.Required(CONF_AUTHORIZER): cv.Any( - validate_none_, cv.use_id(binary_sensor.BinarySensor) + cv.none, cv.use_id(binary_sensor.BinarySensor) ), cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput), cv.Optional( diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py index f8ef61ddc4..5692fea3e3 100644 --- a/esphome/components/modbus_controller/select/__init__.py +++ b/esphome/components/modbus_controller/select/__init__.py @@ -64,9 +64,10 @@ INTEGER_SENSOR_VALUE_TYPE = { } CONFIG_SCHEMA = cv.All( - select.SELECT_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + select.select_schema(ModbusSelect) + .extend(cv.COMPONENT_SCHEMA) + .extend( { - cv.GenerateID(): cv.declare_id(ModbusSelect), cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), cv.Required(CONF_ADDRESS): cv.positive_int, cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum( diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 70c53cace3..7db973612b 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -204,14 +204,13 @@ def number_schema( async def setup_number_core_( - var, config, *, min_value: float, max_value: float, step: Optional[float] + var, config, *, min_value: float, max_value: float, step: float ): await setup_entity(var, config) cg.add(var.traits.set_min_value(min_value)) cg.add(var.traits.set_max_value(max_value)) - if step is not None: - cg.add(var.traits.set_step(step)) + cg.add(var.traits.set_step(step)) cg.add(var.traits.set_mode(config[CONF_MODE])) @@ -239,7 +238,7 @@ async def setup_number_core_( async def register_number( - var, config, *, min_value: float, max_value: float, step: Optional[float] = None + var, config, *, min_value: float, max_value: float, step: float ): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index b505d89c6f..760f7600b7 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -3,6 +3,8 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import mqtt from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, CONF_ID, CONF_ON_VALUE, CONF_OPTION, @@ -14,6 +16,7 @@ from esphome.const import ( CONF_INDEX, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] @@ -43,8 +46,6 @@ SELECT_OPERATION_OPTIONS = { "LAST": SelectOperation.SELECT_OP_LAST, } -icon = cv.icon - SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { @@ -58,6 +59,30 @@ SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).e } ) +_UNDEF = object() + + +def select_schema( + class_: MockObjClass = _UNDEF, + *, + entity_category: str = _UNDEF, + icon: str = _UNDEF, +): + schema = cv.Schema({}) + if class_ is not _UNDEF: + schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) + if entity_category is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_ENTITY_CATEGORY, default=entity_category + ): cv.entity_category + } + ) + if icon is not _UNDEF: + schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) + return SELECT_SCHEMA.extend(schema) + async def setup_select_core_(var, config, *, options: list[str]): await setup_entity(var, config) diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 60cbae6aa6..d0a84b99ff 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -42,6 +42,9 @@ void SX1509Component::dump_config() { void SX1509Component::loop() { if (this->has_keypad_) { + if (millis() - this->last_loop_timestamp_ < min_loop_period_) + return; + this->last_loop_timestamp_ = millis(); uint16_t key_data = this->read_key_data(); for (auto *binary_sensor : this->keypad_binary_sensors_) binary_sensor->process(key_data); diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h index 50230b1086..8e3b41e233 100644 --- a/esphome/components/sx1509/sx1509.h +++ b/esphome/components/sx1509/sx1509.h @@ -69,6 +69,9 @@ class SX1509Component : public Component, public i2c::I2CDevice { uint8_t debounce_time_ = 1; std::vector keypad_binary_sensors_; + uint32_t last_loop_timestamp_ = 0; + const uint32_t min_loop_period_ = 15; // ms + void setup_keypad_(); void set_debounce_config_(uint8_t config_value); void set_debounce_time_(uint8_t time); diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 4eba77119d..d116cbb8ae 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -43,9 +43,9 @@ def validate(config): CONFIG_SCHEMA = cv.All( - select.SELECT_SCHEMA.extend( + select.select_schema(TemplateSelect) + .extend( { - cv.GenerateID(): cv.declare_id(TemplateSelect), cv.Required(CONF_OPTIONS): cv.All( cv.ensure_list(cv.string_strict), cv.Length(min=1) ), @@ -55,7 +55,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INITIAL_OPTION): cv.string_strict, cv.Optional(CONF_RESTORE_VALUE): cv.boolean, } - ).extend(cv.polling_component_schema("60s")), + ) + .extend(cv.polling_component_schema("60s")), validate, ) diff --git a/esphome/components/tuya/select/__init__.py b/esphome/components/tuya/select/__init__.py index 3d65eda301..dc78b2c3db 100644 --- a/esphome/components/tuya/select/__init__.py +++ b/esphome/components/tuya/select/__init__.py @@ -25,15 +25,18 @@ def ensure_option_map(value): return value -CONFIG_SCHEMA = select.SELECT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TuyaSelect), - cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), - cv.Required(CONF_ENUM_DATAPOINT): cv.uint8_t, - cv.Required(CONF_OPTIONS): ensure_option_map, - cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + select.select_schema(TuyaSelect) + .extend( + { + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_ENUM_DATAPOINT): cv.uint8_t, + cv.Required(CONF_OPTIONS): ensure_option_map, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): diff --git a/esphome/config_validation.py b/esphome/config_validation.py index f0bbc368b8..4b822b46c9 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1514,6 +1514,8 @@ def _entity_base_validator(config): config[CONF_NAME] = id.id config[CONF_INTERNAL] = True return config + if config[CONF_NAME] is None: + config[CONF_NAME] = "" return config @@ -1573,6 +1575,23 @@ def validate_registry_entry(name, registry): return validator +def none(value): + if value in ("none", "None"): + return None + if boolean(value) is False: + return None + raise Invalid("Must be none") + + +def requires_friendly_name(message): + def validate(value): + if CORE.friendly_name is None: + raise Invalid(message) + return value + + return validate + + def validate_registry(name, registry): return ensure_list(validate_registry_entry(name, registry)) @@ -1632,7 +1651,15 @@ MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend( ENTITY_BASE_SCHEMA = Schema( { - Optional(CONF_NAME): string, + Optional(CONF_NAME): Any( + All( + none, + requires_friendly_name( + "Name cannot be None when esphome->friendly_name is not set!" + ), + ), + string, + ), Optional(CONF_INTERNAL): boolean, Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean, Optional(CONF_ICON): icon, diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 6f88f069b3..49a73854a1 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -1,4 +1,5 @@ #include "esphome/core/entity_base.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" namespace esphome { @@ -10,7 +11,13 @@ EntityBase::EntityBase(std::string name) : name_(std::move(name)) { this->calc_o // Entity Name const std::string &EntityBase::get_name() const { return this->name_; } void EntityBase::set_name(const std::string &name) { - this->name_ = name; + if (name.empty()) { + this->name_ = App.get_friendly_name(); + this->has_own_name_ = false; + } else { + this->name_ = name; + this->has_own_name_ = true; + } this->calc_object_id_(); } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index f6aae4e978..5ab1f7b424 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -21,6 +21,9 @@ class EntityBase { const std::string &get_name() const; void set_name(const std::string &name); + // Get whether this Entity has its own name or it should use the device friendly_name. + bool has_own_name() const { return this->has_own_name_; } + // Get the sanitized name of this Entity as an ID. Caching it internally. const std::string &get_object_id(); @@ -52,6 +55,7 @@ class EntityBase { void calc_object_id_(); std::string name_; + bool has_own_name_{false}; std::string object_id_; const char *icon_c_str_{nullptr}; uint32_t object_id_hash_;