diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 7eb4cd1f66..86035b9259 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -28,6 +28,7 @@ jobs: name: Build docker containers runs-on: ubuntu-latest strategy: + fail-fast: false matrix: arch: [amd64, armv7, aarch64] build_type: ["ha-addon", "docker", "lint"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5b7521b3f..671b74d118 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,7 +85,7 @@ jobs: uses: actions/setup-python@v4 id: python with: - python-version: "3.8" + python-version: "3.9" - name: Cache virtualenv uses: actions/cache@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 083aea117d..9c5610d9dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - --branch=release - --branch=beta - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v3.0.0 hooks: - id: pyupgrade - args: [--py38-plus] + args: [--py39-plus] diff --git a/CODEOWNERS b/CODEOWNERS index d95ebcce59..b04b480780 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -258,4 +258,4 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_rtcgq02lm/* @jesserockz -esphome/components/xpt2046/* @numo68 +esphome/components/xpt2046/* @nielsnl68 @numo68 diff --git a/esphome/codegen.py b/esphome/codegen.py index 185e6599b1..ef5b490004 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -22,6 +22,7 @@ from esphome.cpp_generator import ( # noqa static_const_array, statement, variable, + with_local_variable, new_variable, Pvariable, new_Pvariable, diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 3a150146e2..87d72254e8 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["display"] MULTI_CONF = True -Animation_ = display.display_ns.class_("Animation") +Animation_ = display.display_ns.class_("Animation", espImage.Image_) ANIMATION_SCHEMA = cv.Schema( { diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 9fa77d2daa..a8e45e1ea6 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1298,3 +1298,31 @@ message BluetoothConnectionsFreeResponse { uint32 free = 1; uint32 limit = 2; } + +message BluetoothGATTErrorResponse { + option (id) = 82; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; + int32 error = 3; +} + +message BluetoothGATTWriteResponse { + option (id) = 83; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; +} + +message BluetoothGATTNotifyResponse { + option (id) = 84; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; +} diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 73d8044cde..8cb244f1a1 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -5746,6 +5746,118 @@ void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { out.append("}"); } #endif +bool BluetoothGATTErrorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->handle = value.as_uint32(); + return true; + } + case 3: { + this->error = value.as_int32(); + return true; + } + default: + return false; + } +} +void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_uint32(2, this->handle); + buffer.encode_int32(3, this->error); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTErrorResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTErrorResponse {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + sprintf(buffer, "%u", this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" error: "); + sprintf(buffer, "%d", this->error); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothGATTWriteResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->handle = value.as_uint32(); + return true; + } + default: + return false; + } +} +void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_uint32(2, this->handle); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTWriteResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTWriteResponse {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + sprintf(buffer, "%u", this->handle); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothGATTNotifyResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->handle = value.as_uint32(); + return true; + } + default: + return false; + } +} +void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_uint32(2, this->handle); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothGATTNotifyResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTNotifyResponse {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + sprintf(buffer, "%u", this->handle); + 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 325e9a23c3..fb676f1cc8 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1481,6 +1481,43 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class BluetoothGATTErrorResponse : public ProtoMessage { + public: + uint64_t address{0}; + uint32_t handle{0}; + 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; +}; +class BluetoothGATTWriteResponse : public ProtoMessage { + public: + uint64_t address{0}; + uint32_t handle{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; +}; +class BluetoothGATTNotifyResponse : public ProtoMessage { + public: + uint64_t address{0}; + uint32_t handle{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 7bfe1acf48..b603ade9de 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -401,6 +401,30 @@ bool APIServerConnectionBase::send_bluetooth_connections_free_response(const Blu return this->send_message_(msg, 81); } #endif +#ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_gatt_error_response(const BluetoothGATTErrorResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_gatt_error_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 82); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_gatt_write_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 83); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_gatt_notify_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 84); +} +#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 c7ef1468d8..3cb8b59ba5 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -200,6 +200,15 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_BLUETOOTH_PROXY bool send_bluetooth_connections_free_response(const BluetoothConnectionsFreeResponse &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_gatt_error_response(const BluetoothGATTErrorResponse &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &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 965f08aa15..dbd732f466 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -324,11 +324,21 @@ void APIServer::send_bluetooth_gatt_read_response(const BluetoothGATTReadRespons client->send_bluetooth_gatt_read_response(call); } } +void APIServer::send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call) { + for (auto &client : this->clients_) { + client->send_bluetooth_gatt_write_response(call); + } +} void APIServer::send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call) { for (auto &client : this->clients_) { client->send_bluetooth_gatt_notify_data_response(call); } } +void APIServer::send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &call) { + for (auto &client : this->clients_) { + client->send_bluetooth_gatt_notify_response(call); + } +} void APIServer::send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call) { for (auto &client : this->clients_) { client->send_bluetooth_gatt_get_services_response(call); @@ -342,6 +352,17 @@ void APIServer::send_bluetooth_gatt_services_done(uint64_t address) { client->send_bluetooth_gatt_get_services_done_response(call); } } +void APIServer::send_bluetooth_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { + BluetoothGATTErrorResponse call; + call.address = address; + call.handle = handle; + call.error = error; + + for (auto &client : this->clients_) { + client->send_bluetooth_gatt_error_response(call); + } +} + #endif APIServer::APIServer() { global_api_server = this; } void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 72ab55432c..ec8c54a269 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -78,9 +78,12 @@ class APIServer : public Component, public Controller { void send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu, 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); void send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call); + void send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &call); void send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call); void send_bluetooth_gatt_services_done(uint64_t address); + void send_bluetooth_gatt_error(uint64_t address, uint16_t handle, esp_err_t error); #endif void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #ifdef USE_HOMEASSISTANT_TIME diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 0f1f60e62b..44812d29b5 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -100,12 +100,40 @@ async def ble_write_to_code(config, action_id, template_arg, args): else: cg.add(var.set_value_simple(value)) - serv_uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) - cg.add(var.set_service_uuid128(serv_uuid128)) - char_uuid128 = esp32_ble_tracker.as_reversed_hex_array( - config[CONF_CHARACTERISTIC_UUID] - ) - cg.add(var.set_char_uuid128(char_uuid128)) + if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add( + var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) + + if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_char_uuid16( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_char_uuid32( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_CHARACTERISTIC_UUID] + ) + cg.add(var.set_char_uuid128(uuid128)) + return var diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index ba5f78109e..98c19dedf8 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -46,10 +46,14 @@ class BLEWriterClientNode : public BLEClientNode { // Attempts to write the contents of value to char_uuid_. void write(const std::vector &value); - void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } - + void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 3f86df32f5..a757d6f903 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -64,6 +64,13 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es } } +void BLEClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + BLEClientBase::gap_event_handler(event, param); + + for (auto *node : this->nodes_) + node->gap_event_handler(event, param); +} + void BLEClient::set_state(espbt::ClientState state) { BLEClientBase::set_state(state); for (auto &node : nodes_) diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index 0f8373ab1f..177cab2e3c 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -27,7 +27,8 @@ class BLEClientNode { public: virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) = 0; - virtual void loop(){}; + virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {} + virtual void loop() {} void set_address(uint64_t address) { address_ = address; } espbt::ESPBTClient *client; // This should be transitioned to Established once the node no longer needs @@ -51,6 +52,8 @@ class BLEClient : public BLEClientBase { void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; + + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; bool parse_device(const espbt::ESPBTDevice &device) override; void set_enabled(bool enabled); diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py index e8f84d2542..c9bf2995b1 100644 --- a/esphome/components/ble_client/sensor/__init__.py +++ b/esphome/components/ble_client/sensor/__init__.py @@ -5,7 +5,11 @@ from esphome.const import ( CONF_CHARACTERISTIC_UUID, CONF_LAMBDA, CONF_TRIGGER_ID, + CONF_TYPE, CONF_SERVICE_UUID, + DEVICE_CLASS_SIGNAL_STRENGTH, + STATE_CLASS_MEASUREMENT, + UNIT_DECIBEL_MILLIWATT, ) from esphome import automation from .. import ble_client_ns @@ -16,6 +20,8 @@ CONF_DESCRIPTOR_UUID = "descriptor_uuid" CONF_NOTIFY = "notify" CONF_ON_NOTIFY = "on_notify" +TYPE_CHARACTERISTIC = "characteristic" +TYPE_RSSI = "rssi" adv_data_t = cg.std_vector.template(cg.uint8) adv_data_t_const_ref = adv_data_t.operator("ref").operator("const") @@ -27,33 +33,67 @@ BLESensorNotifyTrigger = ble_client_ns.class_( "BLESensorNotifyTrigger", automation.Trigger.template(cg.float_) ) -CONFIG_SCHEMA = cv.All( - sensor.sensor_schema( - BLESensor, - accuracy_decimals=0, - ) - .extend( - { - cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, - cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, - cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid, - cv.Optional(CONF_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_NOTIFY, default=False): cv.boolean, - cv.Optional(CONF_ON_NOTIFY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - BLESensorNotifyTrigger - ), - } - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(ble_client.BLE_CLIENT_SCHEMA) +BLEClientRssiSensor = ble_client_ns.class_( + "BLEClientRSSISensor", sensor.Sensor, cg.PollingComponent, ble_client.BLEClientNode ) -async def to_code(config): +def checkType(value): + if CONF_TYPE not in value and CONF_SERVICE_UUID in value: + raise cv.Invalid( + "Looks like you're trying to create a ble characteristic sensor. Please add `type: characteristic` to your sensor config." + ) + return value + + +CONFIG_SCHEMA = cv.All( + checkType, + cv.typed_schema( + { + TYPE_CHARACTERISTIC: sensor.sensor_schema( + BLESensor, + accuracy_decimals=0, + ) + .extend(cv.polling_component_schema("60s")) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend( + { + cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_NOTIFY, default=False): cv.boolean, + cv.Optional(CONF_ON_NOTIFY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLESensorNotifyTrigger + ), + } + ), + } + ), + TYPE_RSSI: sensor.sensor_schema( + BLEClientRssiSensor, + accuracy_decimals=0, + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(ble_client.BLE_CLIENT_SCHEMA), + }, + lower=True, + ), +) + + +async def rssi_sensor_to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await ble_client.register_ble_node(var, config) + + +async def characteristic_sensor_to_code(config): var = await sensor.new_sensor(config) if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): cg.add( @@ -125,3 +165,10 @@ async def to_code(config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await ble_client.register_ble_node(trigger, config) await automation.build_automation(trigger, [(float, "x")], conf) + + +async def to_code(config): + if config[CONF_TYPE] == TYPE_RSSI: + await rssi_sensor_to_code(config) + elif config[CONF_TYPE] == TYPE_CHARACTERISTIC: + await characteristic_sensor_to_code(config) diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp new file mode 100644 index 0000000000..13e51ed5b3 --- /dev/null +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -0,0 +1,78 @@ +#include "ble_rssi_sensor.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *const TAG = "ble_rssi_sensor"; + +void BLEClientRSSISensor::loop() {} + +void BLEClientRSSISensor::dump_config() { + LOG_SENSOR("", "BLE Client RSSI Sensor", this); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); + LOG_UPDATE_INTERVAL(this); +} + +void BLEClientRSSISensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + if (param->open.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str()); + break; + } + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); + this->status_set_warning(); + this->publish_state(NAN); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: + this->node_state = espbt::ClientState::ESTABLISHED; + break; + default: + break; + } +} + +void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + // server response on RSSI request: + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) { + int8_t rssi = param->read_rssi_cmpl.rssi; + ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi); + this->publish_state(rssi); + } + break; + default: + break; + } +} + +void BLEClientRSSISensor::update() { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str()); + return; + } + + ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str().c_str()); + auto status = esp_ble_gap_read_rssi(this->parent()->get_remote_bda()); + if (status != ESP_OK) { + ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str().c_str(), status); + this->status_set_warning(); + this->publish_state(NAN); + } +} + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.h b/esphome/components/ble_client/sensor/ble_rssi_sensor.h new file mode 100644 index 0000000000..028df83832 --- /dev/null +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" + +#ifdef USE_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, public BLEClientNode { + public: + void loop() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index dcccf844d2..1689c9ba3f 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -58,7 +58,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, case MATCH_BY_SERVICE_UUID: for (auto uuid : device.get_service_uuids()) { if (this->uuid_ == uuid) { - this->publish_state(device.get_rssi()); + this->publish_state(true); this->found_ = true; return true; } @@ -83,7 +83,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, return false; } - this->publish_state(device.get_rssi()); + this->publish_state(true); this->found_ = true; return true; } diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py index bd8e4f98e3..7c7bfc58a7 100644 --- a/esphome/components/ble_rssi/sensor.py +++ b/esphome/components/ble_rssi/sensor.py @@ -9,7 +9,7 @@ from esphome.const import ( CONF_MAC_ADDRESS, DEVICE_CLASS_SIGNAL_STRENGTH, STATE_CLASS_MEASUREMENT, - UNIT_DECIBEL, + UNIT_DECIBEL_MILLIWATT, ) DEPENDENCIES = ["esp32_ble_tracker"] @@ -31,7 +31,7 @@ def _validate(config): CONFIG_SCHEMA = cv.All( sensor.sensor_schema( BLERSSISensor, - unit_of_measurement=UNIT_DECIBEL, + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, accuracy_decimals=0, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, state_class=STATE_CLASS_MEASUREMENT, diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index b5acea89dd..84841d9bf4 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -4,7 +4,7 @@ import esphome.codegen as cg from esphome.const import CONF_ACTIVE, CONF_ID AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"] -DEPENDENCIES = ["esp32"] +DEPENDENCIES = ["api", "esp32"] CODEOWNERS = ["@jesserockz"] diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 96fee39f95..3f06cd5c25 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -4,43 +4,36 @@ #ifdef USE_ESP32 -#ifdef USE_API #include "esphome/components/api/api_server.h" -#endif namespace esphome { namespace bluetooth_proxy { static const char *const TAG = "bluetooth_proxy"; +static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; +static const esp_err_t ESP_GATT_WRONG_ADDRESS = -2; + BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; this->address_ = 0; } bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (!api::global_api_server->is_connected()) + return false; ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(), device.get_rssi()); this->send_api_packet_(device); - this->address_type_map_[device.address_uint64()] = device.get_address_type(); - if (this->address_ == 0) return true; - if (this->state_ == espbt::ClientState::DISCOVERED) { - ESP_LOGV(TAG, "Connecting to address %s", this->address_str().c_str()); - return true; - } - BLEClientBase::parse_device(device); return true; } void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { -#ifndef USE_API - return; -#else api::BluetoothLEAdvertisementResponse resp; resp.address = device.address_uint64(); if (!device.get_name().empty()) @@ -62,7 +55,113 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi resp.manufacturer_data.push_back(std::move(manufacturer_data)); } api::global_api_server->send_bluetooth_le_advertisement(resp); -#endif +} + +void BluetoothProxy::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + BLEClientBase::gattc_event_handler(event, gattc_if, param); + switch (event) { + case ESP_GATTC_DISCONNECT_EVT: { + api::global_api_server->send_bluetooth_device_connection(this->address_, false, this->mtu_, + param->disconnect.reason); + api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), + this->get_bluetooth_connections_limit()); + this->address_ = 0; + } + case ESP_GATTC_OPEN_EVT: { + if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { + api::global_api_server->send_bluetooth_device_connection(this->address_, false, this->mtu_, param->open.status); + break; + } + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); + api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), + this->get_bluetooth_connections_limit()); + break; + } + case ESP_GATTC_READ_DESCR_EVT: + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->conn_id_) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char/descriptor at handle 0x%2X, status=%d", param->read.handle, + param->read.status); + api::global_api_server->send_bluetooth_gatt_error(this->address_, param->read.handle, param->read.status); + break; + } + api::BluetoothGATTReadResponse resp; + resp.address = this->address_; + resp.handle = param->read.handle; + resp.data.reserve(param->read.value_len); + for (uint16_t i = 0; i < param->read.value_len; i++) { + resp.data.push_back(param->read.value[i]); + } + api::global_api_server->send_bluetooth_gatt_read_response(resp); + break; + } + case ESP_GATTC_WRITE_CHAR_EVT: + case ESP_GATTC_WRITE_DESCR_EVT: { + if (param->write.conn_id != this->conn_id_) + break; + if (param->write.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error writing char/descriptor at handle 0x%2X, status=%d", param->write.handle, + param->write.status); + api::global_api_server->send_bluetooth_gatt_error(this->address_, param->write.handle, param->write.status); + break; + } + api::BluetoothGATTWriteResponse resp; + resp.address = this->address_; + resp.handle = param->write.handle; + api::global_api_server->send_bluetooth_gatt_write_response(resp); + break; + } + case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { + if (param->unreg_for_notify.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error unregistering notifications for handle 0x%2X, status=%d", param->unreg_for_notify.handle, + param->unreg_for_notify.status); + api::global_api_server->send_bluetooth_gatt_error(this->address_, param->unreg_for_notify.handle, + param->unreg_for_notify.status); + break; + } + api::BluetoothGATTNotifyResponse resp; + resp.address = this->address_; + resp.handle = param->unreg_for_notify.handle; + api::global_api_server->send_bluetooth_gatt_notify_response(resp); + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + if (param->reg_for_notify.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error registering notifications for handle 0x%2X, status=%d", param->reg_for_notify.handle, + param->reg_for_notify.status); + api::global_api_server->send_bluetooth_gatt_error(this->address_, param->reg_for_notify.handle, + param->reg_for_notify.status); + break; + } + api::BluetoothGATTNotifyResponse resp; + resp.address = this->address_; + resp.handle = param->reg_for_notify.handle; + api::global_api_server->send_bluetooth_gatt_notify_response(resp); + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->conn_id_) + break; + ESP_LOGV(TAG, "ESP_GATTC_NOTIFY_EVT: handle=0x%2X", param->notify.handle); + api::BluetoothGATTNotifyDataResponse resp; + resp.address = this->address_; + resp.handle = param->notify.handle; + resp.data.reserve(param->notify.value_len); + for (uint16_t i = 0; i < param->notify.value_len; i++) { + resp.data.push_back(param->notify.value[i]); + } + api::global_api_server->send_bluetooth_gatt_notify_data_response(resp); + break; + } + default: + break; + } } void BluetoothProxy::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, @@ -141,7 +240,6 @@ void BluetoothProxy::dump_config() { ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); } void BluetoothProxy::loop() { BLEClientBase::loop(); -#ifdef USE_API if (this->state_ != espbt::ClientState::IDLE && !api::global_api_server->is_connected()) { ESP_LOGI(TAG, "[%s] Disconnecting.", this->address_str().c_str()); auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_); @@ -177,28 +275,12 @@ void BluetoothProxy::loop() { api::global_api_server->send_bluetooth_gatt_services(resp); this->send_service_++; } -#endif } -#ifdef USE_API void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest &msg) { switch (msg.request_type) { case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: { this->address_ = msg.address; - if (this->address_type_map_.find(this->address_) != this->address_type_map_.end()) { - // Utilise the address type cache - this->remote_addr_type_ = this->address_type_map_[this->address_]; - } else { - this->remote_addr_type_ = BLE_ADDR_TYPE_PUBLIC; - } - this->remote_bda_[0] = (this->address_ >> 40) & 0xFF; - this->remote_bda_[1] = (this->address_ >> 32) & 0xFF; - this->remote_bda_[2] = (this->address_ >> 24) & 0xFF; - this->remote_bda_[3] = (this->address_ >> 16) & 0xFF; - this->remote_bda_[4] = (this->address_ >> 8) & 0xFF; - this->remote_bda_[5] = (this->address_ >> 0) & 0xFF; - this->set_state(espbt::ClientState::DISCOVERED); - esp_ble_gap_stop_scanning(); break; } case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: { @@ -220,16 +302,19 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) { if (this->state_ != espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected."); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } if (this->address_ != msg.address) { ESP_LOGW(TAG, "Address mismatch for read GATT characteristic request"); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_WRONG_ADDRESS); return; } auto *characteristic = this->get_characteristic(msg.handle); if (characteristic == nullptr) { ESP_LOGW(TAG, "Cannot read GATT characteristic, not found."); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_INVALID_HANDLE); return; } @@ -239,43 +324,53 @@ void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &ms esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, characteristic->handle, ESP_GATT_AUTH_REQ_NONE); if (err != ERR_OK) { ESP_LOGW(TAG, "esp_ble_gattc_read_char error, err=%d", err); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); } } void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) { if (this->state_ != espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected."); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } if (this->address_ != msg.address) { ESP_LOGW(TAG, "Address mismatch for write GATT characteristic request"); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_WRONG_ADDRESS); return; } auto *characteristic = this->get_characteristic(msg.handle); if (characteristic == nullptr) { ESP_LOGW(TAG, "Cannot write GATT characteristic, not found."); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_INVALID_HANDLE); return; } ESP_LOGV(TAG, "Writing GATT characteristic %s", characteristic->uuid.to_string().c_str()); - characteristic->write_value((uint8_t *) msg.data.data(), msg.data.size(), - msg.response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP); + auto err = characteristic->write_value((uint8_t *) msg.data.data(), msg.data.size(), + msg.response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP); + if (err != ERR_OK) { + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + } } void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) { if (this->state_ != espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "Cannot read GATT characteristic descriptor, not connected."); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } if (this->address_ != msg.address) { ESP_LOGW(TAG, "Address mismatch for read GATT characteristic descriptor request"); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_WRONG_ADDRESS); return; } auto *descriptor = this->get_descriptor(msg.handle); if (descriptor == nullptr) { ESP_LOGW(TAG, "Cannot read GATT characteristic descriptor, not found."); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_INVALID_HANDLE); return; } @@ -286,22 +381,26 @@ void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTRead esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, descriptor->handle, ESP_GATT_AUTH_REQ_NONE); if (err != ERR_OK) { ESP_LOGW(TAG, "esp_ble_gattc_read_char error, err=%d", err); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); } } void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) { if (this->state_ != espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "Cannot write GATT characteristic descriptor, not connected."); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } if (this->address_ != msg.address) { ESP_LOGW(TAG, "Address mismatch for write GATT characteristic descriptor request"); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_WRONG_ADDRESS); return; } auto *descriptor = this->get_descriptor(msg.handle); if (descriptor == nullptr) { ESP_LOGW(TAG, "Cannot write GATT characteristic descriptor, not found."); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_INVALID_HANDLE); return; } @@ -313,20 +412,34 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri (uint8_t *) msg.data.data(), ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (err != ERR_OK) { ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, err=%d", err); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); } } void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) { + if (this->state_ != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "Cannot get GATT services, not connected."); + api::global_api_server->send_bluetooth_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED); + return; + } if (this->address_ != msg.address) { ESP_LOGW(TAG, "Address mismatch for service list request"); + api::global_api_server->send_bluetooth_gatt_error(msg.address, 0, ESP_GATT_WRONG_ADDRESS); return; } this->send_service_ = 0; } void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) { + if (this->state_ != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "Cannot configure notify, not connected."); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + return; + } + if (this->address_ != msg.address) { ESP_LOGW(TAG, "Address mismatch for notify"); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_WRONG_ADDRESS); return; } @@ -334,6 +447,7 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest if (characteristic == nullptr) { ESP_LOGW(TAG, "Cannot notify GATT characteristic, not found."); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_INVALID_HANDLE); return; } @@ -342,17 +456,17 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, characteristic->handle); if (err != ESP_OK) { ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, err=%d", err); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); } } else { err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, characteristic->handle); if (err != ESP_OK) { ESP_LOGW(TAG, "esp_ble_gattc_unregister_for_notify failed, err=%d", err); + api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); } } } -#endif - BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace bluetooth_proxy diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 8ff43aff3f..9529b99f73 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -4,6 +4,7 @@ #include +#include "esphome/components/api/api_pb2.h" #include "esphome/components/esp32_ble_client/ble_client_base.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/core/automation.h" @@ -12,10 +13,6 @@ #include -#ifdef USE_API -#include "esphome/components/api/api_pb2.h" -#endif // USE_API - namespace esphome { namespace bluetooth_proxy { @@ -31,7 +28,6 @@ class BluetoothProxy : public BLEClientBase { void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; -#ifdef USE_API void bluetooth_device_request(const api::BluetoothDeviceRequest &msg); void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg); void bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg); @@ -39,7 +35,6 @@ class BluetoothProxy : public BLEClientBase { void bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg); void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg); void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg); -#endif int get_bluetooth_connections_free() { return this->state_ == espbt::ClientState::IDLE ? 1 : 0; } int get_bluetooth_connections_limit() { return 1; } @@ -50,7 +45,6 @@ class BluetoothProxy : public BLEClientBase { protected: void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); - std::map address_type_map_; int16_t send_service_{-1}; bool active_; }; diff --git a/esphome/components/esp32_ble_client/ble_characteristic.cpp b/esphome/components/esp32_ble_client/ble_characteristic.cpp index 873833368c..2692498e98 100644 --- a/esphome/components/esp32_ble_client/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_client/ble_characteristic.cpp @@ -64,17 +64,18 @@ BLEDescriptor *BLECharacteristic::get_descriptor_by_handle(uint16_t handle) { return nullptr; } -void BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type) { +esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type) { auto *client = this->service->client; auto status = esp_ble_gattc_write_char(client->get_gattc_if(), client->get_conn_id(), this->handle, new_val_size, new_val, write_type, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "Error sending write value to BLE gattc server, status=%d", status); } + return status; } -void BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size) { - write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP); +esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size) { + return write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP); } } // namespace esp32_ble_client diff --git a/esphome/components/esp32_ble_client/ble_characteristic.h b/esphome/components/esp32_ble_client/ble_characteristic.h index ffa9227cc4..d290b282fc 100644 --- a/esphome/components/esp32_ble_client/ble_characteristic.h +++ b/esphome/components/esp32_ble_client/ble_characteristic.h @@ -24,8 +24,8 @@ class BLECharacteristic { BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid); BLEDescriptor *get_descriptor(uint16_t uuid); BLEDescriptor *get_descriptor_by_handle(uint16_t handle); - void write_value(uint8_t *new_val, int16_t new_val_size); - void write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type); + esp_err_t write_value(uint8_t *new_val, int16_t new_val_size); + esp_err_t write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type); BLEService *service; }; diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index d4512b52b0..4aaa6dfa32 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -648,11 +648,17 @@ void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_p // (called CSS here) switch (record_type) { + case ESP_BLE_AD_TYPE_NAME_SHORT: case ESP_BLE_AD_TYPE_NAME_CMPL: { // CSS 1.2 LOCAL NAME // "The Local Name data type shall be the same as, or a shortened version of, the local name assigned to the // device." CSS 1: Optional in this context; shall not appear more than once in a block. - this->name_ = std::string(reinterpret_cast(record), record_length); + // SHORTENED LOCAL NAME + // "The Shortened Local Name data type defines a shortened version of the Local Name data type. The Shortened + // Local Name data type shall not be used to advertise a name that is longer than the Local Name data type." + if (record_length > this->name_.length()) { + this->name_ = std::string(reinterpret_cast(record), record_length); + } break; } case ESP_BLE_AD_TYPE_TX_PWR: { diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index cf33ec126b..d4b2078524 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -1,6 +1,5 @@ import logging from dataclasses import dataclass -from typing import List from esphome.const import ( CONF_ID, @@ -200,7 +199,7 @@ async def esp8266_pin_to_code(config): @coroutine_with_priority(-999.0) async def add_pin_initial_states_array(): # Add includes at the very end, so that they override everything - initial_states: List[PinInitialState] = CORE.data[KEY_ESP8266][ + initial_states: list[PinInitialState] = CORE.data[KEY_ESP8266][ KEY_PIN_INITIAL_STATES ] initial_modes_s = ", ".join(str(x.mode) for x in initial_states) diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index d0153f6104..53fd337ed8 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -98,7 +98,7 @@ async def to_code(config): def _process_git_config(config: dict, refresh) -> str: - repo_dir = git.clone_or_update( + repo_dir, _ = git.clone_or_update( url=config[CONF_URL], ref=config.get(CONF_REF), refresh=refresh, diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index a253a778de..2e78db8356 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -58,6 +58,7 @@ PROTOCOLS = { "sharp": Protocol.PROTOCOL_SHARP, "toshiba_daiseikai": Protocol.PROTOCOL_TOSHIBA_DAISEIKAI, "toshiba": Protocol.PROTOCOL_TOSHIBA, + "zhlt01": Protocol.PROTOCOL_ZHLT01, } CONF_HORIZONTAL_DEFAULT = "horizontal_default" diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index cd24411763..bed1dc76c0 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -53,6 +53,7 @@ const std::map> PROTOCOL_CONSTRUCTOR_MAP {PROTOCOL_SHARP, []() { return new SharpHeatpumpIR(); }}, // NOLINT {PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }}, // NOLINT {PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }}, // NOLINT + {PROTOCOL_ZHLT01, []() { return new ZHLT01HeatpumpIR(); }}, // NOLINT }; void HeatpumpIRClimate::setup() { diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index decf1eae07..c60b944111 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -53,6 +53,7 @@ enum Protocol { PROTOCOL_SHARP, PROTOCOL_TOSHIBA_DAISEIKAI, PROTOCOL_TOSHIBA, + PROTOCOL_ZHLT01, }; // Simple enum to represent horizontal directios diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp index 2fc9c74634..ca4c28e0b4 100644 --- a/esphome/components/mcp23s17/mcp23s17.cpp +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -23,6 +23,13 @@ void MCP23S17::setup() { this->transfer_byte(0b00011000); // Enable HAEN pins for addressing this->disable(); + this->enable(); + cmd = 0b01001000; + this->transfer_byte(cmd); + this->transfer_byte(mcp23x17_base::MCP23X17_IOCONA); + this->transfer_byte(0b00011000); // Enable HAEN pins for addressing + this->disable(); + // Read current output register state this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_); this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index e60b016a17..57f714f233 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -571,24 +571,16 @@ int64_t payload_to_number(const std::vector &data, SensorValueType sens static_cast(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask); } break; case SensorValueType::U_QWORD: - // Ignore bitmask for U_QWORD - value = get_data(data, offset); - break; case SensorValueType::S_QWORD: - // Ignore bitmask for S_QWORD - value = get_data(data, offset); + // Ignore bitmask for QWORD + value = get_data(data, offset); break; case SensorValueType::U_QWORD_R: - // Ignore bitmask for U_QWORD - value = get_data(data, offset); - value = static_cast(value & 0xFFFF) << 48 | (value & 0xFFFF000000000000) >> 48 | - static_cast(value & 0xFFFF0000) << 32 | (value & 0x0000FFFF00000000) >> 32 | - static_cast(value & 0xFFFF00000000) << 16 | (value & 0x00000000FFFF0000) >> 16; - break; - case SensorValueType::S_QWORD_R: - // Ignore bitmask for S_QWORD - value = get_data(data, offset); - break; + case SensorValueType::S_QWORD_R: { + // Ignore bitmask for QWORD + uint64_t tmp = get_data(data, offset); + value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000); + } break; case SensorValueType::RAW: default: break; diff --git a/esphome/components/neopixelbus/_methods.py b/esphome/components/neopixelbus/_methods.py index 4e3c3ca778..98a2d152e1 100644 --- a/esphome/components/neopixelbus/_methods.py +++ b/esphome/components/neopixelbus/_methods.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any, List +from typing import Any import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( @@ -349,7 +349,7 @@ def _spi_extra_validate(config): class MethodDescriptor: method_schema: Any to_code: Any - supported_chips: List[str] + supported_chips: list[str] extra_validate: Any = None diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 67220cae08..3b5a6a5908 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -5,14 +5,17 @@ from esphome.config_helpers import merge_config from esphome import git, yaml_util from esphome.const import ( + CONF_ESPHOME, CONF_FILE, CONF_FILES, + CONF_MIN_VERSION, CONF_PACKAGES, CONF_REF, CONF_REFRESH, CONF_URL, CONF_USERNAME, CONF_PASSWORD, + __version__ as ESPHOME_VERSION, ) import esphome.config_validation as cv @@ -104,7 +107,7 @@ CONFIG_SCHEMA = cv.All( def _process_base_package(config: dict) -> dict: - repo_dir = git.clone_or_update( + repo_dir, revert = git.clone_or_update( url=config[CONF_URL], ref=config.get(CONF_REF), refresh=config[CONF_REFRESH], @@ -112,21 +115,51 @@ def _process_base_package(config: dict) -> dict: username=config.get(CONF_USERNAME), password=config.get(CONF_PASSWORD), ) - files: str = config[CONF_FILES] + files: list[str] = config[CONF_FILES] + + def get_packages(files) -> dict: + packages = {} + for file in files: + yaml_file: Path = repo_dir / file + + if not yaml_file.is_file(): + raise cv.Invalid( + f"{file} does not exist in repository", path=[CONF_FILES] + ) + + try: + new_yaml = yaml_util.load_yaml(yaml_file) + if ( + CONF_ESPHOME in new_yaml + and CONF_MIN_VERSION in new_yaml[CONF_ESPHOME] + ): + min_version = new_yaml[CONF_ESPHOME][CONF_MIN_VERSION] + if cv.Version.parse(min_version) > cv.Version.parse( + ESPHOME_VERSION + ): + raise cv.Invalid( + f"Current ESPHome Version is too old to use this package: {ESPHOME_VERSION} < {min_version}" + ) + + packages[file] = new_yaml + except EsphomeError as e: + raise cv.Invalid( + f"{file} is not a valid YAML file. Please check the file contents." + ) from e + return packages packages = {} - for file in files: - yaml_file: Path = repo_dir / file - if not yaml_file.is_file(): - raise cv.Invalid(f"{file} does not exist in repository", path=[CONF_FILES]) + try: + packages = get_packages(files) + except cv.Invalid: + if revert is not None: + revert() + packages = get_packages(files) + finally: + if packages is None: + raise cv.Invalid("Failed to load packages") - try: - packages[file] = yaml_util.load_yaml(yaml_file) - except EsphomeError as e: - raise cv.Invalid( - f"{file} is not a valid YAML file. Please check the file contents." - ) from e return {"packages": packages} diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index f747f9ee40..52b7261f8b 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -11,62 +11,43 @@ void PulseMeterSensor::setup() { this->isr_pin_ = pin_->to_isr(); this->pin_->attach_interrupt(PulseMeterSensor::gpio_intr, this, gpio::INTERRUPT_ANY_EDGE); + this->pulse_width_us_ = 0; this->last_detected_edge_us_ = 0; - this->last_valid_low_edge_us_ = 0; this->last_valid_high_edge_us_ = 0; + this->last_valid_low_edge_us_ = 0; this->sensor_is_high_ = this->isr_pin_.digital_read(); + this->has_valid_high_edge_ = false; + this->has_valid_low_edge_ = false; } void PulseMeterSensor::loop() { + // Get a local copy of the volatile sensor values, to make sure they are not + // modified by the ISR. This could cause overflow in the following arithmetic + const uint32_t last_valid_high_edge_us = this->last_valid_high_edge_us_; + const bool has_valid_high_edge = this->has_valid_high_edge_; const uint32_t now = micros(); - // Check to see if we should filter this edge out - if (this->filter_mode_ == FILTER_EDGE) { - if ((this->last_detected_edge_us_ - this->last_valid_high_edge_us_) >= this->filter_us_) { - // Don't measure the first valid pulse (we need at least two pulses to measure the width) - if (this->last_valid_high_edge_us_ != 0) { - this->pulse_width_us_ = (this->last_detected_edge_us_ - this->last_valid_high_edge_us_); - } - this->total_pulses_++; - this->last_valid_high_edge_us_ = this->last_detected_edge_us_; - } - } else { - // Make sure the signal has been stable long enough - if ((now - this->last_detected_edge_us_) >= this->filter_us_) { - // Only consider HIGH pulses and "new" edges if sensor state is LOW - if (!this->sensor_is_high_ && this->isr_pin_.digital_read() && - (this->last_detected_edge_us_ != this->last_valid_high_edge_us_)) { - // Don't measure the first valid pulse (we need at least two pulses to measure the width) - if (this->last_valid_high_edge_us_ != 0) { - this->pulse_width_us_ = (this->last_detected_edge_us_ - this->last_valid_high_edge_us_); - } - this->sensor_is_high_ = true; - this->total_pulses_++; - this->last_valid_high_edge_us_ = this->last_detected_edge_us_; - } - // Only consider LOW pulses and "new" edges if sensor state is HIGH - else if (this->sensor_is_high_ && !this->isr_pin_.digital_read() && - (this->last_detected_edge_us_ != this->last_valid_low_edge_us_)) { - this->sensor_is_high_ = false; - this->last_valid_low_edge_us_ = this->last_detected_edge_us_; - } - } - } - - // If we've exceeded our timeout interval without receiving any pulses, assume 0 pulses/min until - // we get at least two valid pulses. - const uint32_t time_since_valid_edge_us = now - this->last_valid_high_edge_us_; - if ((this->last_valid_high_edge_us_ != 0) && (time_since_valid_edge_us > this->timeout_us_) && - (this->pulse_width_us_ != 0)) { + // If we've exceeded our timeout interval without receiving any pulses, assume + // 0 pulses/min until we get at least two valid pulses. + const uint32_t time_since_valid_edge_us = now - last_valid_high_edge_us; + if ((has_valid_high_edge) && (time_since_valid_edge_us > this->timeout_us_)) { ESP_LOGD(TAG, "No pulse detected for %us, assuming 0 pulses/min", time_since_valid_edge_us / 1000000); + this->pulse_width_us_ = 0; + this->last_detected_edge_us_ = 0; + this->last_valid_high_edge_us_ = 0; + this->last_valid_low_edge_us_ = 0; + this->has_detected_edge_ = false; + this->has_valid_high_edge_ = false; + this->has_valid_low_edge_ = false; } // We quantize our pulse widths to 1 ms to avoid unnecessary jitter const uint32_t pulse_width_ms = this->pulse_width_us_ / 1000; if (this->pulse_width_dedupe_.next(pulse_width_ms)) { if (pulse_width_ms == 0) { - // Treat 0 pulse width as 0 pulses/min (normally because we've not detected any pulses for a while) + // Treat 0 pulse width as 0 pulses/min (normally because we've not + // detected any pulses for a while) this->publish_state(0); } else { // Calculate pulses/min from the pulse width in ms @@ -96,9 +77,11 @@ void PulseMeterSensor::dump_config() { } void IRAM_ATTR PulseMeterSensor::gpio_intr(PulseMeterSensor *sensor) { - // This is an interrupt handler - we can't call any virtual method from this method + // This is an interrupt handler - we can't call any virtual method from this + // method - // Get the current time before we do anything else so the measurements are consistent + // Get the current time before we do anything else so the measurements are + // consistent const uint32_t now = micros(); // We only look at rising edges in EDGE mode, and all edges in PULSE mode @@ -106,7 +89,45 @@ void IRAM_ATTR PulseMeterSensor::gpio_intr(PulseMeterSensor *sensor) { if (sensor->isr_pin_.digital_read()) { sensor->last_detected_edge_us_ = now; } + } + + // Check to see if we should filter this edge out + if (sensor->filter_mode_ == FILTER_EDGE) { + if ((sensor->last_detected_edge_us_ - sensor->last_valid_high_edge_us_) >= sensor->filter_us_) { + // Don't measure the first valid pulse (we need at least two pulses to + // measure the width) + if (sensor->has_valid_high_edge_) { + sensor->pulse_width_us_ = (sensor->last_detected_edge_us_ - sensor->last_valid_high_edge_us_); + } + sensor->total_pulses_++; + sensor->last_valid_high_edge_us_ = sensor->last_detected_edge_us_; + sensor->has_valid_high_edge_ = true; + } } else { + // Filter Mode is PULSE + bool pin_val = sensor->isr_pin_.digital_read(); + // Ignore false edges that may be caused by bouncing and exit the ISR ASAP + if (pin_val == sensor->sensor_is_high_) { + return; + } + // Make sure the signal has been stable long enough + if (sensor->has_detected_edge_ && (now - sensor->last_detected_edge_us_ >= sensor->filter_us_)) { + if (pin_val) { + sensor->has_valid_high_edge_ = true; + sensor->last_valid_high_edge_us_ = sensor->last_detected_edge_us_; + sensor->sensor_is_high_ = true; + } else { + // Count pulses when a sufficiently long high pulse is concluded. + sensor->total_pulses_++; + if (sensor->has_valid_low_edge_) { + sensor->pulse_width_us_ = sensor->last_detected_edge_us_ - sensor->last_valid_low_edge_us_; + } + sensor->has_valid_low_edge_ = true; + sensor->last_valid_low_edge_us_ = sensor->last_detected_edge_us_; + sensor->sensor_is_high_ = false; + } + } + sensor->has_detected_edge_ = true; sensor->last_detected_edge_us_ = now; } } diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index bf50eab6ff..ed4fb2a1f4 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -1,8 +1,8 @@ #pragma once +#include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/core/helpers.h" namespace esphome { @@ -42,11 +42,14 @@ class PulseMeterSensor : public sensor::Sensor, public Component { Deduplicator total_dedupe_; volatile uint32_t last_detected_edge_us_ = 0; - volatile uint32_t last_valid_low_edge_us_ = 0; volatile uint32_t last_valid_high_edge_us_ = 0; + volatile uint32_t last_valid_low_edge_us_ = 0; volatile uint32_t pulse_width_us_ = 0; volatile uint32_t total_pulses_ = 0; volatile bool sensor_is_high_ = false; + volatile bool has_detected_edge_ = false; + volatile bool has_valid_high_edge_ = false; + volatile bool has_valid_low_edge_ = false; }; } // namespace pulse_meter diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index a1c73c385e..b505d89c6f 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -1,4 +1,3 @@ -from typing import List import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation @@ -60,7 +59,7 @@ SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).e ) -async def setup_select_core_(var, config, *, options: List[str]): +async def setup_select_core_(var, config, *, options: list[str]): await setup_entity(var, config) cg.add(var.traits.set_options(options)) @@ -76,14 +75,14 @@ async def setup_select_core_(var, config, *, options: List[str]): await mqtt.register_mqtt_component(mqtt_, config) -async def register_select(var, config, *, options: List[str]): +async def register_select(var, config, *, options: list[str]): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) cg.add(cg.App.register_select(var)) await setup_select_core_(var, config, options=options) -async def new_select(config, *, options: List[str]): +async def new_select(config, *, options: list[str]): var = cg.new_Pvariable(config[CONF_ID]) await register_select(var, config, options=options) return var diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index d6ba038057..12daf34d6e 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -29,6 +29,7 @@ from esphome.const import ( CONF_WINDOW_SIZE, CONF_MQTT_ID, CONF_FORCE_UPDATE, + DEVICE_CLASS_DISTANCE, DEVICE_CLASS_DURATION, DEVICE_CLASS_EMPTY, DEVICE_CLASS_APPARENT_POWER, @@ -43,6 +44,7 @@ from esphome.const import ( DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MONETARY, DEVICE_CLASS_NITROGEN_DIOXIDE, DEVICE_CLASS_NITROGEN_MONOXIDE, @@ -56,11 +58,14 @@ from esphome.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SPEED, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_VOLUME, + DEVICE_CLASS_WEIGHT, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_generator import MockObjClass @@ -77,12 +82,14 @@ DEVICE_CLASSES = [ DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_DATE, + DEVICE_CLASS_DISTANCE, DEVICE_CLASS_DURATION, DEVICE_CLASS_ENERGY, DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MONETARY, DEVICE_CLASS_NITROGEN_DIOXIDE, DEVICE_CLASS_NITROGEN_MONOXIDE, @@ -96,11 +103,14 @@ DEVICE_CLASSES = [ DEVICE_CLASS_PRESSURE, DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SPEED, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_VOLUME, + DEVICE_CLASS_WEIGHT, ] sensor_ns = cg.esphome_ns.namespace("sensor") diff --git a/esphome/components/ssd1327_base/ssd1327_base.cpp b/esphome/components/ssd1327_base/ssd1327_base.cpp index 4cb8d17a3d..4223a013a4 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.cpp +++ b/esphome/components/ssd1327_base/ssd1327_base.cpp @@ -76,6 +76,8 @@ void SSD1327::setup() { this->command(0x55); this->command(SSD1327_SETVCOMHVOLTAGE); // Set High Voltage Level of COM Pin this->command(0x1C); + this->command(SSD1327_SETGPIO); // Switch voltage converter on (for Aliexpress display) + this->command(0x03); this->command(SSD1327_NORMALDISPLAY); // set display mode set_brightness(this->brightness_); this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index c276be2f5a..d18e305cc2 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -4,7 +4,6 @@ from esphome import pins from esphome.components import display, spi from esphome.const import ( CONF_BACKLIGHT_PIN, - CONF_CS_PIN, CONF_DC_PIN, CONF_HEIGHT, CONF_ID, @@ -69,7 +68,6 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_MODEL): ST7789V_MODEL, cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_EIGHTBITCOLOR, default=False): cv.boolean, cv.Optional(CONF_HEIGHT): cv.int_, @@ -79,7 +77,7 @@ CONFIG_SCHEMA = cv.All( } ) .extend(cv.polling_component_schema("5s")) - .extend(spi.spi_device_schema()), + .extend(spi.spi_device_schema(cs_pin_required=False)), validate_st7789v, ) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 5e26e6d6de..8aa61dbb93 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -69,6 +69,8 @@ from esphome.const import ( ) CONF_PRESET_CHANGE = "preset_change" +CONF_DEFAULT_PRESET = "default_preset" +CONF_ON_BOOT_RESTORE_FROM = "on_boot_restore_from" CODEOWNERS = ["@kbx81"] @@ -80,6 +82,13 @@ ThermostatClimate = thermostat_ns.class_( ThermostatClimateTargetTempConfig = thermostat_ns.struct( "ThermostatClimateTargetTempConfig" ) +OnBootRestoreFrom = thermostat_ns.enum("OnBootRestoreFrom") +ON_BOOT_RESTORE_FROM = { + "MEMORY": OnBootRestoreFrom.MEMORY, + "DEFAULT_PRESET": OnBootRestoreFrom.DEFAULT_PRESET, +} +validate_on_boot_restore_from = cv.enum(ON_BOOT_RESTORE_FROM, upper=True) + ClimateMode = climate_ns.enum("ClimateMode") CLIMATE_MODES = { "OFF": ClimateMode.CLIMATE_MODE_OFF, @@ -125,6 +134,17 @@ def validate_temperature_preset(preset, root_config, name, requirements): ) +def generate_comparable_preset(config, name): + comparable_preset = f"{CONF_PRESET}:\n" f" - {CONF_NAME}: {name}\n" + + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: + comparable_preset += f" {CONF_DEFAULT_TARGET_TEMPERATURE_LOW}: {config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW]}\n" + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: + comparable_preset += f" {CONF_DEFAULT_TARGET_TEMPERATURE_HIGH}: {config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH]}\n" + + return comparable_preset + + def validate_thermostat(config): # verify corresponding action(s) exist(s) for any defined climate mode or action requirements = { @@ -277,13 +297,32 @@ def validate_thermostat(config): CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION], } - # Validate temperature requirements for default configuraation - validate_temperature_preset(config, config, "default", requirements) + # Legacy high/low configs + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: + comparable_preset = generate_comparable_preset(config, "Your new preset") - # Validate temperature requirements for away configuration + raise cv.Invalid( + f"{CONF_DEFAULT_TARGET_TEMPERATURE_LOW} is no longer valid. Please switch to using a preset for an equivalent experience.\nEquivalent configuration:\n\n" + f"{comparable_preset}" + ) + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: + comparable_preset = generate_comparable_preset(config, "Your new preset") + + raise cv.Invalid( + f"{CONF_DEFAULT_TARGET_TEMPERATURE_HIGH} is no longer valid. Please switch to using a preset for an equivalent experience.\nEquivalent configuration:\n\n" + f"{comparable_preset}" + ) + + # Legacy away mode - raise an error instructing the user to switch to presets if CONF_AWAY_CONFIG in config: - away = config[CONF_AWAY_CONFIG] - validate_temperature_preset(away, config, "away", requirements) + comparable_preset = generate_comparable_preset(config[CONF_AWAY_CONFIG], "Away") + + raise cv.Invalid( + f"{CONF_AWAY_CONFIG} is no longer valid. Please switch to using a preset named " + "Away" + " for an equivalent experience.\nEquivalent configuration:\n\n" + f"{comparable_preset}" + ) # Validate temperature requirements for presets if CONF_PRESET in config: @@ -292,7 +331,12 @@ def validate_thermostat(config): preset_config, config, preset_config[CONF_NAME], requirements ) - # Verify default climate mode is valid given above configuration + # Warn about using the removed CONF_DEFAULT_MODE and advise users + if CONF_DEFAULT_MODE in config and config[CONF_DEFAULT_MODE] is not None: + raise cv.Invalid( + f"{CONF_DEFAULT_MODE} is no longer valid. Please switch to using presets and specify a {CONF_DEFAULT_PRESET}." + ) + default_mode = config[CONF_DEFAULT_MODE] requirements = { "HEAT_COOL": [CONF_COOL_ACTION, CONF_HEAT_ACTION], @@ -403,6 +447,38 @@ def validate_thermostat(config): f"{CONF_SWING_MODE} is set to {swing_mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration" ) + # If a default preset is requested then ensure that preset is defined + if CONF_DEFAULT_PRESET in config: + default_preset = config[CONF_DEFAULT_PRESET] + + if CONF_PRESET not in config: + raise cv.Invalid( + f"{CONF_DEFAULT_PRESET} is specified but no presets are defined" + ) + + presets = config[CONF_PRESET] + found_preset = False + + for preset in presets: + if preset[CONF_NAME] == default_preset: + found_preset = True + break + + if found_preset is False: + raise cv.Invalid( + f"{CONF_DEFAULT_PRESET} set to '{default_preset}' but no such preset has been defined. Available presets: {[preset[CONF_NAME] for preset in presets]}" + ) + + # If restoring default preset on boot is true then ensure we have a default preset + if ( + CONF_ON_BOOT_RESTORE_FROM in config + and config[CONF_ON_BOOT_RESTORE_FROM] is OnBootRestoreFrom.DEFAULT_PRESET + ): + if CONF_DEFAULT_PRESET not in config: + raise cv.Invalid( + f"{CONF_DEFAULT_PRESET} must be defined to use {CONF_ON_BOOT_RESTORE_FROM} in DEFAULT_PRESET mode" + ) + if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config: raise cv.Invalid( f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_COOLING}" @@ -502,9 +578,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_TARGET_TEMPERATURE_CHANGE_ACTION ): automation.validate_automation(single=True), - cv.Optional(CONF_DEFAULT_MODE, default="OFF"): cv.templatable( - validate_climate_mode - ), + cv.Optional(CONF_DEFAULT_MODE, default=None): cv.valid, + cv.Optional(CONF_DEFAULT_PRESET): cv.templatable(cv.string), cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Optional( @@ -542,6 +617,7 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA), + cv.Optional(CONF_ON_BOOT_RESTORE_FROM): validate_on_boot_restore_from, cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation( single=True ), @@ -564,9 +640,10 @@ async def to_code(config): CONF_COOL_ACTION in config or (config[CONF_FAN_ONLY_COOLING] and CONF_FAN_ONLY_ACTION in config) ) + if two_points_available: + cg.add(var.set_supports_two_points(True)) sens = await cg.get_variable(config[CONF_SENSOR]) - cg.add(var.set_default_mode(config[CONF_DEFAULT_MODE])) cg.add( var.set_set_point_minimum_differential( config[CONF_SET_POINT_MINIMUM_DIFFERENTIAL] @@ -579,23 +656,6 @@ async def to_code(config): cg.add(var.set_heat_deadband(config[CONF_HEAT_DEADBAND])) cg.add(var.set_heat_overrun(config[CONF_HEAT_OVERRUN])) - if two_points_available is True: - cg.add(var.set_supports_two_points(True)) - normal_config = ThermostatClimateTargetTempConfig( - config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], - config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], - ) - elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: - cg.add(var.set_supports_two_points(False)) - normal_config = ThermostatClimateTargetTempConfig( - config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] - ) - elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: - cg.add(var.set_supports_two_points(False)) - normal_config = ThermostatClimateTargetTempConfig( - config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] - ) - if CONF_MAX_COOLING_RUN_TIME in config: cg.add( var.set_cooling_maximum_run_time_in_sec(config[CONF_MAX_COOLING_RUN_TIME]) @@ -661,7 +721,6 @@ async def to_code(config): cg.add(var.set_supports_fan_with_heating(config[CONF_FAN_WITH_HEATING])) cg.add(var.set_use_startup_delay(config[CONF_STARTUP_DELAY])) - cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_HOME, normal_config)) await automation.build_automation( var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION] @@ -808,27 +867,8 @@ async def to_code(config): config[CONF_TARGET_TEMPERATURE_CHANGE_ACTION], ) - if CONF_AWAY_CONFIG in config: - away = config[CONF_AWAY_CONFIG] - - if two_points_available is True: - away_config = ThermostatClimateTargetTempConfig( - away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], - away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], - ) - elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away: - away_config = ThermostatClimateTargetTempConfig( - away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] - ) - elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away: - away_config = ThermostatClimateTargetTempConfig( - away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] - ) - cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_AWAY, away_config)) - if CONF_PRESET in config: for preset_config in config[CONF_PRESET]: - name = preset_config[CONF_NAME] standard_preset = None if name.upper() in climate.CLIMATE_PRESETS: @@ -872,6 +912,19 @@ async def to_code(config): else: cg.add(var.set_custom_preset_config(name, preset_target_variable)) + if CONF_DEFAULT_PRESET in config: + default_preset_name = config[CONF_DEFAULT_PRESET] + + # if the name is a built in preset use the appropriate naming format + if default_preset_name.upper() in climate.CLIMATE_PRESETS: + climate_preset = climate.CLIMATE_PRESETS[default_preset_name.upper()] + cg.add(var.set_default_preset(climate_preset)) + else: + cg.add(var.set_default_preset(default_preset_name)) + + if CONF_ON_BOOT_RESTORE_FROM in config: + cg.add(var.set_on_boot_restore_from(config[CONF_ON_BOOT_RESTORE_FROM])) + if CONF_PRESET_CHANGE in config: await automation.build_automation( var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE] diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index dc4e1e437e..61e279d4a6 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -25,15 +25,27 @@ void ThermostatClimate::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - // restore all climate data, if possible - auto restore = this->restore_state_(); - if (restore.has_value()) { - restore->to_call(this).perform(); - } else { - // restore from defaults, change_away handles temps for us - this->mode = this->default_mode_; - this->change_preset_(climate::CLIMATE_PRESET_HOME); + + auto use_default_preset = true; + + if (this->on_boot_restore_from_ == thermostat::OnBootRestoreFrom::MEMORY) { + // restore all climate data, if possible + auto restore = this->restore_state_(); + if (restore.has_value()) { + use_default_preset = false; + restore->to_call(this).perform(); + } } + + // Either we failed to restore state or the user has requested we always apply the default preset + if (use_default_preset) { + if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) { + this->change_preset_(this->default_preset_); + } else if (!this->default_custom_preset_.empty()) { + this->change_custom_preset_(this->default_custom_preset_); + } + } + // refresh the climate action based on the restored settings, we'll publish_state() later this->switch_to_action_(this->compute_action_(), false); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); @@ -923,9 +935,9 @@ bool ThermostatClimate::supplemental_heating_required_() { (this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING)); } -void ThermostatClimate::dump_preset_config_(const std::string &preset, - const ThermostatClimateTargetTempConfig &config) { - const auto *preset_name = preset.c_str(); +void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, + bool is_default_preset) { + ESP_LOGCONFIG(TAG, " %s Is Default: %s", preset_name, YESNO(is_default_preset)); if (this->supports_heat_) { if (this->supports_two_points_) { @@ -962,9 +974,19 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { auto config = this->preset_config_.find(preset); if (config != this->preset_config_.end()) { - ESP_LOGI(TAG, "Switching to preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); - this->change_preset_internal_(config->second); + ESP_LOGI(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) || + this->preset.value() != preset) { + // Fire any preset changed trigger if defined + Trigger<> *trig = this->preset_change_trigger_; + assert(trig != nullptr); + trig->trigger(); + this->refresh(); + ESP_LOGI(TAG, "Preset %s applied", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + } else { + ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + } this->custom_preset.reset(); this->preset = preset; } else { @@ -976,9 +998,19 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) auto config = this->custom_preset_config_.find(custom_preset); if (config != this->custom_preset_config_.end()) { - ESP_LOGI(TAG, "Switching to custom preset %s", custom_preset.c_str()); - this->change_preset_internal_(config->second); + ESP_LOGI(TAG, "Custom preset %s requested", custom_preset.c_str()); + if (this->change_preset_internal_(config->second) || (!this->custom_preset.has_value()) || + this->custom_preset.value() != custom_preset) { + // Fire any preset changed trigger if defined + Trigger<> *trig = this->preset_change_trigger_; + assert(trig != nullptr); + trig->trigger(); + this->refresh(); + ESP_LOGI(TAG, "Custom preset %s applied", custom_preset.c_str()); + } else { + ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset.c_str()); + } this->preset.reset(); this->custom_preset = custom_preset; } else { @@ -986,39 +1018,46 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) } } -void ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTempConfig &config) { +bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTempConfig &config) { + bool something_changed = false; + if (this->supports_two_points_) { - this->target_temperature_low = config.default_temperature_low; - this->target_temperature_high = config.default_temperature_high; + if (this->target_temperature_low != config.default_temperature_low) { + this->target_temperature_low = config.default_temperature_low; + something_changed = true; + } + if (this->target_temperature_high != config.default_temperature_high) { + this->target_temperature_high = config.default_temperature_high; + something_changed = true; + } } else { - this->target_temperature = config.default_temperature; + if (this->target_temperature != config.default_temperature) { + this->target_temperature = config.default_temperature; + something_changed = true; + } } - // Note: The mode, fan_mode, and swing_mode can all be defined on the preset but if the climate.control call - // also specifies them then the control's version will override these for that call - if (config.mode_.has_value()) { - this->mode = *config.mode_; + // Note: The mode, fan_mode and swing_mode can all be defined in the preset but if the climate.control call + // also specifies them then the climate.control call's values will override the preset's values for that call + if (config.mode_.has_value() && (this->mode != config.mode_.value())) { ESP_LOGV(TAG, "Setting mode to %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); + this->mode = *config.mode_; + something_changed = true; } - if (config.fan_mode_.has_value()) { - this->fan_mode = *config.fan_mode_; + if (config.fan_mode_.has_value() && (this->fan_mode != config.fan_mode_.value())) { ESP_LOGV(TAG, "Setting fan mode to %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_))); + this->fan_mode = *config.fan_mode_; + something_changed = true; } - if (config.swing_mode_.has_value()) { + if (config.swing_mode_.has_value() && (this->swing_mode != config.swing_mode_.value())) { ESP_LOGV(TAG, "Setting swing mode to %s", LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_))); this->swing_mode = *config.swing_mode_; + something_changed = true; } - // Fire any preset changed trigger if defined - if (this->preset != preset) { - Trigger<> *trig = this->preset_change_trigger_; - assert(trig != nullptr); - trig->trigger(); - } - - this->refresh(); + return something_changed; } void ThermostatClimate::set_preset_config(climate::ClimatePreset preset, @@ -1061,7 +1100,15 @@ ThermostatClimate::ThermostatClimate() temperature_change_trigger_(new Trigger<>()), preset_change_trigger_(new Trigger<>()) {} -void ThermostatClimate::set_default_mode(climate::ClimateMode default_mode) { this->default_mode_ = default_mode; } +void ThermostatClimate::set_default_preset(const std::string &custom_preset) { + this->default_custom_preset_ = custom_preset; +} + +void ThermostatClimate::set_default_preset(climate::ClimatePreset preset) { this->default_preset_ = preset; } + +void ThermostatClimate::set_on_boot_restore_from(thermostat::OnBootRestoreFrom on_boot_restore_from) { + this->on_boot_restore_from_ = on_boot_restore_from; +} void ThermostatClimate::set_set_point_minimum_differential(float differential) { this->set_point_minimum_differential_ = differential; } @@ -1213,8 +1260,9 @@ Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->p void ThermostatClimate::dump_config() { LOG_CLIMATE("", "Thermostat", this); - if (this->supports_two_points_) + if (this->supports_two_points_) { ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); + } ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_)); if (this->supports_cool_) { ESP_LOGCONFIG(TAG, " Cooling Parameters:"); @@ -1284,7 +1332,7 @@ void ThermostatClimate::dump_config() { const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true)); - this->dump_preset_config_(preset_name, it.second); + this->dump_preset_config_(preset_name, it.second, it.first == this->default_preset_); } ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS: "); @@ -1292,8 +1340,10 @@ void ThermostatClimate::dump_config() { const auto *preset_name = it.first.c_str(); ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true)); - this->dump_preset_config_(preset_name, it.second); + this->dump_preset_config_(preset_name, it.second, it.first == this->default_custom_preset_); } + ESP_LOGCONFIG(TAG, " On boot, restore from: %s", + this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? "DEFAULT_PRESET" : "MEMORY"); } ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index a5498dc53d..68cbb17e34 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -22,6 +22,7 @@ enum ThermostatClimateTimerIndex : size_t { TIMER_IDLE_ON = 9, }; +enum OnBootRestoreFrom : size_t { MEMORY = 0, DEFAULT_PRESET = 1 }; struct ThermostatClimateTimer { const std::string name; bool active; @@ -57,7 +58,9 @@ class ThermostatClimate : public climate::Climate, public Component { void setup() override; void dump_config() override; - void set_default_mode(climate::ClimateMode default_mode); + void set_default_preset(const std::string &custom_preset); + void set_default_preset(climate::ClimatePreset preset); + void set_on_boot_restore_from(thermostat::OnBootRestoreFrom on_boot_restore_from); void set_set_point_minimum_differential(float differential); void set_cool_deadband(float deadband); void set_cool_overrun(float overrun); @@ -165,7 +168,8 @@ class ThermostatClimate : public climate::Climate, public Component { /// Applies the temperature, mode, fan, and swing modes of the provided config. /// This is agnostic of custom vs built in preset - void change_preset_internal_(const ThermostatClimateTargetTempConfig &config); + /// Returns true if something was changed + bool change_preset_internal_(const ThermostatClimateTargetTempConfig &config); /// Return the traits of this controller. climate::ClimateTraits traits() override; @@ -225,7 +229,8 @@ class ThermostatClimate : public climate::Climate, public Component { bool supplemental_cooling_required_(); bool supplemental_heating_required_(); - void dump_preset_config_(const std::string &preset_name, const ThermostatClimateTargetTempConfig &config); + void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, + bool is_default_preset); /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; @@ -397,7 +402,6 @@ class ThermostatClimate : public climate::Climate, public Component { /// These are used to determine when a trigger/action needs to be called climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; - climate::ClimateMode default_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; @@ -441,6 +445,15 @@ class ThermostatClimate : public climate::Climate, public Component { std::map preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") std::map custom_preset_config_{}; + + /// Default standard preset to use on start up + climate::ClimatePreset default_preset_{}; + /// Default custom preset to use on start up + std::string default_custom_preset_{}; + + /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior + /// state will attempt to be restored if possible + thermostat::OnBootRestoreFrom on_boot_restore_from_{thermostat::OnBootRestoreFrom::MEMORY}; }; } // namespace thermostat diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index 7e16d7141f..af2b6c720c 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -6,6 +6,8 @@ namespace esphome { namespace time { static const char *const TAG = "automation"; +static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider + // there has been a drastic time synchronization void CronTrigger::add_second(uint8_t second) { this->seconds_[second] = true; } void CronTrigger::add_minute(uint8_t minute) { this->minutes_[minute] = true; } @@ -23,12 +25,17 @@ void CronTrigger::loop() { return; if (this->last_check_.has_value()) { - if (*this->last_check_ > time && this->last_check_->timestamp - time.timestamp > 900) { + if (*this->last_check_ > time && this->last_check_->timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) { // We went back in time (a lot), probably caused by time synchronization ESP_LOGW(TAG, "Time has jumped back!"); } else if (*this->last_check_ >= time) { // already handled this one return; + } else if (time > *this->last_check_ && time.timestamp - this->last_check_->timestamp > MAX_TIMESTAMP_DRIFT) { + // We went ahead in time (a lot), probably caused by time synchronization + ESP_LOGW(TAG, "Time has jumped ahead!"); + this->last_check_ = time; + return; } while (true) { diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h index d7e53962e2..701468aa1e 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h @@ -23,6 +23,12 @@ class TouchscreenBinarySensor : public binary_sensor::BinarySensor, this->y_min_ = y_min; this->y_max_ = y_max; } + int16_t get_x_min() { return this->x_min_; } + int16_t get_x_max() { return this->x_max_; } + int16_t get_y_min() { return this->y_min_; } + int16_t get_y_max() { return this->y_max_; } + int16_t get_width() { return this->x_max_ - this->x_min_; } + int16_t get_height() { return this->y_max_ - this->y_min_; } void set_page(display::DisplayPage *page) { this->page_ = page; } diff --git a/esphome/components/web_server/server_index.h b/esphome/components/web_server/server_index.h index 75c7130151..c02ef279b7 100644 --- a/esphome/components/web_server/server_index.h +++ b/esphome/components/web_server/server_index.h @@ -6,7 +6,7 @@ namespace esphome { namespace web_server { const uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xbd, 0x7d, 0xd9, 0x76, 0xdb, 0xc8, 0x92, 0xe0, 0xf3, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xbd, 0x7d, 0xd9, 0x76, 0xdb, 0xc8, 0x92, 0xe0, 0xf3, 0x9c, 0x33, 0x7f, 0x30, 0x2f, 0x30, 0x4a, 0x6d, 0x03, 0x25, 0x10, 0x22, 0x29, 0xcb, 0x76, 0x81, 0x02, 0x79, 0xe5, 0xa5, 0xae, 0x5d, 0xe5, 0xad, 0x2c, 0xd9, 0x75, 0xab, 0x54, 0x2c, 0x0b, 0x22, 0x93, 0x22, 0xca, 0x20, 0xc0, 0x02, 0x92, 0x5a, 0x8a, 0x42, 0x9f, 0x7e, 0xea, 0xa7, 0x39, 0x67, 0xd6, 0x87, 0x7e, 0x99, 0xd3, 0xfd, 0x30, 0x1f, 0x31, @@ -524,62 +524,64 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x5b, 0x06, 0xb9, 0xff, 0xfc, 0x07, 0x47, 0x40, 0x7e, 0x68, 0x06, 0xb9, 0xf7, 0xd8, 0x4a, 0xa0, 0xa3, 0xe9, 0xac, 0x3d, 0x67, 0xce, 0xe1, 0x8f, 0x6c, 0x88, 0x69, 0xd3, 0xd0, 0xfa, 0x2f, 0xb5, 0x78, 0x50, 0x86, 0x1b, 0x79, 0x8f, 0x89, 0x9d, 0xcc, 0xf8, 0x15, 0x04, 0xe1, 0xfc, 0x46, 0x82, 0xbc, 0xb8, 0x1d, 0x3d, 0x38, 0xff, 0x63, 0xe9, 0x41, - 0x5f, 0xee, 0x57, 0xf4, 0xa8, 0x45, 0xdc, 0x29, 0x62, 0x40, 0x8e, 0xfe, 0x3e, 0xbd, 0x3b, 0xf6, 0x16, 0xc3, 0xb7, - 0xc2, 0x16, 0xb9, 0x80, 0xef, 0x3e, 0xe7, 0x74, 0x40, 0x64, 0x15, 0x87, 0xb6, 0x0c, 0xc0, 0xcc, 0xf1, 0xdb, 0xb4, - 0xea, 0xe5, 0x54, 0xdc, 0x5c, 0x09, 0xe9, 0xda, 0xd9, 0x8e, 0x0e, 0xde, 0x60, 0xa2, 0x77, 0xb8, 0xcc, 0x78, 0x84, - 0xbf, 0xfc, 0x88, 0xc7, 0x3c, 0xc1, 0x4b, 0xb1, 0xf2, 0x02, 0x19, 0xe6, 0x25, 0x7f, 0x87, 0x39, 0xd5, 0xea, 0x90, - 0x60, 0x86, 0x01, 0x83, 0x57, 0x6c, 0x1c, 0x47, 0x8e, 0xed, 0xcc, 0x61, 0xc7, 0xc2, 0x98, 0xad, 0x5a, 0x42, 0x33, - 0xe5, 0x32, 0xbb, 0xb6, 0xfa, 0x7d, 0x3b, 0x39, 0x7e, 0xbf, 0x2c, 0x3c, 0x94, 0x01, 0x46, 0x5b, 0x7a, 0x00, 0x30, - 0xbe, 0x2a, 0xc9, 0x51, 0xd8, 0x57, 0x56, 0x83, 0x2d, 0xcc, 0x86, 0x8e, 0xdf, 0x05, 0x37, 0x82, 0x8a, 0xf1, 0x7b, - 0x50, 0x3f, 0x38, 0xad, 0x6d, 0x30, 0x6b, 0x8c, 0x6e, 0x7a, 0xa0, 0xe1, 0x4a, 0x18, 0x49, 0x04, 0x07, 0x1a, 0xa5, - 0x9e, 0xfe, 0x05, 0x64, 0x55, 0xb8, 0xa8, 0x78, 0x7c, 0x71, 0x20, 0xef, 0x7c, 0xdb, 0x18, 0xb9, 0xa5, 0x88, 0x7d, - 0xf5, 0xbd, 0xa9, 0x4d, 0x50, 0x17, 0xf4, 0x5b, 0x20, 0xe9, 0xdc, 0x1b, 0x35, 0x02, 0xa6, 0x5c, 0x5b, 0xd2, 0x73, - 0x08, 0x6d, 0xa1, 0x0f, 0xc6, 0xec, 0x34, 0x1e, 0x49, 0xb1, 0xee, 0x59, 0xf2, 0xaa, 0x48, 0x8b, 0xb0, 0x08, 0x3b, - 0x9e, 0xf0, 0x9d, 0xe1, 0x05, 0xb5, 0x5a, 0x98, 0x66, 0x76, 0xff, 0x5e, 0x4f, 0x43, 0x52, 0xcf, 0x56, 0xb7, 0xf1, - 0x57, 0x52, 0x1e, 0x82, 0xaf, 0xf6, 0xf7, 0xe1, 0x3d, 0xfc, 0xa5, 0x94, 0xf7, 0x86, 0xb6, 0xeb, 0x93, 0x50, 0xbc, - 0x57, 0xfd, 0x66, 0x4a, 0x94, 0x08, 0x9b, 0xa0, 0xbf, 0xbc, 0xdb, 0x2a, 0x32, 0xa9, 0xb4, 0xba, 0x3b, 0x95, 0xd2, - 0x82, 0x67, 0x43, 0x4a, 0x81, 0x00, 0xed, 0xfa, 0x3b, 0x86, 0x28, 0x3c, 0x6d, 0xe1, 0xcf, 0x9a, 0x30, 0xbc, 0x0f, - 0x0d, 0x94, 0x34, 0x7c, 0x09, 0xcd, 0xb7, 0x85, 0xe0, 0x85, 0x7e, 0x3f, 0x92, 0xa8, 0x12, 0x62, 0xaa, 0xce, 0x31, - 0x6b, 0x0e, 0x91, 0x44, 0x8e, 0x80, 0xed, 0x19, 0xf1, 0x26, 0xc1, 0xae, 0x32, 0x9a, 0xf2, 0x14, 0xfa, 0x3a, 0xfa, - 0x33, 0xce, 0xeb, 0xea, 0xbc, 0xda, 0xce, 0x59, 0x33, 0x05, 0x32, 0x7c, 0xe3, 0xa0, 0x8a, 0xae, 0x2e, 0x88, 0xcf, - 0x99, 0x89, 0x6d, 0x5c, 0x7d, 0xf0, 0x6d, 0x4d, 0xf6, 0xad, 0xb9, 0x29, 0x58, 0xc5, 0x34, 0xb4, 0x2f, 0x30, 0x65, - 0x06, 0x7f, 0x56, 0xc5, 0xea, 0x41, 0x32, 0x94, 0x9f, 0x44, 0xf8, 0xdb, 0x58, 0xe8, 0x47, 0x59, 0x6d, 0x40, 0x4e, - 0xdf, 0xab, 0x24, 0x48, 0x5f, 0x8c, 0xcb, 0x26, 0x12, 0x60, 0x2f, 0xe0, 0x2f, 0xf7, 0xab, 0xae, 0x4a, 0xc8, 0x3b, - 0x90, 0x98, 0x53, 0x30, 0x8e, 0x73, 0xba, 0x5a, 0xab, 0xf0, 0xaf, 0x45, 0x34, 0x2b, 0x52, 0xd3, 0xae, 0x64, 0xc5, - 0xc0, 0xc6, 0x22, 0x3b, 0x90, 0xc9, 0x68, 0xe6, 0x07, 0x9b, 0xcd, 0xbb, 0x8f, 0x63, 0x91, 0x87, 0x86, 0x1f, 0xb4, - 0xb7, 0x05, 0x91, 0x6d, 0x10, 0x63, 0x57, 0xe2, 0x44, 0xc6, 0x0d, 0x5e, 0x19, 0xac, 0x7e, 0x43, 0x91, 0xb9, 0xe1, - 0x6d, 0x73, 0xb5, 0xf4, 0xb8, 0xb4, 0x0e, 0xae, 0x8c, 0xdf, 0x1d, 0xb3, 0x88, 0xfb, 0x51, 0x4a, 0xb9, 0x49, 0x8e, - 0x21, 0x16, 0xbc, 0x0e, 0xdb, 0x76, 0x4b, 0x90, 0x3c, 0xc6, 0xaf, 0x70, 0x12, 0xa4, 0xf7, 0xa1, 0xb0, 0x4a, 0xd8, - 0xda, 0x9d, 0x76, 0xfb, 0x6f, 0x0e, 0xf6, 0x2c, 0xb1, 0x9b, 0x77, 0xb7, 0xe0, 0x75, 0x97, 0xdc, 0x61, 0x91, 0x9f, - 0x11, 0x8a, 0xfc, 0x0c, 0x4b, 0x24, 0x74, 0x85, 0xf6, 0x96, 0x40, 0xd3, 0xb6, 0x58, 0x3a, 0x12, 0x31, 0xbc, 0x19, - 0xb8, 0x0b, 0x31, 0x7e, 0xd4, 0x6b, 0x0b, 0xbb, 0xb5, 0x70, 0xa5, 0x6d, 0x95, 0xe1, 0xa2, 0x0c, 0x04, 0x9e, 0xaa, - 0x88, 0x1f, 0xa8, 0x75, 0xa6, 0x92, 0x5d, 0xe4, 0x50, 0x3a, 0x27, 0x75, 0xb5, 0x75, 0xb1, 0x38, 0x9e, 0x81, 0x1c, - 0x52, 0x09, 0x2a, 0xef, 0x65, 0x87, 0x5d, 0x9a, 0x0a, 0x93, 0x62, 0x57, 0x23, 0x92, 0xd3, 0x4e, 0x7f, 0x37, 0x92, - 0xf6, 0x0e, 0xee, 0xdd, 0x02, 0x36, 0x2f, 0xa8, 0x39, 0x34, 0x2a, 0xfc, 0x38, 0xdb, 0x3a, 0x63, 0xc7, 0xad, 0x68, - 0x1e, 0x57, 0xe1, 0x3f, 0xd4, 0x7e, 0xfd, 0x5d, 0xa5, 0x08, 0x65, 0x9a, 0xa5, 0x7c, 0x8c, 0x8c, 0x2c, 0x0e, 0x24, - 0x1c, 0x31, 0x68, 0x29, 0x63, 0x8b, 0x64, 0x34, 0x02, 0xf1, 0x01, 0x56, 0xe2, 0x5f, 0x15, 0x83, 0x94, 0x9a, 0xa0, - 0xb4, 0xfb, 0x7f, 0xfd, 0x5f, 0xff, 0x5b, 0x86, 0x15, 0x81, 0xac, 0x00, 0x16, 0xa6, 0xc1, 0x54, 0x27, 0x8c, 0xec, - 0x1c, 0x1c, 0xd1, 0x78, 0xdc, 0x9a, 0x46, 0xc9, 0x04, 0x20, 0x28, 0x98, 0xb8, 0xca, 0x24, 0xeb, 0x81, 0x0b, 0x24, - 0x58, 0xe6, 0xe1, 0xbc, 0x04, 0xaf, 0x5e, 0x84, 0x2b, 0xf6, 0xbb, 0xf2, 0x56, 0x55, 0xbe, 0x30, 0x31, 0xb4, 0x91, - 0xc5, 0x6a, 0xf0, 0x5c, 0x2d, 0x93, 0x55, 0xfd, 0x82, 0x24, 0x29, 0x3c, 0x58, 0x2d, 0x8d, 0x15, 0x5a, 0xea, 0x83, - 0x90, 0x7f, 0xfb, 0xe7, 0xff, 0xfc, 0xdf, 0xd5, 0x2b, 0x9e, 0x6f, 0xfc, 0xf5, 0x9f, 0xfe, 0xe1, 0xff, 0xfe, 0x9f, - 0xff, 0x82, 0x59, 0xc2, 0xf2, 0x0c, 0x84, 0xb6, 0x92, 0x55, 0x1d, 0x80, 0x88, 0x3d, 0x65, 0x55, 0x0e, 0x47, 0x3d, - 0xdd, 0x75, 0x9f, 0x26, 0x24, 0xde, 0x94, 0xd0, 0x11, 0x5f, 0x53, 0x7a, 0x34, 0x51, 0xed, 0x1a, 0xf2, 0xc1, 0x52, - 0x5a, 0x74, 0xac, 0x6f, 0xef, 0xb4, 0xed, 0x6a, 0x79, 0xfb, 0x46, 0xdf, 0x2d, 0x5c, 0x98, 0x5b, 0x65, 0xe0, 0xf8, - 0x7a, 0xd9, 0x96, 0x2a, 0x8c, 0x85, 0x25, 0x65, 0x55, 0x6e, 0x61, 0x7c, 0x79, 0x89, 0xaf, 0x41, 0xd7, 0x28, 0xa6, - 0x55, 0xae, 0xf5, 0xe9, 0xfd, 0xb2, 0x00, 0x44, 0x27, 0xb8, 0x34, 0x22, 0x58, 0x46, 0x67, 0xa7, 0x2d, 0xb4, 0x4e, - 0x92, 0x8b, 0x92, 0x46, 0x11, 0xde, 0xcc, 0xfd, 0x47, 0x7f, 0x57, 0xfe, 0x69, 0x86, 0x56, 0x81, 0xe5, 0xcc, 0xa2, - 0x73, 0xe9, 0xe3, 0x3c, 0x68, 0xb7, 0xe7, 0xe7, 0xee, 0xb2, 0x9a, 0xc1, 0xbb, 0x6a, 0x32, 0x0a, 0xb0, 0x99, 0x03, - 0xd2, 0xa1, 0xab, 0x8e, 0xe5, 0x81, 0x59, 0xdf, 0xc6, 0xd0, 0x4f, 0x59, 0x7e, 0xb9, 0xa4, 0x70, 0x52, 0xfc, 0x1b, - 0x1e, 0x8e, 0xca, 0xc8, 0x1b, 0x94, 0x18, 0x58, 0x2c, 0x8d, 0x5e, 0x5d, 0xd1, 0x6b, 0xda, 0x59, 0xcd, 0x4d, 0x31, - 0x0f, 0x77, 0xcd, 0x63, 0xd9, 0xfb, 0x78, 0xd0, 0x3a, 0xed, 0x78, 0xd3, 0xee, 0x52, 0x0f, 0xcf, 0x79, 0x36, 0x33, - 0x4f, 0x73, 0x59, 0xc4, 0x46, 0x6c, 0xa2, 0x22, 0x96, 0xb2, 0x5e, 0x9c, 0xd4, 0x96, 0x5f, 0xe0, 0x76, 0x03, 0xda, - 0x66, 0x11, 0x0f, 0x88, 0x69, 0x7b, 0xe6, 0x79, 0x6f, 0x84, 0x27, 0xe9, 0xd9, 0xd2, 0x98, 0xab, 0x27, 0x9a, 0x62, - 0x5c, 0xb0, 0x9e, 0xf7, 0x53, 0xfa, 0xd4, 0xdd, 0x1c, 0x4a, 0x84, 0x15, 0x5e, 0xc8, 0x63, 0xd4, 0x77, 0x35, 0x7f, - 0x5c, 0x8a, 0x62, 0x70, 0x81, 0xd7, 0xd6, 0x0b, 0xb5, 0x28, 0x6a, 0x5f, 0x80, 0xb5, 0x43, 0x60, 0xda, 0xcd, 0x56, - 0x54, 0x88, 0xad, 0xde, 0x85, 0x2f, 0xb4, 0xed, 0x1d, 0xcd, 0xe7, 0xd4, 0xd0, 0x05, 0x6e, 0x24, 0x1b, 0x1a, 0x25, - 0x05, 0xa5, 0x08, 0x88, 0x13, 0x79, 0xd9, 0x46, 0xb2, 0xad, 0x78, 0x92, 0x67, 0xf5, 0xf4, 0xfb, 0xb6, 0xff, 0x1f, - 0x22, 0x28, 0x4d, 0x5d, 0x85, 0x7b, 0x00, 0x00}; + 0x5f, 0xee, 0x57, 0xf4, 0xd0, 0x49, 0xe1, 0x85, 0xf1, 0xfb, 0x57, 0x16, 0x79, 0xa2, 0xaf, 0xa8, 0x65, 0x23, 0xca, + 0xf4, 0xf4, 0x91, 0x97, 0xe8, 0x9e, 0x20, 0x54, 0x6e, 0x86, 0xf0, 0x9f, 0xf1, 0x09, 0xb0, 0x2d, 0xfc, 0xd4, 0x9c, + 0x1d, 0xc0, 0x4f, 0xaa, 0xb5, 0x19, 0xc6, 0x0a, 0x0a, 0xbb, 0xac, 0x85, 0xf3, 0x29, 0x1c, 0x41, 0x51, 0x84, 0x7d, + 0x7a, 0x77, 0x70, 0x46, 0x91, 0x63, 0xf8, 0xee, 0x73, 0x4e, 0x1d, 0x44, 0xb6, 0x72, 0x68, 0xcb, 0xc0, 0xce, 0x1c, + 0xbf, 0x79, 0xab, 0x5e, 0x4e, 0xc5, 0x8d, 0x98, 0x90, 0xae, 0xb3, 0xed, 0xe8, 0xa0, 0x10, 0x26, 0x90, 0x87, 0xcb, + 0x8c, 0x47, 0xf8, 0x4b, 0x95, 0x78, 0xcc, 0x13, 0xbc, 0x6c, 0x2b, 0x2f, 0xa6, 0x61, 0xbe, 0xf3, 0x77, 0x98, 0xab, + 0xad, 0x30, 0x9e, 0x61, 0x20, 0xe2, 0x15, 0x1b, 0xc7, 0x91, 0x63, 0x3b, 0x73, 0x90, 0x04, 0x30, 0x66, 0xab, 0x96, + 0x28, 0x4d, 0x39, 0xd2, 0xae, 0xad, 0x7e, 0x8f, 0x4f, 0x8e, 0xdf, 0x45, 0x0b, 0x0f, 0x65, 0xe0, 0xd2, 0x96, 0x9e, + 0x05, 0x8c, 0xaf, 0x4a, 0x72, 0x54, 0x22, 0x95, 0x35, 0x62, 0x0b, 0x73, 0xa4, 0xe3, 0x77, 0xc1, 0x3d, 0xa1, 0x62, + 0xfc, 0xce, 0xd4, 0x0f, 0x4e, 0x6b, 0x1b, 0xcc, 0x25, 0xa3, 0x9b, 0x1e, 0x68, 0xb8, 0x12, 0x9e, 0x12, 0x41, 0x87, + 0x46, 0xa9, 0xa7, 0x7f, 0xb1, 0x59, 0x15, 0x86, 0x2a, 0x1e, 0x5f, 0x1c, 0xc8, 0xbb, 0xe4, 0x36, 0x46, 0x84, 0xe9, + 0x24, 0xa0, 0xfa, 0x8e, 0xd5, 0x26, 0xa8, 0x21, 0xfa, 0xed, 0x92, 0x74, 0x9e, 0x8e, 0x9a, 0x06, 0x53, 0xb9, 0x2d, + 0xe9, 0x91, 0x84, 0xb6, 0xd0, 0x33, 0x63, 0x76, 0x1a, 0x8f, 0xa4, 0xba, 0xf0, 0x2c, 0x79, 0x05, 0xa5, 0x45, 0x58, + 0x84, 0x1d, 0x4f, 0xf8, 0xe4, 0xf0, 0x82, 0xda, 0x32, 0x4c, 0x33, 0xbb, 0x7f, 0xaf, 0xa7, 0x21, 0xa9, 0x67, 0xc1, + 0xdb, 0xf8, 0xab, 0x2e, 0x0f, 0xc1, 0x07, 0xfc, 0xfb, 0xf0, 0x1e, 0xfe, 0xb2, 0xcb, 0x7b, 0x43, 0xdb, 0xf5, 0x49, + 0xd8, 0xde, 0xab, 0x7e, 0xe3, 0x25, 0x4a, 0x9a, 0x4d, 0xd0, 0x8b, 0xde, 0x6d, 0x15, 0xa4, 0x54, 0x86, 0xdd, 0x9d, + 0x4a, 0x19, 0xc2, 0xb3, 0x21, 0xfd, 0x40, 0x30, 0x77, 0xfd, 0x1d, 0x43, 0xc4, 0x9e, 0xb6, 0xf0, 0x67, 0x4d, 0xc8, + 0xde, 0x87, 0x06, 0x4a, 0xca, 0xbe, 0x84, 0xe6, 0xdb, 0x42, 0xa0, 0x43, 0xbf, 0x1f, 0x49, 0x04, 0x0a, 0xf1, 0x57, + 0xe7, 0x98, 0x35, 0x87, 0x53, 0x22, 0xf7, 0xc0, 0xf6, 0x8c, 0x38, 0x96, 0x60, 0x57, 0x19, 0xa5, 0x79, 0x0a, 0x7d, + 0x1d, 0xfd, 0x79, 0xe8, 0x75, 0x75, 0x5e, 0x6d, 0xd3, 0xac, 0x99, 0x02, 0x19, 0xbe, 0x71, 0x00, 0x46, 0x57, 0x22, + 0xc4, 0x67, 0xd2, 0x84, 0x78, 0xa8, 0x3e, 0x24, 0xb7, 0x26, 0xab, 0xd7, 0xdc, 0x14, 0xac, 0x62, 0x1a, 0xda, 0x17, + 0x98, 0x8a, 0x83, 0x3f, 0xab, 0x62, 0xf5, 0x20, 0x19, 0xca, 0x4f, 0x22, 0xfc, 0x2d, 0x2f, 0xf4, 0xa3, 0xac, 0x36, + 0x20, 0xa7, 0xef, 0x60, 0x12, 0xa4, 0x2f, 0xc6, 0x65, 0x13, 0x09, 0xb0, 0x43, 0xf0, 0x97, 0x06, 0x56, 0x57, 0x30, + 0xe4, 0xdd, 0x4a, 0xcc, 0x55, 0x18, 0xc7, 0x39, 0x5d, 0xd9, 0x55, 0xf8, 0xd7, 0x22, 0xa5, 0x15, 0xa9, 0x69, 0x57, + 0xb2, 0x62, 0x60, 0x63, 0x11, 0x88, 0x1a, 0x91, 0xe4, 0x66, 0x7e, 0x08, 0xda, 0xbc, 0x53, 0x39, 0x16, 0xf9, 0x6d, + 0xf8, 0xa1, 0x7c, 0x5b, 0x10, 0xd9, 0x06, 0xf1, 0x78, 0x25, 0x4e, 0x64, 0x34, 0xe1, 0x55, 0xc4, 0xea, 0x37, 0x1f, + 0x99, 0x1b, 0xde, 0x36, 0x57, 0x4b, 0x8f, 0x4b, 0xeb, 0xe0, 0xca, 0xb8, 0xe0, 0x31, 0x8b, 0xb8, 0x1f, 0xa5, 0x94, + 0xf3, 0xe4, 0x18, 0x62, 0xc1, 0xeb, 0xb0, 0x6d, 0xb7, 0x04, 0xc9, 0x63, 0xfc, 0x6a, 0x28, 0x41, 0x7a, 0x1f, 0x0a, + 0xab, 0x44, 0xb0, 0xdd, 0x69, 0xb7, 0xff, 0xe6, 0x60, 0xcf, 0x12, 0xbb, 0x79, 0x77, 0x0b, 0x5e, 0x77, 0xc9, 0xcd, + 0x16, 0x79, 0x1f, 0xa1, 0xc8, 0xfb, 0xb0, 0x44, 0xa2, 0x58, 0x68, 0x6f, 0x09, 0x34, 0x6d, 0x8b, 0xa5, 0x23, 0x11, + 0x1b, 0x9c, 0x81, 0x1b, 0x12, 0xe3, 0xc7, 0xc2, 0xb6, 0xb0, 0x5b, 0x0b, 0x57, 0xda, 0x56, 0x99, 0x33, 0xca, 0xf0, + 0xe0, 0xa9, 0x8a, 0x24, 0x82, 0xb9, 0xc0, 0x54, 0x12, 0x8d, 0x1c, 0x4a, 0xe7, 0xba, 0xae, 0xb6, 0x2e, 0x16, 0xc7, + 0x33, 0x90, 0x43, 0x2a, 0xf1, 0xe5, 0xbd, 0xec, 0xb0, 0x4b, 0x53, 0x61, 0xb2, 0xed, 0x6a, 0xa4, 0x73, 0xda, 0xe9, + 0xef, 0x46, 0xd2, 0x8e, 0xc2, 0xbd, 0x5b, 0xc0, 0xe6, 0x05, 0xf5, 0x89, 0xc6, 0x8a, 0x1f, 0x67, 0x5b, 0x67, 0xec, + 0xb8, 0x15, 0xcd, 0xe3, 0x2a, 0xac, 0x88, 0x5a, 0xb5, 0xbf, 0xab, 0x14, 0xac, 0x4c, 0xdf, 0x94, 0x8f, 0x91, 0x91, + 0x1d, 0x82, 0x84, 0x23, 0x06, 0x2d, 0x65, 0xcc, 0x92, 0x8c, 0x51, 0x20, 0x3e, 0xc0, 0x4a, 0xfc, 0xab, 0x62, 0x9b, + 0x52, 0x13, 0x94, 0x76, 0xff, 0xaf, 0xff, 0xeb, 0x7f, 0xcb, 0x70, 0x25, 0x90, 0x15, 0xc0, 0xc2, 0xf4, 0x9a, 0xea, + 0xe4, 0x92, 0x9d, 0x83, 0x83, 0x1b, 0x8f, 0x5b, 0xd3, 0x28, 0x99, 0x00, 0x04, 0x05, 0x13, 0xda, 0x50, 0xd6, 0x03, + 0x17, 0x48, 0xb0, 0xcc, 0x43, 0x7f, 0x09, 0x5e, 0xbd, 0x08, 0x57, 0xec, 0x77, 0xe5, 0xc3, 0xaa, 0x3c, 0x64, 0x62, + 0x68, 0x23, 0x3b, 0xd6, 0xe0, 0xb9, 0x5a, 0x86, 0xac, 0xfa, 0xc5, 0x4b, 0x52, 0x78, 0xb0, 0x5a, 0x7a, 0x2c, 0xb4, + 0xd4, 0x07, 0x2c, 0xff, 0xf6, 0xcf, 0xff, 0xf9, 0xbf, 0xab, 0x57, 0x3c, 0x37, 0xf9, 0xeb, 0x3f, 0xfd, 0xc3, 0xff, + 0xfd, 0x3f, 0xff, 0x05, 0xb3, 0x8f, 0xe5, 0xd9, 0x0a, 0x6d, 0x25, 0xab, 0x3a, 0x58, 0x11, 0x7b, 0xca, 0xaa, 0x1c, + 0x99, 0x7a, 0x1a, 0xed, 0x3e, 0x4d, 0x48, 0xbc, 0x29, 0xa1, 0x23, 0xbe, 0xa6, 0xb4, 0x6b, 0xa2, 0xda, 0x35, 0xe4, + 0x83, 0xa5, 0xb4, 0x28, 0x5d, 0xc0, 0xde, 0x69, 0xdb, 0xd5, 0xf2, 0xf6, 0x8d, 0xbe, 0x5b, 0xb8, 0x30, 0xb7, 0xca, + 0xec, 0xf1, 0xf5, 0xb2, 0x2d, 0x55, 0x78, 0x0c, 0x4b, 0xca, 0xaa, 0xdc, 0xc2, 0xb8, 0xf5, 0x12, 0x5f, 0x83, 0xae, + 0x51, 0x4c, 0xab, 0x5c, 0xeb, 0xd3, 0xfb, 0x65, 0x01, 0x88, 0x4e, 0x70, 0x69, 0x44, 0x10, 0x8e, 0xce, 0x64, 0x5b, + 0x68, 0xc0, 0x24, 0x17, 0x25, 0x8d, 0x22, 0xbc, 0xa4, 0xfb, 0x8f, 0xfe, 0xae, 0xfc, 0xd3, 0x0c, 0xad, 0x02, 0xcb, + 0x99, 0x45, 0xe7, 0xd2, 0x77, 0x7a, 0xd0, 0x6e, 0xcf, 0xcf, 0xdd, 0x65, 0x35, 0x83, 0x77, 0xd5, 0x64, 0x14, 0xb8, + 0x33, 0x07, 0xa4, 0xc3, 0x5c, 0x1d, 0x23, 0x04, 0x77, 0xa1, 0x8d, 0x21, 0xa5, 0xb2, 0xfc, 0x72, 0x49, 0x61, 0xaa, + 0xf8, 0x37, 0x3c, 0x74, 0x95, 0x11, 0x3d, 0x28, 0x31, 0xb0, 0x58, 0x1a, 0xbd, 0xba, 0xa2, 0xd7, 0xb4, 0xb3, 0x9a, + 0xf3, 0x62, 0x1e, 0x1a, 0x9b, 0xc7, 0xbd, 0xf7, 0xf1, 0x00, 0x77, 0xda, 0xf1, 0xa6, 0xdd, 0xa5, 0x1e, 0x9e, 0xf3, + 0x6c, 0x66, 0x9e, 0x12, 0xb3, 0x88, 0x8d, 0xd8, 0x44, 0x45, 0x42, 0x65, 0xbd, 0x38, 0x01, 0x2e, 0xbf, 0xc0, 0xed, + 0x06, 0xb4, 0xcd, 0x22, 0x1e, 0x10, 0xd3, 0xf6, 0xcc, 0x73, 0xe4, 0x08, 0x4f, 0xe8, 0xb3, 0xa5, 0x31, 0x57, 0x4f, + 0x34, 0xc5, 0x78, 0x63, 0x3d, 0x9f, 0xa8, 0xf4, 0xa9, 0xbb, 0x39, 0x94, 0x08, 0x57, 0xbc, 0x90, 0xc7, 0xb3, 0xef, + 0x6a, 0x7e, 0xbe, 0x14, 0xc5, 0xe0, 0x5a, 0xaf, 0xad, 0x17, 0x6a, 0x51, 0xd4, 0xbe, 0x00, 0x6b, 0x87, 0xc0, 0xb4, + 0x9b, 0xad, 0xa8, 0x10, 0x5b, 0xbd, 0x0b, 0x5f, 0x68, 0x9b, 0x3e, 0x9a, 0xcf, 0xa9, 0xa1, 0x0b, 0xdc, 0x48, 0xb6, + 0x39, 0x4a, 0x0a, 0x4a, 0x3d, 0x10, 0x27, 0xfd, 0xb2, 0x8d, 0x64, 0x5b, 0xf1, 0x24, 0x73, 0x00, 0xe8, 0xf7, 0x78, + 0xff, 0x3f, 0x32, 0x18, 0x26, 0x95, 0xdd, 0x7b, 0x00, 0x00}; } // namespace web_server } // namespace esphome diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index bc37337ca5..cdabe66d44 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -81,6 +81,7 @@ class WebServerBase : public Component { return; } this->server_ = std::make_shared(this->port_); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); this->server_->begin(); for (auto *handler : this->handlers_) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 846a8e1303..a4c246da89 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -332,8 +332,7 @@ def manual_ip(config): ) -def wifi_network(config, static_ip): - ap = cg.variable(config[CONF_ID], WiFiAP()) +def wifi_network(config, ap, static_ip): if CONF_SSID in config: cg.add(ap.set_ssid(config[CONF_SSID])) if CONF_PASSWORD in config: @@ -360,14 +359,21 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) - for network in config.get(CONF_NETWORKS, []): + def add_sta(ap, network): ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) - cg.add(var.add_sta(wifi_network(network, ip_config))) + cg.add(var.add_sta(wifi_network(network, ap, ip_config))) + + for network in config.get(CONF_NETWORKS, []): + cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network) if CONF_AP in config: conf = config[CONF_AP] ip_config = conf.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) - cg.add(var.set_ap(wifi_network(conf, ip_config))) + cg.with_local_variable( + conf[CONF_ID], + WiFiAP(), + lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_config))), + ) cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) diff --git a/esphome/components/xpt2046/__init__.py b/esphome/components/xpt2046/__init__.py index 3de89a6425..3b8a925bb2 100644 --- a/esphome/components/xpt2046/__init__.py +++ b/esphome/components/xpt2046/__init__.py @@ -1,129 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation -from esphome import pins -from esphome.components import spi -from esphome.const import CONF_ID, CONF_ON_STATE, CONF_THRESHOLD, CONF_TRIGGER_ID -CODEOWNERS = ["@numo68"] -AUTO_LOAD = ["binary_sensor"] -DEPENDENCIES = ["spi"] -MULTI_CONF = True - -CONF_REPORT_INTERVAL = "report_interval" -CONF_CALIBRATION_X_MIN = "calibration_x_min" -CONF_CALIBRATION_X_MAX = "calibration_x_max" -CONF_CALIBRATION_Y_MIN = "calibration_y_min" -CONF_CALIBRATION_Y_MAX = "calibration_y_max" -CONF_DIMENSION_X = "dimension_x" -CONF_DIMENSION_Y = "dimension_y" -CONF_SWAP_X_Y = "swap_x_y" -CONF_IRQ_PIN = "irq_pin" - -xpt2046_ns = cg.esphome_ns.namespace("xpt2046") -CONF_XPT2046_ID = "xpt2046_id" - -XPT2046Component = xpt2046_ns.class_( - "XPT2046Component", cg.PollingComponent, spi.SPIDevice +CONFIG_SCHEMA = cv.invalid( + "This component sould now be used as platform of the Touchscreen component." ) - -XPT2046OnStateTrigger = xpt2046_ns.class_( - "XPT2046OnStateTrigger", automation.Trigger.template(cg.int_, cg.int_, cg.bool_) -) - - -def validate_xpt2046(config): - if ( - abs( - cv.int_(config[CONF_CALIBRATION_X_MAX]) - - cv.int_(config[CONF_CALIBRATION_X_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration X values difference < 1000") - - if ( - abs( - cv.int_(config[CONF_CALIBRATION_Y_MAX]) - - cv.int_(config[CONF_CALIBRATION_Y_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration Y values difference < 1000") - - return config - - -def report_interval(value): - if value == "never": - return 4294967295 # uint32_t max - return cv.positive_time_period_milliseconds(value) - - -CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(XPT2046Component), - cv.Optional(CONF_IRQ_PIN): pins.gpio_input_pin_schema, - cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_DIMENSION_X, default=100): cv.positive_not_null_int, - cv.Optional(CONF_DIMENSION_Y, default=100): cv.positive_not_null_int, - cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), - cv.Optional(CONF_REPORT_INTERVAL, default="never"): report_interval, - cv.Optional(CONF_SWAP_X_Y, default=False): cv.boolean, - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - XPT2046OnStateTrigger - ), - } - ), - } - ) - .extend(cv.polling_component_schema("50ms")) - .extend(spi.spi_device_schema()), - validate_xpt2046, -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await spi.register_spi_device(var, config) - - cg.add(var.set_threshold(config[CONF_THRESHOLD])) - cg.add(var.set_report_interval(config[CONF_REPORT_INTERVAL])) - cg.add(var.set_dimensions(config[CONF_DIMENSION_X], config[CONF_DIMENSION_Y])) - cg.add( - var.set_calibration( - config[CONF_CALIBRATION_X_MIN], - config[CONF_CALIBRATION_X_MAX], - config[CONF_CALIBRATION_Y_MIN], - config[CONF_CALIBRATION_Y_MAX], - ) - ) - - if CONF_SWAP_X_Y in config: - cg.add(var.set_swap_x_y(config[CONF_SWAP_X_Y])) - - if CONF_IRQ_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) - cg.add(var.set_irq_pin(pin)) - - for conf in config.get(CONF_ON_STATE, []): - await automation.build_automation( - var.get_on_state_trigger(), - [(cg.int_, "x"), (cg.int_, "y"), (cg.bool_, "touched")], - conf, - ) diff --git a/esphome/components/xpt2046/binary_sensor.py b/esphome/components/xpt2046/binary_sensor.py index 6ec09a2295..5a6cfe4919 100644 --- a/esphome/components/xpt2046/binary_sensor.py +++ b/esphome/components/xpt2046/binary_sensor.py @@ -1,55 +1,3 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import binary_sensor -from . import ( - xpt2046_ns, - XPT2046Component, - CONF_XPT2046_ID, -) - -CONF_X_MIN = "x_min" -CONF_X_MAX = "x_max" -CONF_Y_MIN = "y_min" -CONF_Y_MAX = "y_max" - -DEPENDENCIES = ["xpt2046"] -XPT2046Button = xpt2046_ns.class_("XPT2046Button", binary_sensor.BinarySensor) - - -def validate_xpt2046_button(config): - if cv.int_(config[CONF_X_MAX]) < cv.int_(config[CONF_X_MIN]) or cv.int_( - config[CONF_Y_MAX] - ) < cv.int_(config[CONF_Y_MIN]): - raise cv.Invalid("x_max is less than x_min or y_max is less than y_min") - - return config - - -CONFIG_SCHEMA = cv.All( - binary_sensor.binary_sensor_schema(XPT2046Button).extend( - { - cv.GenerateID(CONF_XPT2046_ID): cv.use_id(XPT2046Component), - cv.Required(CONF_X_MIN): cv.int_range(min=0, max=4095), - cv.Required(CONF_X_MAX): cv.int_range(min=0, max=4095), - cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=4095), - cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=4095), - } - ), - validate_xpt2046_button, -) - - -async def to_code(config): - var = await binary_sensor.new_binary_sensor(config) - hub = await cg.get_variable(config[CONF_XPT2046_ID]) - cg.add( - var.set_area( - config[CONF_X_MIN], - config[CONF_X_MAX], - config[CONF_Y_MIN], - config[CONF_Y_MAX], - ) - ) - - cg.add(hub.register_button(var)) +CONFIG_SCHEMA = cv.invalid("Rename this platform component to Touchscreen.") diff --git a/esphome/components/xpt2046/touchscreen.py b/esphome/components/xpt2046/touchscreen.py new file mode 100644 index 0000000000..868525ccb1 --- /dev/null +++ b/esphome/components/xpt2046/touchscreen.py @@ -0,0 +1,116 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import spi, touchscreen +from esphome.const import CONF_ID, CONF_THRESHOLD + +CODEOWNERS = ["@numo68", "@nielsnl68"] +DEPENDENCIES = ["spi"] + +XPT2046_ns = cg.esphome_ns.namespace("xpt2046") +XPT2046Component = XPT2046_ns.class_( + "XPT2046Component", touchscreen.Touchscreen, cg.PollingComponent, spi.SPIDevice +) + +CONF_INTERRUPT_PIN = "interrupt_pin" + +CONF_REPORT_INTERVAL = "report_interval" +CONF_CALIBRATION_X_MIN = "calibration_x_min" +CONF_CALIBRATION_X_MAX = "calibration_x_max" +CONF_CALIBRATION_Y_MIN = "calibration_y_min" +CONF_CALIBRATION_Y_MAX = "calibration_y_max" +CONF_SWAP_X_Y = "swap_x_y" + +# obsolete Keys +CONF_DIMENSION_X = "dimension_x" +CONF_DIMENSION_Y = "dimension_y" +CONF_IRQ_PIN = "irq_pin" + + +def validate_xpt2046(config): + if ( + abs( + cv.int_(config[CONF_CALIBRATION_X_MAX]) + - cv.int_(config[CONF_CALIBRATION_X_MIN]) + ) + < 1000 + ): + raise cv.Invalid("Calibration X values difference < 1000") + + if ( + abs( + cv.int_(config[CONF_CALIBRATION_Y_MAX]) + - cv.int_(config[CONF_CALIBRATION_Y_MIN]) + ) + < 1000 + ): + raise cv.Invalid("Calibration Y values difference < 1000") + + return config + + +def report_interval(value): + if value == "never": + return 4294967295 # uint32_t max + return cv.positive_time_period_milliseconds(value) + + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XPT2046Component), + cv.Optional(CONF_INTERRUPT_PIN): cv.All( + pins.internal_gpio_input_pin_schema + ), + cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), + cv.Optional(CONF_REPORT_INTERVAL, default="never"): report_interval, + cv.Optional(CONF_SWAP_X_Y, default=False): cv.boolean, + # obsolete Keys + cv.Optional(CONF_IRQ_PIN): cv.invalid("Rename IRQ_PIN to INTERUPT_PIN"), + cv.Optional(CONF_DIMENSION_X): cv.invalid( + "This key is now obsolete, please remove it" + ), + cv.Optional(CONF_DIMENSION_Y): cv.invalid( + "This key is now obsolete, please remove it" + ), + }, + ) + .extend(cv.polling_component_schema("50ms")) + .extend(spi.spi_device_schema()), +).add_extra(validate_xpt2046) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + await touchscreen.register_touchscreen(var, config) + + cg.add(var.set_threshold(config[CONF_THRESHOLD])) + cg.add(var.set_report_interval(config[CONF_REPORT_INTERVAL])) + cg.add(var.set_swap_x_y(config[CONF_SWAP_X_Y])) + cg.add( + var.set_calibration( + config[CONF_CALIBRATION_X_MIN], + config[CONF_CALIBRATION_X_MAX], + config[CONF_CALIBRATION_Y_MIN], + config[CONF_CALIBRATION_Y_MAX], + ) + ) + + if CONF_INTERRUPT_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_irq_pin(pin)) diff --git a/esphome/components/xpt2046/xpt2046.cpp b/esphome/components/xpt2046/xpt2046.cpp index aaadeea52e..d6cbe39aa0 100644 --- a/esphome/components/xpt2046/xpt2046.cpp +++ b/esphome/components/xpt2046/xpt2046.cpp @@ -9,31 +9,38 @@ namespace xpt2046 { static const char *const TAG = "xpt2046"; +void XPT2046TouchscreenStore::gpio_intr(XPT2046TouchscreenStore *store) { store->touch = true; } + void XPT2046Component::setup() { if (this->irq_pin_ != nullptr) { // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state // while the channels are read and wiring it as an interrupt is not straightforward and would // need careful masking. A GPIO poll is cheap so we'll just use that. + this->irq_pin_->setup(); // INPUT + this->irq_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->irq_pin_->setup(); + + this->store_.pin = this->irq_pin_->to_isr(); + this->irq_pin_->attach_interrupt(XPT2046TouchscreenStore::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); } spi_setup(); read_adc_(0xD0); // ADC powerdown, enable PENIRQ pin } void XPT2046Component::loop() { - if (this->irq_pin_ != nullptr) { - // Force immediate update if a falling edge (= touched is seen) Ignore if still active - // (that would mean that we missed the release because of a too long update interval) - bool val = this->irq_pin_->digital_read(); - if (!val && this->last_irq_ && !this->touched) { - ESP_LOGD(TAG, "Falling penirq edge, forcing update"); - update(); - } - this->last_irq_ = val; - } + if ((this->irq_pin_ == nullptr) || (!this->store_.touch)) + return; + this->store_.touch = false; + check_touch_(); } void XPT2046Component::update() { + if (this->irq_pin_ == nullptr) + check_touch_(); +} + +void XPT2046Component::check_touch_() { int16_t data[6]; bool touch = false; uint32_t now = millis(); @@ -42,13 +49,13 @@ void XPT2046Component::update() { // In case the penirq pin is present only do the SPI transaction if it reports a touch (is low). // The touch has to be also confirmed with checking the pressure over threshold - if (this->irq_pin_ == nullptr || !this->irq_pin_->digital_read()) { + if ((this->irq_pin_ == nullptr) || !this->irq_pin_->digital_read()) { enable(); - int16_t z1 = read_adc_(0xB1 /* Z1 */); - int16_t z2 = read_adc_(0xC1 /* Z2 */); + int16_t touch_pressure_1 = read_adc_(0xB1 /* touch_pressure_1 */); + int16_t touch_pressure_2 = read_adc_(0xC1 /* touch_pressure_2 */); - this->z_raw = z1 + 4095 - z2; + this->z_raw = touch_pressure_1 + 4095 - touch_pressure_2; touch = (this->z_raw >= this->threshold_); if (touch) { @@ -63,64 +70,73 @@ void XPT2046Component::update() { data[5] = read_adc_(0x90 /* Y */); // Last Y touch power down disable(); - } - if (touch) { - this->x_raw = best_two_avg(data[0], data[2], data[4]); - this->y_raw = best_two_avg(data[1], data[3], data[5]); - } else { - this->x_raw = this->y_raw = 0; - } + if (touch) { + this->x_raw = best_two_avg(data[0], data[2], data[4]); + this->y_raw = best_two_avg(data[1], data[3], data[5]); - ESP_LOGV(TAG, "Update [x, y] = [%d, %d], z = %d%s", this->x_raw, this->y_raw, this->z_raw, (touch ? " touched" : "")); + ESP_LOGVV(TAG, "Update [x, y] = [%d, %d], z = %d", this->x_raw, this->y_raw, this->z_raw); - if (touch) { - // Normalize raw data according to calibration min and max + TouchPoint touchpoint; - int16_t x_val = normalize(this->x_raw, this->x_raw_min_, this->x_raw_max_); - int16_t y_val = normalize(this->y_raw, this->y_raw_min_, this->y_raw_max_); + touchpoint.x = normalize(this->x_raw, this->x_raw_min_, this->x_raw_max_); + touchpoint.y = normalize(this->y_raw, this->y_raw_min_, this->y_raw_max_); - if (this->swap_x_y_) { - std::swap(x_val, y_val); - } + if (this->swap_x_y_) { + std::swap(touchpoint.x, touchpoint.y); + } - if (this->invert_x_) { - x_val = 0x7fff - x_val; - } + if (this->invert_x_) { + touchpoint.x = 0xfff - touchpoint.x; + } - if (this->invert_y_) { - y_val = 0x7fff - y_val; - } + if (this->invert_y_) { + touchpoint.y = 0xfff - touchpoint.y; + } - x_val = (int16_t)((int) x_val * this->x_dim_ / 0x7fff); - y_val = (int16_t)((int) y_val * this->y_dim_ / 0x7fff); + switch (static_cast(this->display_->get_rotation())) { + case ROTATE_0_DEGREES: + break; + case ROTATE_90_DEGREES: + std::swap(touchpoint.x, touchpoint.y); + touchpoint.y = 0xfff - touchpoint.y; + break; + case ROTATE_180_DEGREES: + touchpoint.x = 0xfff - touchpoint.x; + touchpoint.y = 0xfff - touchpoint.y; + break; + case ROTATE_270_DEGREES: + std::swap(touchpoint.x, touchpoint.y); + touchpoint.x = 0xfff - touchpoint.x; + break; + } - if (!this->touched || (now - this->last_pos_ms_) >= this->report_millis_) { - ESP_LOGD(TAG, "Raw [x, y] = [%d, %d], transformed = [%d, %d]", this->x_raw, this->y_raw, x_val, y_val); + touchpoint.x = (int16_t)((int) touchpoint.x * this->display_->get_width() / 0xfff); + touchpoint.y = (int16_t)((int) touchpoint.y * this->display_->get_height() / 0xfff); - this->x = x_val; - this->y = y_val; - this->touched = true; - this->last_pos_ms_ = now; + if (!this->touched || (now - this->last_pos_ms_) >= this->report_millis_) { + ESP_LOGV(TAG, "Touching at [%03X, %03X] => [%3d, %3d]", this->x_raw, this->y_raw, touchpoint.x, touchpoint.y); - this->on_state_trigger_->process(this->x, this->y, true); - for (auto *button : this->buttons_) - button->touch(this->x, this->y); - } - } else { - if (this->touched) { - ESP_LOGD(TAG, "Released [%d, %d]", this->x, this->y); + this->defer([this, touchpoint]() { this->send_touch_(touchpoint); }); - this->touched = false; - - this->on_state_trigger_->process(this->x, this->y, false); - for (auto *button : this->buttons_) - button->release(); + this->x = touchpoint.x; + this->y = touchpoint.y; + this->touched = true; + this->last_pos_ms_ = now; + } + } else { + this->x_raw = this->y_raw = 0; + if (this->touched) { + ESP_LOGV(TAG, "Released [%d, %d]", this->x, this->y); + this->touched = false; + for (auto *listener : this->touch_listeners_) + listener->release(); + } } } } -void XPT2046Component::set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { +void XPT2046Component::set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { // NOLINT this->x_raw_min_ = std::min(x_min, x_max); this->x_raw_max_ = std::max(x_min, x_max); this->y_raw_min_ = std::min(y_min, y_max); @@ -137,11 +153,11 @@ void XPT2046Component::dump_config() { ESP_LOGCONFIG(TAG, " X max: %d", this->x_raw_max_); ESP_LOGCONFIG(TAG, " Y min: %d", this->y_raw_min_); ESP_LOGCONFIG(TAG, " Y max: %d", this->y_raw_max_); - ESP_LOGCONFIG(TAG, " X dim: %d", this->x_dim_); - ESP_LOGCONFIG(TAG, " Y dim: %d", this->y_dim_); - if (this->swap_x_y_) { - ESP_LOGCONFIG(TAG, " Swap X/Y"); - } + + ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->swap_x_y_)); + ESP_LOGCONFIG(TAG, " Invert X: %s", YESNO(this->invert_x_)); + ESP_LOGCONFIG(TAG, " Invert Y: %s", YESNO(this->invert_y_)); + ESP_LOGCONFIG(TAG, " threshold: %d", this->threshold_); ESP_LOGCONFIG(TAG, " Report interval: %u", this->report_millis_); @@ -150,8 +166,8 @@ void XPT2046Component::dump_config() { float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; } -int16_t XPT2046Component::best_two_avg(int16_t x, int16_t y, int16_t z) { - int16_t da, db, dc; +int16_t XPT2046Component::best_two_avg(int16_t x, int16_t y, int16_t z) { // NOLINT + int16_t da, db, dc; // NOLINT int16_t reta = 0; da = (x > y) ? x - y : y - x; @@ -175,15 +191,15 @@ int16_t XPT2046Component::normalize(int16_t val, int16_t min_val, int16_t max_va if (val <= min_val) { ret = 0; } else if (val >= max_val) { - ret = 0x7fff; + ret = 0xfff; } else { - ret = (int16_t)((int) 0x7fff * (val - min_val) / (max_val - min_val)); + ret = (int16_t)((int) 0xfff * (val - min_val) / (max_val - min_val)); } return ret; } -int16_t XPT2046Component::read_adc_(uint8_t ctrl) { +int16_t XPT2046Component::read_adc_(uint8_t ctrl) { // NOLINT uint8_t data[2]; write_byte(ctrl); @@ -193,25 +209,5 @@ int16_t XPT2046Component::read_adc_(uint8_t ctrl) { return ((data[0] << 8) | data[1]) >> 3; } -void XPT2046OnStateTrigger::process(int x, int y, bool touched) { this->trigger(x, y, touched); } - -void XPT2046Button::touch(int16_t x, int16_t y) { - bool touched = (x >= this->x_min_ && x <= this->x_max_ && y >= this->y_min_ && y <= this->y_max_); - - if (touched) { - this->publish_state(true); - this->state_ = true; - } else { - release(); - } -} - -void XPT2046Button::release() { - if (this->state_) { - this->publish_state(false); - this->state_ = false; - } -} - } // namespace xpt2046 } // namespace esphome diff --git a/esphome/components/xpt2046/xpt2046.h b/esphome/components/xpt2046/xpt2046.h index e7270f7d7d..c3d0462c6a 100644 --- a/esphome/components/xpt2046/xpt2046.h +++ b/esphome/components/xpt2046/xpt2046.h @@ -3,42 +3,31 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" #include "esphome/components/spi/spi.h" -#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" namespace esphome { namespace xpt2046 { -class XPT2046OnStateTrigger : public Trigger { - public: - void process(int x, int y, bool touched); +using namespace touchscreen; + +struct XPT2046TouchscreenStore { + volatile bool touch; + ISRInternalGPIOPin pin; + + static void gpio_intr(XPT2046TouchscreenStore *store); }; -class XPT2046Button : public binary_sensor::BinarySensor { - public: - /// Set the touch screen area where the button will detect the touch. - void set_area(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { - this->x_min_ = x_min; - this->x_max_ = x_max; - this->y_min_ = y_min; - this->y_max_ = y_max; - } - - void touch(int16_t x, int16_t y); - void release(); - - protected: - int16_t x_min_, x_max_, y_min_, y_max_; - bool state_{false}; -}; - -class XPT2046Component : public PollingComponent, +class XPT2046Component : public Touchscreen, + public PollingComponent, public spi::SPIDevice { public: /// Set the logical touch screen dimensions. void set_dimensions(int16_t x, int16_t y) { - this->x_dim_ = x; - this->y_dim_ = y; + this->display_width_ = x; + this->display_height_ = y; } /// Set the coordinates for the touch screen edges. void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max); @@ -47,14 +36,12 @@ class XPT2046Component : public PollingComponent, /// Set the interval to report the touch point perodically. void set_report_interval(uint32_t interval) { this->report_millis_ = interval; } + uint32_t get_report_interval() { return this->report_millis_; } + /// Set the threshold for the touch detection. void set_threshold(int16_t threshold) { this->threshold_ = threshold; } /// Set the pin used to detect the touch. - void set_irq_pin(GPIOPin *pin) { this->irq_pin_ = pin; } - /// Get an access to the on_state automation trigger - XPT2046OnStateTrigger *get_on_state_trigger() const { return this->on_state_trigger_; } - /// Register a virtual button to the component. - void register_button(XPT2046Button *button) { this->buttons_.push_back(button); } + void set_irq_pin(InternalGPIOPin *pin) { this->irq_pin_ = pin; } void setup() override; void dump_config() override; @@ -103,21 +90,19 @@ class XPT2046Component : public PollingComponent, static int16_t normalize(int16_t val, int16_t min_val, int16_t max_val); int16_t read_adc_(uint8_t ctrl); + void check_touch_(); int16_t threshold_; int16_t x_raw_min_, x_raw_max_, y_raw_min_, y_raw_max_; - int16_t x_dim_, y_dim_; + bool invert_x_, invert_y_; bool swap_x_y_; uint32_t report_millis_; uint32_t last_pos_ms_{0}; - GPIOPin *irq_pin_{nullptr}; - bool last_irq_{true}; - - XPT2046OnStateTrigger *on_state_trigger_{new XPT2046OnStateTrigger()}; - std::vector buttons_{}; + InternalGPIOPin *irq_pin_{nullptr}; + XPT2046TouchscreenStore store_; }; } // namespace xpt2046 diff --git a/esphome/config.py b/esphome/config.py index 04717be6f5..0bf4ec8df3 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -23,7 +23,7 @@ from esphome.core import CORE, EsphomeError from esphome.helpers import indent from esphome.util import safe_print, OrderedDict -from typing import List, Optional, Tuple, Union +from typing import Optional, Union from esphome.loader import get_component, get_platform, ComponentManifest from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue from esphome.voluptuous_schema import ExtraKeysInvalid @@ -50,10 +50,10 @@ def iter_components(config): yield p_name, platform, p_config -ConfigPath = List[Union[str, int]] +ConfigPath = list[Union[str, int]] -def _path_begins_with(path, other): # type: (ConfigPath, ConfigPath) -> bool +def _path_begins_with(path: ConfigPath, other: ConfigPath) -> bool: if len(path) < len(other): return False return path[: len(other)] == other @@ -67,7 +67,7 @@ class _ValidationStepTask: self.step = step @property - def _cmp_tuple(self) -> Tuple[float, int]: + def _cmp_tuple(self) -> tuple[float, int]: return (-self.priority, self.id_number) def __eq__(self, other): @@ -84,21 +84,20 @@ class Config(OrderedDict, fv.FinalValidateConfig): def __init__(self): super().__init__() # A list of voluptuous errors - self.errors = [] # type: List[vol.Invalid] + self.errors: list[vol.Invalid] = [] # A list of paths that should be fully outputted # The values will be the paths to all "domain", for example (['logger'], 'logger') # or (['sensor', 'ultrasonic'], 'sensor.ultrasonic') - self.output_paths = [] # type: List[Tuple[ConfigPath, str]] + self.output_paths: list[tuple[ConfigPath, str]] = [] # A list of components ids with the config path - self.declare_ids = [] # type: List[Tuple[core.ID, ConfigPath]] + self.declare_ids: list[tuple[core.ID, ConfigPath]] = [] self._data = {} # Store pending validation tasks (in heap order) - self._validation_tasks: List[_ValidationStepTask] = [] + self._validation_tasks: list[_ValidationStepTask] = [] # ID to ensure stable order for keys with equal priority self._validation_tasks_id = 0 - def add_error(self, error): - # type: (vol.Invalid) -> None + def add_error(self, error: vol.Invalid) -> None: if isinstance(error, vol.MultipleInvalid): for err in error.errors: self.add_error(err) @@ -132,20 +131,16 @@ class Config(OrderedDict, fv.FinalValidateConfig): e.prepend(path) self.add_error(e) - def add_str_error(self, message, path): - # type: (str, ConfigPath) -> None + def add_str_error(self, message: str, path: ConfigPath) -> None: self.add_error(vol.Invalid(message, path)) - def add_output_path(self, path, domain): - # type: (ConfigPath, str) -> None + def add_output_path(self, path: ConfigPath, domain: str) -> None: self.output_paths.append((path, domain)) - def remove_output_path(self, path, domain): - # type: (ConfigPath, str) -> None + def remove_output_path(self, path: ConfigPath, domain: str) -> None: self.output_paths.remove((path, domain)) - def is_in_error_path(self, path): - # type: (ConfigPath) -> bool + def is_in_error_path(self, path: ConfigPath) -> bool: for err in self.errors: if _path_begins_with(err.path, path): return True @@ -157,16 +152,16 @@ class Config(OrderedDict, fv.FinalValidateConfig): conf = conf[key] conf[path[-1]] = value - def get_error_for_path(self, path): - # type: (ConfigPath) -> Optional[vol.Invalid] + def get_error_for_path(self, path: ConfigPath) -> Optional[vol.Invalid]: for err in self.errors: if self.get_deepest_path(err.path) == path: self.errors.remove(err) return err return None - def get_deepest_document_range_for_path(self, path, get_key=False): - # type: (ConfigPath, bool) -> Optional[ESPHomeDataBase] + def get_deepest_document_range_for_path( + self, path: ConfigPath, get_key: bool = False + ) -> Optional[ESPHomeDataBase]: data = self doc_range = None for index, path_item in enumerate(path): @@ -207,8 +202,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): return {} return data - def get_deepest_path(self, path): - # type: (ConfigPath) -> ConfigPath + def get_deepest_path(self, path: ConfigPath) -> ConfigPath: """Return the path that is the deepest reachable by following path.""" data = self part = [] @@ -532,7 +526,7 @@ class IDPassValidationStep(ConfigValidationStep): # because the component that did not validate doesn't have any IDs set return - searching_ids = [] # type: List[Tuple[core.ID, ConfigPath]] + searching_ids: list[tuple[core.ID, ConfigPath]] = [] for id, path in iter_ids(result): if id.is_declaration: if id.id is not None: @@ -780,8 +774,7 @@ def _get_parent_name(path, config): return path[-1] -def _format_vol_invalid(ex, config): - # type: (vol.Invalid, Config) -> str +def _format_vol_invalid(ex: vol.Invalid, config: Config) -> str: message = "" paren = _get_parent_name(ex.path[:-1], config) @@ -862,8 +855,9 @@ def _print_on_next_line(obj): return False -def dump_dict(config, path, at_root=True): - # type: (Config, ConfigPath, bool) -> Tuple[str, bool] +def dump_dict( + config: Config, path: ConfigPath, at_root: bool = True +) -> tuple[str, bool]: conf = config.get_nested_item(path) ret = "" multiline = False diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index 39b57e441b..bfb0f65417 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -5,8 +5,7 @@ from esphome.core import CORE from esphome.helpers import read_file -def read_config_file(path): - # type: (str) -> str +def read_config_file(path: str) -> str: if CORE.vscode and ( not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path) ): diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 0ff0ba83d9..09436c1fbf 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1689,7 +1689,7 @@ class Version: @classmethod def parse(cls, value: str) -> "Version": - match = re.match(r"(\d+).(\d+).(\d+)", value) + match = re.match(r"^(\d+).(\d+).(\d+)-?\w*$", value) if match is None: raise ValueError(f"Not a valid version number {value}") major = int(match[1]) @@ -1703,7 +1703,7 @@ def version_number(value): try: return str(Version.parse(value)) except ValueError as e: - raise Invalid("Not a version number") from e + raise Invalid("Not a valid version number") from e def platformio_version_constraint(value): diff --git a/esphome/const.py b/esphome/const.py index e93808e6d4..9d608ebf9b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.9.4" +__version__ = "2022.10.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" @@ -396,6 +396,7 @@ CONF_MIN_POWER = "min_power" CONF_MIN_RANGE = "min_range" CONF_MIN_TEMPERATURE = "min_temperature" CONF_MIN_VALUE = "min_value" +CONF_MIN_VERSION = "min_version" CONF_MINUTE = "minute" CONF_MINUTES = "minutes" CONF_MISO_PIN = "miso_pin" @@ -904,7 +905,6 @@ DEVICE_CLASS_GARAGE_DOOR = "garage_door" DEVICE_CLASS_HEAT = "heat" DEVICE_CLASS_LIGHT = "light" DEVICE_CLASS_LOCK = "lock" -DEVICE_CLASS_MOISTURE = "moisture" DEVICE_CLASS_MOTION = "motion" DEVICE_CLASS_MOVING = "moving" DEVICE_CLASS_OCCUPANCY = "occupancy" @@ -922,15 +922,17 @@ DEVICE_CLASS_WINDOW = "window" # device classes of both binary_sensor and sensor component DEVICE_CLASS_EMPTY = "" DEVICE_CLASS_BATTERY = "battery" +DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" DEVICE_CLASS_GAS = "gas" +DEVICE_CLASS_MOISTURE = "moisture" DEVICE_CLASS_POWER = "power" # device classes of sensor component DEVICE_CLASS_APPARENT_POWER = "apparent_power" DEVICE_CLASS_AQI = "aqi" DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" -DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" DEVICE_CLASS_CURRENT = "current" DEVICE_CLASS_DATE = "date" +DEVICE_CLASS_DISTANCE = "distance" DEVICE_CLASS_DURATION = "duration" DEVICE_CLASS_ENERGY = "energy" DEVICE_CLASS_FREQUENCY = "frequency" @@ -948,11 +950,14 @@ DEVICE_CLASS_POWER_FACTOR = "power_factor" DEVICE_CLASS_PRESSURE = "pressure" DEVICE_CLASS_REACTIVE_POWER = "reactive_power" DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" +DEVICE_CLASS_SPEED = "speed" DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide" DEVICE_CLASS_TEMPERATURE = "temperature" DEVICE_CLASS_TIMESTAMP = "timestamp" DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" DEVICE_CLASS_VOLTAGE = "voltage" +DEVICE_CLASS_VOLUME = "volume" +DEVICE_CLASS_WEIGHT = "weight" # device classes of both binary_sensor and button component DEVICE_CLASS_UPDATE = "update" # device classes of button component diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 3ee94efd64..a422cd9507 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -2,7 +2,7 @@ import logging import math import os import re -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union +from typing import TYPE_CHECKING, Optional, Union from esphome.const import ( CONF_COMMENT, @@ -469,19 +469,19 @@ class EsphomeCore: # Task counter for pending tasks self.task_counter = 0 # The variable cache, for each ID this holds a MockObj of the variable obj - self.variables: Dict[str, "MockObj"] = {} + self.variables: dict[str, "MockObj"] = {} # A list of statements that go in the main setup() block - self.main_statements: List["Statement"] = [] + self.main_statements: list["Statement"] = [] # A list of statements to insert in the global block (includes and global variables) - self.global_statements: List["Statement"] = [] + self.global_statements: list["Statement"] = [] # A set of platformio libraries to add to the project - self.libraries: List[Library] = [] + self.libraries: list[Library] = [] # A set of build flags to set in the platformio project - self.build_flags: Set[str] = set() + self.build_flags: set[str] = set() # A set of defines to set for the compile process in esphome/core/defines.h - self.defines: Set["Define"] = set() + self.defines: set["Define"] = set() # A map of all platformio options to apply - self.platformio_options: Dict[str, Union[str, List[str]]] = {} + self.platformio_options: dict[str, Union[str, list[str]]] = {} # A set of strings of names of loaded integrations, used to find namespace ID conflicts self.loaded_integrations = set() # A set of component IDs to track what Component subclasses are declared @@ -701,7 +701,7 @@ class EsphomeCore: _LOGGER.debug("Adding define: %s", define) return define - def add_platformio_option(self, key: str, value: Union[str, List[str]]) -> None: + def add_platformio_option(self, key: str, value: Union[str, list[str]]) -> None: new_val = value old_val = self.platformio_options.get(key) if isinstance(old_val, list): @@ -734,7 +734,7 @@ class EsphomeCore: _LOGGER.debug("Waiting for variable %s", id) yield - async def get_variable_with_full_id(self, id: ID) -> Tuple[ID, "MockObj"]: + async def get_variable_with_full_id(self, id: ID) -> tuple[ID, "MockObj"]: if not isinstance(id, ID): raise ValueError(f"ID {id!r} must be of type ID!") return await _FakeAwaitable(self._get_variable_with_full_id_generator(id)) diff --git a/esphome/core/config.py b/esphome/core/config.py index 82cf37d44d..733cdcd66e 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -15,6 +15,7 @@ from esphome.const import ( CONF_FRAMEWORK, CONF_INCLUDES, CONF_LIBRARIES, + CONF_MIN_VERSION, CONF_NAME, CONF_ON_BOOT, CONF_ON_LOOP, @@ -30,6 +31,7 @@ from esphome.const import ( KEY_CORE, TARGET_PLATFORMS, PLATFORM_ESP8266, + __version__ as ESPHOME_VERSION, ) from esphome.core import CORE, coroutine_with_priority from esphome.helpers import copy_file_if_changed, walk_files @@ -96,6 +98,16 @@ def valid_project_name(value: str): return value +def validate_version(value: str): + min_version = cv.Version.parse(value) + current_version = cv.Version.parse(ESPHOME_VERSION) + if current_version < min_version: + raise cv.Invalid( + f"Your ESPHome version is too old. Please update to at least {min_version}" + ) + return value + + CONF_ESP8266_RESTORE_FROM_FLASH = "esp8266_restore_from_flash" CONFIG_SCHEMA = cv.All( cv.Schema( @@ -136,6 +148,9 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_VERSION): cv.string_strict, } ), + cv.Optional(CONF_MIN_VERSION, default=ESPHOME_VERSION): cv.All( + cv.version_number, validate_version + ), } ), validate_hostname, diff --git a/esphome/coroutine.py b/esphome/coroutine.py index 58f79c6b36..5f391dc7ad 100644 --- a/esphome/coroutine.py +++ b/esphome/coroutine.py @@ -48,7 +48,8 @@ import heapq import inspect import logging import types -from typing import Any, Awaitable, Callable, Generator, Iterator, List, Tuple +from typing import Any, Callable +from collections.abc import Awaitable, Generator, Iterator _LOGGER = logging.getLogger(__name__) @@ -177,7 +178,7 @@ class _Task: return _Task(priority, self.id_number, self.iterator, self.original_function) @property - def _cmp_tuple(self) -> Tuple[float, int]: + def _cmp_tuple(self) -> tuple[float, int]: return (-self.priority, self.id_number) def __eq__(self, other): @@ -194,7 +195,7 @@ class FakeEventLoop: """Emulate an asyncio EventLoop to run some registered coroutine jobs in sequence.""" def __init__(self): - self._pending_tasks: List[_Task] = [] + self._pending_tasks: list[_Task] = [] self._task_counter = 0 def add_job(self, func, *args, **kwargs): diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 42828450e8..a0f60e1ff8 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -5,7 +5,13 @@ import re from esphome.yaml_util import ESPHomeDataBase # pylint: disable=unused-import, wrong-import-order -from typing import Any, Generator, List, Optional, Tuple, Type, Union, Sequence +from typing import ( + Any, + Callable, + Optional, + Union, +) +from collections.abc import Generator, Sequence from esphome.core import ( # noqa CORE, @@ -44,9 +50,9 @@ SafeExpType = Union[ int, float, TimePeriod, - Type[bool], - Type[int], - Type[float], + type[bool], + type[int], + type[float], Sequence[Any], ] @@ -140,7 +146,7 @@ class CallExpression(Expression): class StructInitializer(Expression): __slots__ = ("base", "args") - def __init__(self, base: Expression, *args: Tuple[str, Optional[SafeExpType]]): + def __init__(self, base: Expression, *args: tuple[str, Optional[SafeExpType]]): self.base = base # TODO: args is always a Tuple, is this check required? if not isinstance(args, OrderedDict): @@ -200,7 +206,7 @@ class ParameterListExpression(Expression): __slots__ = ("parameters",) def __init__( - self, *parameters: Union[ParameterExpression, Tuple[SafeExpType, str]] + self, *parameters: Union[ParameterExpression, tuple[SafeExpType, str]] ): self.parameters = [] for parameter in parameters: @@ -468,7 +474,9 @@ def statement(expression: Union[Expression, Statement]) -> Statement: return ExpressionStatement(expression) -def variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": +def variable( + id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True +) -> "MockObj": """Declare a new variable, not pointer type, in the code generation. :param id_: The ID used to declare the variable. @@ -485,10 +493,37 @@ def variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": id_.type = type_ assignment = AssignmentExpression(id_.type, "", id_, rhs) CORE.add(assignment) - CORE.register_variable(id_, obj) + if register: + CORE.register_variable(id_, obj) return obj +def with_local_variable( + id_: ID, rhs: SafeExpType, callback: Callable[["MockObj"], None], *args +) -> None: + """Declare a new variable, not pointer type, in the code generation, within a scoped block + The variable is only usable within the callback + The callback cannot be async. + + :param id_: The ID used to declare the variable. + :param rhs: The expression to place on the right hand side of the assignment. + :param callback: The function to invoke that will receive the temporary variable + :param args: args to pass to the callback in addition to the temporary variable + + """ + + # throw if the callback is async: + assert not inspect.iscoroutinefunction( + callback + ), "with_local_variable() callback cannot be async!" + + CORE.add(RawStatement("{")) # output opening curly brace + obj = variable(id_, rhs, None, True) + # invoke user-provided callback to generate code with this local variable + callback(obj, *args) + CORE.add(RawStatement("}")) # output closing curly brace + + def new_variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": """Declare and define a new variable, not pointer type, in the code generation. @@ -590,7 +625,7 @@ def add_define(name: str, value: SafeExpType = None): CORE.add_define(Define(name, safe_exp(value))) -def add_platformio_option(key: str, value: Union[str, List[str]]): +def add_platformio_option(key: str, value: Union[str, list[str]]): CORE.add_platformio_option(key, value) @@ -607,7 +642,7 @@ async def get_variable(id_: ID) -> "MockObj": return await CORE.get_variable(id_) -async def get_variable_with_full_id(id_: ID) -> Tuple[ID, "MockObj"]: +async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: """ Wait for the given ID to be defined in the code generation and return it as a MockObj. @@ -622,7 +657,7 @@ async def get_variable_with_full_id(id_: ID) -> Tuple[ID, "MockObj"]: async def process_lambda( value: Lambda, - parameters: List[Tuple[SafeExpType, str]], + parameters: list[tuple[SafeExpType, str]], capture: str = "=", return_type: SafeExpType = None, ) -> Generator[LambdaExpression, None, None]: @@ -676,7 +711,7 @@ def is_template(value): async def templatable( value: Any, - args: List[Tuple[SafeExpType, str]], + args: list[tuple[SafeExpType, str]], output_type: Optional[SafeExpType], to_exp: Any = None, ): @@ -724,7 +759,7 @@ class MockObj(Expression): attr = attr[1:] return MockObj(f"{self.base}{self.op}{attr}", next_op) - def __call__(self, *args): # type: (SafeExpType) -> MockObj + def __call__(self, *args: SafeExpType) -> "MockObj": call = CallExpression(self.base, *args) return MockObj(call, self.op) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 9127f88e39..0ab4b75a16 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -13,7 +13,7 @@ from esphome.const import ( # pylint: disable=unused-import from esphome.core import coroutine, ID, CORE -from esphome.types import ConfigType +from esphome.types import ConfigType, ConfigFragmentType from esphome.cpp_generator import add, get_variable from esphome.cpp_types import App from esphome.util import Registry, RegistryEntry @@ -107,8 +107,10 @@ async def setup_entity(var, config): add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) -def extract_registry_entry_config(registry, full_config): - # type: (Registry, ConfigType) -> RegistryEntry +def extract_registry_entry_config( + registry: Registry, + full_config: ConfigType, +) -> tuple[RegistryEntry, ConfigFragmentType]: key, config = next((k, v) for k, v in full_config.items() if k in registry) return registry[key], config diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 07ee151949..1a51f3056f 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -533,7 +533,7 @@ class DashboardEntry: return os.path.basename(self.path) @property - def storage(self): # type: () -> Optional[StorageJSON] + def storage(self) -> Optional[StorageJSON]: if not self._loaded_storage: self._storage = StorageJSON.load( ext_storage_path(settings.config_dir, self.filename) @@ -829,7 +829,7 @@ class UndoDeleteRequestHandler(BaseHandler): shutil.move(os.path.join(trash_path, configuration), config_file) -PING_RESULT = {} # type: dict +PING_RESULT: dict = {} IMPORT_RESULT = {} STOP_EVENT = threading.Event() PING_REQUEST = threading.Event() @@ -945,7 +945,7 @@ def get_static_path(*args): return os.path.join(get_base_frontend_path(), "static", *args) -@functools.lru_cache(maxsize=None) +@functools.cache def get_static_file_url(name): base = f"./static/{name}" diff --git a/esphome/final_validate.py b/esphome/final_validate.py index 96dd2fd651..5e9d2207b0 100644 --- a/esphome/final_validate.py +++ b/esphome/final_validate.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Dict, Any +from typing import Any import contextvars from esphome.types import ConfigFragmentType, ID, ConfigPathType @@ -9,7 +9,7 @@ import esphome.config_validation as cv class FinalValidateConfig(ABC): @property @abstractmethod - def data(self) -> Dict[str, Any]: + def data(self) -> dict[str, Any]: """A dictionary that can be used by post validation functions to store global data during the validation phase. Each component should store its data under a unique key diff --git a/esphome/git.py b/esphome/git.py index 64c8d6a6b7..54fedc035f 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -2,6 +2,7 @@ from pathlib import Path import subprocess import hashlib import logging +from typing import Callable, Optional import urllib.parse from datetime import datetime @@ -12,7 +13,7 @@ import esphome.config_validation as cv _LOGGER = logging.getLogger(__name__) -def run_git_command(cmd, cwd=None): +def run_git_command(cmd, cwd=None) -> str: try: ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) except FileNotFoundError as err: @@ -28,6 +29,8 @@ def run_git_command(cmd, cwd=None): raise cv.Invalid(lines[-1][len("fatal: ") :]) raise cv.Invalid(err_str) + return ret.stdout.decode("utf-8").strip() + def _compute_destination_path(key: str, domain: str) -> Path: base_dir = Path(CORE.config_dir) / ".esphome" / domain @@ -44,7 +47,7 @@ def clone_or_update( domain: str, username: str = None, password: str = None, -) -> Path: +) -> tuple[Path, Optional[Callable[[], None]]]: key = f"{url}@{ref}" if username is not None and password is not None: @@ -78,6 +81,7 @@ def clone_or_update( file_timestamp = Path(repo_dir / ".git" / "HEAD") age = datetime.now() - datetime.fromtimestamp(file_timestamp.stat().st_mtime) if age.total_seconds() > refresh.total_seconds: + old_sha = run_git_command(["git", "rev-parse", "HEAD"], str(repo_dir)) _LOGGER.info("Updating %s", key) _LOGGER.debug("Location: %s", repo_dir) # Stash local changes (if any) @@ -92,4 +96,10 @@ def clone_or_update( # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch) run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) - return repo_dir + def revert(): + _LOGGER.info("Reverting changes to %s -> %s", key, old_sha) + run_git_command(["git", "reset", "--hard", old_sha], str(repo_dir)) + + return repo_dir, revert + + return repo_dir, None diff --git a/esphome/helpers.py b/esphome/helpers.py index e958aca78e..b5a6306342 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -6,6 +6,7 @@ import os from pathlib import Path from typing import Union import tempfile +from urllib.parse import urlparse _LOGGER = logging.getLogger(__name__) @@ -40,7 +41,7 @@ def indent(text, padding=" "): # From https://stackoverflow.com/a/14945195/8924614 def cpp_string_escape(string, encoding="utf-8"): - def _should_escape(byte): # type: (int) -> bool + def _should_escape(byte: int) -> bool: if not 32 <= byte < 127: return True if byte in (ord("\\"), ord('"')): @@ -134,7 +135,8 @@ def resolve_ip_address(host): errs.append(str(err)) try: - return socket.gethostbyname(host) + host_url = host if (urlparse(host).scheme != "") else "http://" + host + return socket.gethostbyname(urlparse(host_url).hostname) except OSError as err: errs.append(str(err)) raise EsphomeError(f"Error resolving IP address: {', '.join(errs)}") from err diff --git a/esphome/loader.py b/esphome/loader.py index 05d2e5a213..a0676eb90e 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -1,5 +1,5 @@ import logging -from typing import Callable, List, Optional, Any, ContextManager +from typing import Callable, Optional, Any, ContextManager from types import ModuleType import importlib import importlib.util @@ -62,19 +62,19 @@ class ComponentManifest: return getattr(self.module, "to_code", None) @property - def dependencies(self) -> List[str]: + def dependencies(self) -> list[str]: return getattr(self.module, "DEPENDENCIES", []) @property - def conflicts_with(self) -> List[str]: + def conflicts_with(self) -> list[str]: return getattr(self.module, "CONFLICTS_WITH", []) @property - def auto_load(self) -> List[str]: + def auto_load(self) -> list[str]: return getattr(self.module, "AUTO_LOAD", []) @property - def codeowners(self) -> List[str]: + def codeowners(self) -> list[str]: return getattr(self.module, "CODEOWNERS", []) @property @@ -87,7 +87,7 @@ class ComponentManifest: return getattr(self.module, "FINAL_VALIDATE_SCHEMA", None) @property - def resources(self) -> List[FileResource]: + def resources(self) -> list[FileResource]: """Return a list of all file resources defined in the package of this component. This will return all cpp source files that are located in the same folder as the @@ -106,7 +106,7 @@ class ComponentManifest: class ComponentMetaFinder(importlib.abc.MetaPathFinder): def __init__( - self, components_path: Path, allowed_components: Optional[List[str]] = None + self, components_path: Path, allowed_components: Optional[list[str]] = None ) -> None: self._allowed_components = allowed_components self._finders = [] @@ -117,7 +117,7 @@ class ComponentMetaFinder(importlib.abc.MetaPathFinder): continue self._finders.append(finder) - def find_spec(self, fullname: str, path: Optional[List[str]], target=None): + def find_spec(self, fullname: str, path: Optional[list[str]], target=None): if not fullname.startswith("esphome.components."): return None parts = fullname.split(".") @@ -144,7 +144,7 @@ def clear_component_meta_finders(): def install_meta_finder( - components_path: Path, allowed_components: Optional[List[str]] = None + components_path: Path, allowed_components: Optional[list[str]] = None ): sys.meta_path.insert(0, ComponentMetaFinder(components_path, allowed_components)) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index c4bf3d3f1a..a4a3f32092 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -1,6 +1,6 @@ from dataclasses import dataclass import json -from typing import List, Union +from typing import Union from pathlib import Path import logging @@ -310,7 +310,7 @@ class IDEData: return str(Path(self.firmware_elf_path).with_suffix(".bin")) @property - def extra_flash_images(self) -> List[FlashImage]: + def extra_flash_images(self) -> list[FlashImage]: return [ FlashImage(path=entry["path"], offset=entry["offset"]) for entry in self.raw["extra"]["flash_images"] diff --git a/esphome/storage_json.py b/esphome/storage_json.py index a941fca0af..af71d4583c 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -4,7 +4,7 @@ from datetime import datetime import json import logging import os -from typing import Any, Optional, List +from typing import Optional from esphome import const from esphome.core import CORE @@ -15,19 +15,19 @@ from esphome.types import CoreType _LOGGER = logging.getLogger(__name__) -def storage_path(): # type: () -> str +def storage_path() -> str: return CORE.relative_internal_path(f"{CORE.config_filename}.json") -def ext_storage_path(base_path, config_filename): # type: (str, str) -> str +def ext_storage_path(base_path: str, config_filename: str) -> str: return os.path.join(base_path, ".esphome", f"{config_filename}.json") -def esphome_storage_path(base_path): # type: (str) -> str +def esphome_storage_path(base_path: str) -> str: return os.path.join(base_path, ".esphome", "esphome.json") -def trash_storage_path(base_path): # type: (str) -> str +def trash_storage_path(base_path: str) -> str: return os.path.join(base_path, ".esphome", "trash") @@ -49,29 +49,29 @@ class StorageJSON: ): # Version of the storage JSON schema assert storage_version is None or isinstance(storage_version, int) - self.storage_version = storage_version # type: int + self.storage_version: int = storage_version # The name of the node - self.name = name # type: str + self.name: str = name # The comment of the node - self.comment = comment # type: str + self.comment: str = comment # The esphome version this was compiled with - self.esphome_version = esphome_version # type: str + self.esphome_version: str = esphome_version # The version of the file in src/main.cpp - Used to migrate the file assert src_version is None or isinstance(src_version, int) - self.src_version = src_version # type: int + self.src_version: int = src_version # Address of the ESP, for example livingroom.local or a static IP - self.address = address # type: str + self.address: str = address # Web server port of the ESP, for example 80 assert web_port is None or isinstance(web_port, int) - self.web_port = web_port # type: int + self.web_port: int = web_port # The type of hardware in use, like "ESP32", "ESP32C3", "ESP8266", etc. - self.target_platform = target_platform # type: str + self.target_platform: str = target_platform # The absolute path to the platformio project - self.build_path = build_path # type: str + self.build_path: str = build_path # The absolute path to the firmware binary - self.firmware_bin_path = firmware_bin_path # type: str + self.firmware_bin_path: str = firmware_bin_path # A list of strings of names of loaded integrations - self.loaded_integrations = loaded_integrations # type: List[str] + self.loaded_integrations: list[str] = loaded_integrations self.loaded_integrations.sort() def as_dict(self): @@ -97,8 +97,8 @@ class StorageJSON: @staticmethod def from_esphome_core( - esph, old - ): # type: (CoreType, Optional[StorageJSON]) -> StorageJSON + esph: CoreType, old: Optional["StorageJSON"] + ) -> "StorageJSON": hardware = esph.target_platform.upper() if esph.is_esp32: from esphome.components import esp32 @@ -135,7 +135,7 @@ class StorageJSON: ) @staticmethod - def _load_impl(path): # type: (str) -> Optional[StorageJSON] + def _load_impl(path: str) -> Optional["StorageJSON"]: with codecs.open(path, "r", encoding="utf-8") as f_handle: storage = json.load(f_handle) storage_version = storage["storage_version"] @@ -166,13 +166,13 @@ class StorageJSON: ) @staticmethod - def load(path): # type: (str) -> Optional[StorageJSON] + def load(path: str) -> Optional["StorageJSON"]: try: return StorageJSON._load_impl(path) except Exception: # pylint: disable=broad-except return None - def __eq__(self, o): # type: (Any) -> bool + def __eq__(self, o) -> bool: return isinstance(o, StorageJSON) and self.as_dict() == o.as_dict() @@ -182,15 +182,15 @@ class EsphomeStorageJSON: ): # Version of the storage JSON schema assert storage_version is None or isinstance(storage_version, int) - self.storage_version = storage_version # type: int + self.storage_version: int = storage_version # The cookie secret for the dashboard - self.cookie_secret = cookie_secret # type: str + self.cookie_secret: str = cookie_secret # The last time ESPHome checked for an update as an isoformat encoded str - self.last_update_check_str = last_update_check # type: str + self.last_update_check_str: str = last_update_check # Cache of the version gotten in the last version check - self.remote_version = remote_version # type: Optional[str] + self.remote_version: Optional[str] = remote_version - def as_dict(self): # type: () -> dict + def as_dict(self) -> dict: return { "storage_version": self.storage_version, "cookie_secret": self.cookie_secret, @@ -199,24 +199,24 @@ class EsphomeStorageJSON: } @property - def last_update_check(self): # type: () -> Optional[datetime] + def last_update_check(self) -> Optional[datetime]: try: return datetime.strptime(self.last_update_check_str, "%Y-%m-%dT%H:%M:%S") except Exception: # pylint: disable=broad-except return None @last_update_check.setter - def last_update_check(self, new): # type: (datetime) -> None + def last_update_check(self, new: datetime) -> None: self.last_update_check_str = new.strftime("%Y-%m-%dT%H:%M:%S") - def to_json(self): # type: () -> dict + def to_json(self) -> dict: return f"{json.dumps(self.as_dict(), indent=2)}\n" - def save(self, path): # type: (str) -> None + def save(self, path: str) -> None: write_file_if_changed(path, self.to_json()) @staticmethod - def _load_impl(path): # type: (str) -> Optional[EsphomeStorageJSON] + def _load_impl(path: str) -> Optional["EsphomeStorageJSON"]: with codecs.open(path, "r", encoding="utf-8") as f_handle: storage = json.load(f_handle) storage_version = storage["storage_version"] @@ -228,14 +228,14 @@ class EsphomeStorageJSON: ) @staticmethod - def load(path): # type: (str) -> Optional[EsphomeStorageJSON] + def load(path: str) -> Optional["EsphomeStorageJSON"]: try: return EsphomeStorageJSON._load_impl(path) except Exception: # pylint: disable=broad-except return None @staticmethod - def get_default(): # type: () -> EsphomeStorageJSON + def get_default() -> "EsphomeStorageJSON": return EsphomeStorageJSON( storage_version=1, cookie_secret=binascii.hexlify(os.urandom(64)).decode(), @@ -243,5 +243,5 @@ class EsphomeStorageJSON: remote_version=None, ) - def __eq__(self, o): # type: (Any) -> bool + def __eq__(self, o) -> bool: return isinstance(o, EsphomeStorageJSON) and self.as_dict() == o.as_dict() diff --git a/esphome/types.py b/esphome/types.py index 6bbfb00ce6..adb16fa91b 100644 --- a/esphome/types.py +++ b/esphome/types.py @@ -1,5 +1,5 @@ """This helper module tracks commonly used types in the esphome python codebase.""" -from typing import Dict, Union, List +from typing import Union from esphome.core import ID, Lambda, EsphomeCore @@ -8,11 +8,11 @@ ConfigFragmentType = Union[ int, float, None, - Dict[Union[str, int], "ConfigFragmentType"], - List["ConfigFragmentType"], + dict[Union[str, int], "ConfigFragmentType"], + list["ConfigFragmentType"], ID, Lambda, ] -ConfigType = Dict[str, ConfigFragmentType] +ConfigType = dict[str, ConfigFragmentType] CoreType = EsphomeCore ConfigPathType = Union[str, int] diff --git a/esphome/util.py b/esphome/util.py index 927c50fe89..0d60212f50 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -1,5 +1,4 @@ -import typing -from typing import Union, List +from typing import Union import collections import io @@ -35,7 +34,7 @@ class RegistryEntry: return Schema(self.raw_schema) -class Registry(dict): +class Registry(dict[str, RegistryEntry]): def __init__(self, base_schema=None, type_id_key=None): super().__init__() self.base_schema = base_schema or {} @@ -242,7 +241,7 @@ def is_dev_esphome_version(): return "dev" in const.__version__ -def parse_esphome_version() -> typing.Tuple[int, int, int]: +def parse_esphome_version() -> tuple[int, int, int]: match = re.match(r"^(\d+).(\d+).(\d+)(-dev\d*|b\d*)?$", const.__version__) if match is None: raise ValueError(f"Failed to parse ESPHome version '{const.__version__}'") @@ -282,7 +281,7 @@ class SerialPort: # from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py -def get_serial_ports() -> List[SerialPort]: +def get_serial_ports() -> list[SerialPort]: from serial.tools.list_ports import comports result = [] diff --git a/esphome/vscode.py b/esphome/vscode.py index 6a43a654ed..32a6b524f6 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -10,15 +10,13 @@ import esphome.config_validation as cv from typing import Optional -def _get_invalid_range(res, invalid): - # type: (Config, cv.Invalid) -> Optional[DocumentRange] +def _get_invalid_range(res: Config, invalid: cv.Invalid) -> Optional[DocumentRange]: return res.get_deepest_document_range_for_path( invalid.path, invalid.error_message == "extra keys not allowed" ) -def _dump_range(range): - # type: (Optional[DocumentRange]) -> Optional[dict] +def _dump_range(range: Optional[DocumentRange]) -> Optional[dict]: if range is None: return None return { diff --git a/esphome/writer.py b/esphome/writer.py index 31b47e243e..7a3c13e80b 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -2,7 +2,7 @@ import logging import os import re from pathlib import Path -from typing import Dict, List, Union +from typing import Union from esphome.config import iter_components from esphome.const import ( @@ -98,7 +98,7 @@ def replace_file_content(text, pattern, repl): return content_new, count -def storage_should_clean(old, new): # type: (StorageJSON, StorageJSON) -> bool +def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool: if old is None: return True @@ -123,7 +123,7 @@ def update_storage_json(): new.save(path) -def format_ini(data: Dict[str, Union[str, List[str]]]) -> str: +def format_ini(data: dict[str, Union[str, list[str]]]) -> str: content = "" for key, value in sorted(data.items()): if isinstance(value, list): @@ -226,7 +226,7 @@ the custom_components folder or the external_components feature. def copy_src_tree(): - source_files: List[loader.FileResource] = [] + source_files: list[loader.FileResource] = [] for _, component, _ in iter_components(CORE.config): source_files += component.resources source_files_map = { diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 089ae4f1c2..3743f650f3 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -1,7 +1,7 @@ import socket import threading import time -from typing import Dict, Optional +from typing import Optional import logging from dataclasses import dataclass @@ -71,12 +71,12 @@ class DashboardStatus(threading.Thread): threading.Thread.__init__(self) self.zc = zc self.query_hosts: set[str] = set() - self.key_to_host: Dict[str, str] = {} + self.key_to_host: dict[str, str] = {} self.stop_event = threading.Event() self.query_event = threading.Event() self.on_update = on_update - def request_query(self, hosts: Dict[str, str]) -> None: + def request_query(self, hosts: dict[str, str]) -> None: self.query_hosts = set(hosts.values()) self.key_to_host = hosts self.query_event.set() diff --git a/pyproject.toml b/pyproject.toml index 7a75060c8e..a49abb7b3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [tool.black] -target-version = ["py36", "py37", "py38"] +target-version = ["py39", "py310"] exclude = 'generated' diff --git a/requirements_test.txt b/requirements_test.txt index 5ef791906d..d72449fd66 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ -pylint==2.15.2 +pylint==2.15.3 flake8==5.0.4 black==22.8.0 # also change in .pre-commit-config.yaml when updating -pyupgrade==2.37.3 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.0.0 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests diff --git a/script/lint-python b/script/lint-python index 90b5dcd59f..7de1de80b0 100755 --- a/script/lint-python +++ b/script/lint-python @@ -109,7 +109,7 @@ def main(): print_error(file_, linno, msg) errors += 1 - PYUPGRADE_TARGET = "--py38-plus" + PYUPGRADE_TARGET = "--py39-plus" cmd = ["pyupgrade", PYUPGRADE_TARGET] + files print() print("Running pyupgrade...") diff --git a/setup.py b/setup.py index 941c8089ec..95453960ff 100755 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ setup( zip_safe=False, platforms="any", test_suite="tests", - python_requires=">=3.8,<4.0", + python_requires=">=3.9.0", install_requires=REQUIRES, keywords=["home", "automation"], entry_points={"console_scripts": ["esphome = esphome.__main__:main"]}, diff --git a/tests/test1.yaml b/tests/test1.yaml index e213a8b041..82cdac4623 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -290,8 +290,6 @@ adalight: esp32_ble_tracker: -bluetooth_proxy: - ble_client: - mac_address: AA:BB:CC:DD:EE:FF id: ble_foo @@ -321,6 +319,7 @@ mcp23s17: sensor: - platform: ble_client + type: characteristic ble_client_id: ble_foo name: Green iTag btn service_uuid: ffe0 @@ -335,6 +334,11 @@ sensor: then: - lambda: |- ESP_LOGD("green_btn", "Button was pressed, val%f", x); + - platform: ble_client + type: rssi + ble_client_id: ble_foo + name: Green iTag RSSI + update_interval: 15s - platform: adc pin: A0 name: Living Room Brightness diff --git a/tests/test2.yaml b/tests/test2.yaml index 5507fa0631..d485d3f16c 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -506,6 +506,9 @@ xiaomi_ble: mopeka_ble: +bluetooth_proxy: + active: true + xiaomi_rtcgq02lm: - id: motion_rtcgq02lm mac_address: 01:02:03:04:05:06 diff --git a/tests/test3.yaml b/tests/test3.yaml index 4eee0fd2c9..8fc66f4918 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1061,8 +1061,13 @@ climate: - platform: thermostat name: Thermostat Climate sensor: ha_hello_world - default_target_temperature_low: 18°C - default_target_temperature_high: 24°C + preset: + - name: Default Preset + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + - name: Away + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C idle_action: - switch.turn_on: gpio_switch1 cool_action: @@ -1137,9 +1142,6 @@ climate: fan_only_cooling: true fan_with_cooling: true fan_with_heating: true - away_config: - default_target_temperature_low: 16°C - default_target_temperature_high: 20°C - platform: pid id: pid_climate name: PID Climate Controller diff --git a/tests/test4.yaml b/tests/test4.yaml index 6293e0f7b7..cf517bb391 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -348,15 +348,16 @@ binary_sensor: on_state: then: - lambda: 'ESP_LOGI("ar1:", "%d", x);' - - platform: xpt2046 - xpt2046_id: xpt_touchscreen + - platform: touchscreen + touchscreen_id: xpt_touchscreen id: touch_key0 x_min: 80 x_max: 160 y_min: 106 y_max: 212 - on_state: - - lambda: 'ESP_LOGI("main", "key0: %s", (x ? "touch" : "release"));' + on_press: + - logger.log: Touched + - platform: gpio name: GPIO SX1509 test pin: @@ -598,33 +599,6 @@ external_components: components: [bh1750] - source: ../esphome/components components: [sntp] -xpt2046: - id: xpt_touchscreen - cs_pin: 17 - irq_pin: 16 - update_interval: 50ms - report_interval: 1s - threshold: 400 - dimension_x: 240 - dimension_y: 320 - calibration_x_min: 3860 - calibration_x_max: 280 - calibration_y_min: 340 - calibration_y_max: 3860 - swap_x_y: false - on_state: - # yamllint disable rule:line-length - - lambda: |- - ESP_LOGI("main", "args x=%d, y=%d, touched=%s", x, y, (touched ? "touch" : "release")); - ESP_LOGI("main", "member x=%d, y=%d, touched=%d, x_raw=%d, y_raw=%d, z_raw=%d", - id(xpt_touchscreen).x, - id(xpt_touchscreen).y, - (int) id(xpt_touchscreen).touched, - id(xpt_touchscreen).x_raw, - id(xpt_touchscreen).y_raw, - id(xpt_touchscreen).z_raw - ); - # yamllint enable rule:line-length button: - platform: restart @@ -648,6 +622,25 @@ touchscreen: format: Touch at (%d, %d) args: [touch.x, touch.y] + - platform: xpt2046 + id: xpt_touchscreen + cs_pin: 17 + interrupt_pin: 16 + display: inkplate_display + update_interval: 50ms + report_interval: 1s + threshold: 400 + calibration_x_min: 3860 + calibration_x_max: 280 + calibration_y_min: 340 + calibration_y_max: 3860 + swap_x_y: false + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] + + - platform: lilygo_t5_47 id: lilygo_touchscreen interrupt_pin: GPIO36 diff --git a/tests/unit_tests/strategies.py b/tests/unit_tests/strategies.py index 30768f9d56..20a3d190da 100644 --- a/tests/unit_tests/strategies.py +++ b/tests/unit_tests/strategies.py @@ -1,12 +1,9 @@ -from typing import Text - import hypothesis.strategies._internal.core as st from hypothesis.strategies._internal.strategies import SearchStrategy @st.defines_strategy(force_reusable_values=True) -def mac_addr_strings(): - # type: () -> SearchStrategy[Text] +def mac_addr_strings() -> SearchStrategy[str]: """A strategy for MAC address strings. This consists of six strings representing integers [0..255],