mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Merge branch 'beta' into bump-2022.10.0
This commit is contained in:
		
							
								
								
									
										1
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -28,6 +28,7 @@ jobs: | |||||||
|     name: Build docker containers |     name: Build docker containers | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     strategy: |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|         arch: [amd64, armv7, aarch64] |         arch: [amd64, armv7, aarch64] | ||||||
|         build_type: ["ha-addon", "docker", "lint"] |         build_type: ["ha-addon", "docker", "lint"] | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -85,7 +85,7 @@ jobs: | |||||||
|         uses: actions/setup-python@v4 |         uses: actions/setup-python@v4 | ||||||
|         id: python |         id: python | ||||||
|         with: |         with: | ||||||
|           python-version: "3.8" |           python-version: "3.9" | ||||||
|  |  | ||||||
|       - name: Cache virtualenv |       - name: Cache virtualenv | ||||||
|         uses: actions/cache@v3 |         uses: actions/cache@v3 | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ repos: | |||||||
|           - --branch=release |           - --branch=release | ||||||
|           - --branch=beta |           - --branch=beta | ||||||
|   - repo: https://github.com/asottile/pyupgrade |   - repo: https://github.com/asottile/pyupgrade | ||||||
|     rev: v2.37.3 |     rev: v3.0.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: pyupgrade |       - id: pyupgrade | ||||||
|         args: [--py38-plus] |         args: [--py39-plus] | ||||||
|   | |||||||
| @@ -258,4 +258,4 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl | |||||||
| esphome/components/xiaomi_mhoc303/* @drug123 | esphome/components/xiaomi_mhoc303/* @drug123 | ||||||
| esphome/components/xiaomi_mhoc401/* @vevsvevs | esphome/components/xiaomi_mhoc401/* @vevsvevs | ||||||
| esphome/components/xiaomi_rtcgq02lm/* @jesserockz | esphome/components/xiaomi_rtcgq02lm/* @jesserockz | ||||||
| esphome/components/xpt2046/* @numo68 | esphome/components/xpt2046/* @nielsnl68 @numo68 | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ from esphome.cpp_generator import (  # noqa | |||||||
|     static_const_array, |     static_const_array, | ||||||
|     statement, |     statement, | ||||||
|     variable, |     variable, | ||||||
|  |     with_local_variable, | ||||||
|     new_variable, |     new_variable, | ||||||
|     Pvariable, |     Pvariable, | ||||||
|     new_Pvariable, |     new_Pvariable, | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) | |||||||
| DEPENDENCIES = ["display"] | DEPENDENCIES = ["display"] | ||||||
| MULTI_CONF = True | MULTI_CONF = True | ||||||
|  |  | ||||||
| Animation_ = display.display_ns.class_("Animation") | Animation_ = display.display_ns.class_("Animation", espImage.Image_) | ||||||
|  |  | ||||||
| ANIMATION_SCHEMA = cv.Schema( | ANIMATION_SCHEMA = cv.Schema( | ||||||
|     { |     { | ||||||
|   | |||||||
| @@ -1298,3 +1298,31 @@ message BluetoothConnectionsFreeResponse { | |||||||
|   uint32 free = 1; |   uint32 free = 1; | ||||||
|   uint32 limit = 2; |   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; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -5746,6 +5746,118 @@ void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { | |||||||
|   out.append("}"); |   out.append("}"); | ||||||
| } | } | ||||||
| #endif | #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 api | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -1481,6 +1481,43 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage { | |||||||
|  protected: |  protected: | ||||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; |   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 api | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -401,6 +401,30 @@ bool APIServerConnectionBase::send_bluetooth_connections_free_response(const Blu | |||||||
|   return this->send_message_<BluetoothConnectionsFreeResponse>(msg, 81); |   return this->send_message_<BluetoothConnectionsFreeResponse>(msg, 81); | ||||||
| } | } | ||||||
| #endif | #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_<BluetoothGATTErrorResponse>(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_<BluetoothGATTWriteResponse>(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_<BluetoothGATTNotifyResponse>(msg, 84); | ||||||
|  | } | ||||||
|  | #endif | ||||||
| bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { | bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { | ||||||
|   switch (msg_type) { |   switch (msg_type) { | ||||||
|     case 1: { |     case 1: { | ||||||
|   | |||||||
| @@ -200,6 +200,15 @@ class APIServerConnectionBase : public ProtoService { | |||||||
| #endif | #endif | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|   bool send_bluetooth_connections_free_response(const BluetoothConnectionsFreeResponse &msg); |   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 | #endif | ||||||
|  protected: |  protected: | ||||||
|   bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; |   bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; | ||||||
|   | |||||||
| @@ -324,11 +324,21 @@ void APIServer::send_bluetooth_gatt_read_response(const BluetoothGATTReadRespons | |||||||
|     client->send_bluetooth_gatt_read_response(call); |     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) { | void APIServer::send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call) { | ||||||
|   for (auto &client : this->clients_) { |   for (auto &client : this->clients_) { | ||||||
|     client->send_bluetooth_gatt_notify_data_response(call); |     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) { | void APIServer::send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call) { | ||||||
|   for (auto &client : this->clients_) { |   for (auto &client : this->clients_) { | ||||||
|     client->send_bluetooth_gatt_get_services_response(call); |     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); |     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 | #endif | ||||||
| APIServer::APIServer() { global_api_server = this; } | APIServer::APIServer() { global_api_server = this; } | ||||||
| void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, | void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, | ||||||
|   | |||||||
| @@ -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_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_connections_free(uint8_t free, uint8_t limit); | ||||||
|   void send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call); |   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_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(const BluetoothGATTGetServicesResponse &call); | ||||||
|   void send_bluetooth_gatt_services_done(uint64_t address); |   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 | #endif | ||||||
|   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } |   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } | ||||||
| #ifdef USE_HOMEASSISTANT_TIME | #ifdef USE_HOMEASSISTANT_TIME | ||||||
|   | |||||||
| @@ -100,12 +100,40 @@ async def ble_write_to_code(config, action_id, template_arg, args): | |||||||
|     else: |     else: | ||||||
|         cg.add(var.set_value_simple(value)) |         cg.add(var.set_value_simple(value)) | ||||||
|  |  | ||||||
|     serv_uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) |     if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): | ||||||
|     cg.add(var.set_service_uuid128(serv_uuid128)) |         cg.add( | ||||||
|     char_uuid128 = esp32_ble_tracker.as_reversed_hex_array( |             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] |             config[CONF_CHARACTERISTIC_UUID] | ||||||
|         ) |         ) | ||||||
|     cg.add(var.set_char_uuid128(char_uuid128)) |         cg.add(var.set_char_uuid128(uuid128)) | ||||||
|  |  | ||||||
|     return var |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -46,10 +46,14 @@ class BLEWriterClientNode : public BLEClientNode { | |||||||
|   // Attempts to write the contents of value to char_uuid_. |   // Attempts to write the contents of value to char_uuid_. | ||||||
|   void write(const std::vector<uint8_t> &value); |   void write(const std::vector<uint8_t> &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_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, |   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||||
|                            esp_ble_gattc_cb_param_t *param) override; |                            esp_ble_gattc_cb_param_t *param) override; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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) { | void BLEClient::set_state(espbt::ClientState state) { | ||||||
|   BLEClientBase::set_state(state); |   BLEClientBase::set_state(state); | ||||||
|   for (auto &node : nodes_) |   for (auto &node : nodes_) | ||||||
|   | |||||||
| @@ -27,7 +27,8 @@ class BLEClientNode { | |||||||
|  public: |  public: | ||||||
|   virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, |   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; |                                    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; } |   void set_address(uint64_t address) { address_ = address; } | ||||||
|   espbt::ESPBTClient *client; |   espbt::ESPBTClient *client; | ||||||
|   // This should be transitioned to Established once the node no longer needs |   // 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, |   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||||
|                            esp_ble_gattc_cb_param_t *param) override; |                            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; |   bool parse_device(const espbt::ESPBTDevice &device) override; | ||||||
|  |  | ||||||
|   void set_enabled(bool enabled); |   void set_enabled(bool enabled); | ||||||
|   | |||||||
| @@ -5,7 +5,11 @@ from esphome.const import ( | |||||||
|     CONF_CHARACTERISTIC_UUID, |     CONF_CHARACTERISTIC_UUID, | ||||||
|     CONF_LAMBDA, |     CONF_LAMBDA, | ||||||
|     CONF_TRIGGER_ID, |     CONF_TRIGGER_ID, | ||||||
|  |     CONF_TYPE, | ||||||
|     CONF_SERVICE_UUID, |     CONF_SERVICE_UUID, | ||||||
|  |     DEVICE_CLASS_SIGNAL_STRENGTH, | ||||||
|  |     STATE_CLASS_MEASUREMENT, | ||||||
|  |     UNIT_DECIBEL_MILLIWATT, | ||||||
| ) | ) | ||||||
| from esphome import automation | from esphome import automation | ||||||
| from .. import ble_client_ns | from .. import ble_client_ns | ||||||
| @@ -16,6 +20,8 @@ CONF_DESCRIPTOR_UUID = "descriptor_uuid" | |||||||
|  |  | ||||||
| CONF_NOTIFY = "notify" | CONF_NOTIFY = "notify" | ||||||
| CONF_ON_NOTIFY = "on_notify" | CONF_ON_NOTIFY = "on_notify" | ||||||
|  | TYPE_CHARACTERISTIC = "characteristic" | ||||||
|  | TYPE_RSSI = "rssi" | ||||||
|  |  | ||||||
| adv_data_t = cg.std_vector.template(cg.uint8) | adv_data_t = cg.std_vector.template(cg.uint8) | ||||||
| adv_data_t_const_ref = adv_data_t.operator("ref").operator("const") | adv_data_t_const_ref = adv_data_t.operator("ref").operator("const") | ||||||
| @@ -27,11 +33,29 @@ BLESensorNotifyTrigger = ble_client_ns.class_( | |||||||
|     "BLESensorNotifyTrigger", automation.Trigger.template(cg.float_) |     "BLESensorNotifyTrigger", automation.Trigger.template(cg.float_) | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | BLEClientRssiSensor = ble_client_ns.class_( | ||||||
|  |     "BLEClientRSSISensor", sensor.Sensor, cg.PollingComponent, ble_client.BLEClientNode | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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( | CONFIG_SCHEMA = cv.All( | ||||||
|     sensor.sensor_schema( |     checkType, | ||||||
|  |     cv.typed_schema( | ||||||
|  |         { | ||||||
|  |             TYPE_CHARACTERISTIC: sensor.sensor_schema( | ||||||
|                 BLESensor, |                 BLESensor, | ||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|             ) |             ) | ||||||
|  |             .extend(cv.polling_component_schema("60s")) | ||||||
|  |             .extend(ble_client.BLE_CLIENT_SCHEMA) | ||||||
|             .extend( |             .extend( | ||||||
|                 { |                 { | ||||||
|                     cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, |                     cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, | ||||||
| @@ -47,13 +71,29 @@ CONFIG_SCHEMA = cv.All( | |||||||
|                         } |                         } | ||||||
|                     ), |                     ), | ||||||
|                 } |                 } | ||||||
|  |             ), | ||||||
|  |             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(cv.polling_component_schema("60s")) | ||||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA) |             .extend(ble_client.BLE_CLIENT_SCHEMA), | ||||||
|  |         }, | ||||||
|  |         lower=True, | ||||||
|  |     ), | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | 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) |     var = await sensor.new_sensor(config) | ||||||
|     if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): |     if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): | ||||||
|         cg.add( |         cg.add( | ||||||
| @@ -125,3 +165,10 @@ async def to_code(config): | |||||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|         await ble_client.register_ble_node(trigger, config) |         await ble_client.register_ble_node(trigger, config) | ||||||
|         await automation.build_automation(trigger, [(float, "x")], conf) |         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) | ||||||
|   | |||||||
							
								
								
									
										78
									
								
								esphome/components/ble_client/sensor/ble_rssi_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								esphome/components/ble_client/sensor/ble_rssi_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
							
								
								
									
										31
									
								
								esphome/components/ble_client/sensor/ble_rssi_sensor.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								esphome/components/ble_client/sensor/ble_rssi_sensor.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <esp_gattc_api.h> | ||||||
|  |  | ||||||
|  | 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 | ||||||
| @@ -58,7 +58,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, | |||||||
|       case MATCH_BY_SERVICE_UUID: |       case MATCH_BY_SERVICE_UUID: | ||||||
|         for (auto uuid : device.get_service_uuids()) { |         for (auto uuid : device.get_service_uuids()) { | ||||||
|           if (this->uuid_ == uuid) { |           if (this->uuid_ == uuid) { | ||||||
|             this->publish_state(device.get_rssi()); |             this->publish_state(true); | ||||||
|             this->found_ = true; |             this->found_ = true; | ||||||
|             return true; |             return true; | ||||||
|           } |           } | ||||||
| @@ -83,7 +83,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, | |||||||
|           return false; |           return false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         this->publish_state(device.get_rssi()); |         this->publish_state(true); | ||||||
|         this->found_ = true; |         this->found_ = true; | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ from esphome.const import ( | |||||||
|     CONF_MAC_ADDRESS, |     CONF_MAC_ADDRESS, | ||||||
|     DEVICE_CLASS_SIGNAL_STRENGTH, |     DEVICE_CLASS_SIGNAL_STRENGTH, | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     UNIT_DECIBEL, |     UNIT_DECIBEL_MILLIWATT, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| DEPENDENCIES = ["esp32_ble_tracker"] | DEPENDENCIES = ["esp32_ble_tracker"] | ||||||
| @@ -31,7 +31,7 @@ def _validate(config): | |||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = cv.All( | ||||||
|     sensor.sensor_schema( |     sensor.sensor_schema( | ||||||
|         BLERSSISensor, |         BLERSSISensor, | ||||||
|         unit_of_measurement=UNIT_DECIBEL, |         unit_of_measurement=UNIT_DECIBEL_MILLIWATT, | ||||||
|         accuracy_decimals=0, |         accuracy_decimals=0, | ||||||
|         device_class=DEVICE_CLASS_SIGNAL_STRENGTH, |         device_class=DEVICE_CLASS_SIGNAL_STRENGTH, | ||||||
|         state_class=STATE_CLASS_MEASUREMENT, |         state_class=STATE_CLASS_MEASUREMENT, | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import esphome.codegen as cg | |||||||
| from esphome.const import CONF_ACTIVE, CONF_ID | from esphome.const import CONF_ACTIVE, CONF_ID | ||||||
|  |  | ||||||
| AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"] | AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"] | ||||||
| DEPENDENCIES = ["esp32"] | DEPENDENCIES = ["api", "esp32"] | ||||||
| CODEOWNERS = ["@jesserockz"] | CODEOWNERS = ["@jesserockz"] | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,43 +4,36 @@ | |||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
| #ifdef USE_API |  | ||||||
| #include "esphome/components/api/api_server.h" | #include "esphome/components/api/api_server.h" | ||||||
| #endif |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace bluetooth_proxy { | namespace bluetooth_proxy { | ||||||
|  |  | ||||||
| static const char *const TAG = "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() { | BluetoothProxy::BluetoothProxy() { | ||||||
|   global_bluetooth_proxy = this; |   global_bluetooth_proxy = this; | ||||||
|   this->address_ = 0; |   this->address_ = 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { | 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(), |   ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(), | ||||||
|            device.get_rssi()); |            device.get_rssi()); | ||||||
|   this->send_api_packet_(device); |   this->send_api_packet_(device); | ||||||
|  |  | ||||||
|   this->address_type_map_[device.address_uint64()] = device.get_address_type(); |  | ||||||
|  |  | ||||||
|   if (this->address_ == 0) |   if (this->address_ == 0) | ||||||
|     return true; |     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); |   BLEClientBase::parse_device(device); | ||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  |  | ||||||
| void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { | void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { | ||||||
| #ifndef USE_API |  | ||||||
|   return; |  | ||||||
| #else |  | ||||||
|   api::BluetoothLEAdvertisementResponse resp; |   api::BluetoothLEAdvertisementResponse resp; | ||||||
|   resp.address = device.address_uint64(); |   resp.address = device.address_uint64(); | ||||||
|   if (!device.get_name().empty()) |   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)); |     resp.manufacturer_data.push_back(std::move(manufacturer_data)); | ||||||
|   } |   } | ||||||
|   api::global_api_server->send_bluetooth_le_advertisement(resp); |   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, | 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() { | void BluetoothProxy::loop() { | ||||||
|   BLEClientBase::loop(); |   BLEClientBase::loop(); | ||||||
| #ifdef USE_API |  | ||||||
|   if (this->state_ != espbt::ClientState::IDLE && !api::global_api_server->is_connected()) { |   if (this->state_ != espbt::ClientState::IDLE && !api::global_api_server->is_connected()) { | ||||||
|     ESP_LOGI(TAG, "[%s] Disconnecting.", this->address_str().c_str()); |     ESP_LOGI(TAG, "[%s] Disconnecting.", this->address_str().c_str()); | ||||||
|     auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_); |     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); |     api::global_api_server->send_bluetooth_gatt_services(resp); | ||||||
|     this->send_service_++; |     this->send_service_++; | ||||||
|   } |   } | ||||||
| #endif |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #ifdef USE_API |  | ||||||
| void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest &msg) { | void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest &msg) { | ||||||
|   switch (msg.request_type) { |   switch (msg.request_type) { | ||||||
|     case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: { |     case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: { | ||||||
|       this->address_ = msg.address; |       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; |       break; | ||||||
|     } |     } | ||||||
|     case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: { |     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) { | void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) { | ||||||
|   if (this->state_ != espbt::ClientState::ESTABLISHED) { |   if (this->state_ != espbt::ClientState::ESTABLISHED) { | ||||||
|     ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected."); |     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; |     return; | ||||||
|   } |   } | ||||||
|   if (this->address_ != msg.address) { |   if (this->address_ != msg.address) { | ||||||
|     ESP_LOGW(TAG, "Address mismatch for read GATT characteristic request"); |     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; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   auto *characteristic = this->get_characteristic(msg.handle); |   auto *characteristic = this->get_characteristic(msg.handle); | ||||||
|   if (characteristic == nullptr) { |   if (characteristic == nullptr) { | ||||||
|     ESP_LOGW(TAG, "Cannot read GATT characteristic, not found."); |     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; |     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); |       esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, characteristic->handle, ESP_GATT_AUTH_REQ_NONE); | ||||||
|   if (err != ERR_OK) { |   if (err != ERR_OK) { | ||||||
|     ESP_LOGW(TAG, "esp_ble_gattc_read_char error, err=%d", err); |     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) { | void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) { | ||||||
|   if (this->state_ != espbt::ClientState::ESTABLISHED) { |   if (this->state_ != espbt::ClientState::ESTABLISHED) { | ||||||
|     ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected."); |     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; |     return; | ||||||
|   } |   } | ||||||
|   if (this->address_ != msg.address) { |   if (this->address_ != msg.address) { | ||||||
|     ESP_LOGW(TAG, "Address mismatch for write GATT characteristic request"); |     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; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   auto *characteristic = this->get_characteristic(msg.handle); |   auto *characteristic = this->get_characteristic(msg.handle); | ||||||
|   if (characteristic == nullptr) { |   if (characteristic == nullptr) { | ||||||
|     ESP_LOGW(TAG, "Cannot write GATT characteristic, not found."); |     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; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   ESP_LOGV(TAG, "Writing GATT characteristic %s", characteristic->uuid.to_string().c_str()); |   ESP_LOGV(TAG, "Writing GATT characteristic %s", characteristic->uuid.to_string().c_str()); | ||||||
|   characteristic->write_value((uint8_t *) msg.data.data(), msg.data.size(), |   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); |                                          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) { | void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) { | ||||||
|   if (this->state_ != espbt::ClientState::ESTABLISHED) { |   if (this->state_ != espbt::ClientState::ESTABLISHED) { | ||||||
|     ESP_LOGW(TAG, "Cannot read GATT characteristic descriptor, not connected."); |     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; |     return; | ||||||
|   } |   } | ||||||
|   if (this->address_ != msg.address) { |   if (this->address_ != msg.address) { | ||||||
|     ESP_LOGW(TAG, "Address mismatch for read GATT characteristic descriptor request"); |     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; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   auto *descriptor = this->get_descriptor(msg.handle); |   auto *descriptor = this->get_descriptor(msg.handle); | ||||||
|   if (descriptor == nullptr) { |   if (descriptor == nullptr) { | ||||||
|     ESP_LOGW(TAG, "Cannot read GATT characteristic descriptor, not found."); |     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; |     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); |       esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, descriptor->handle, ESP_GATT_AUTH_REQ_NONE); | ||||||
|   if (err != ERR_OK) { |   if (err != ERR_OK) { | ||||||
|     ESP_LOGW(TAG, "esp_ble_gattc_read_char error, err=%d", err); |     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) { | void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) { | ||||||
|   if (this->state_ != espbt::ClientState::ESTABLISHED) { |   if (this->state_ != espbt::ClientState::ESTABLISHED) { | ||||||
|     ESP_LOGW(TAG, "Cannot write GATT characteristic descriptor, not connected."); |     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; |     return; | ||||||
|   } |   } | ||||||
|   if (this->address_ != msg.address) { |   if (this->address_ != msg.address) { | ||||||
|     ESP_LOGW(TAG, "Address mismatch for write GATT characteristic descriptor request"); |     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; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   auto *descriptor = this->get_descriptor(msg.handle); |   auto *descriptor = this->get_descriptor(msg.handle); | ||||||
|   if (descriptor == nullptr) { |   if (descriptor == nullptr) { | ||||||
|     ESP_LOGW(TAG, "Cannot write GATT characteristic descriptor, not found."); |     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; |     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); |                                      (uint8_t *) msg.data.data(), ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||||
|   if (err != ERR_OK) { |   if (err != ERR_OK) { | ||||||
|     ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, err=%d", err); |     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) { | 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) { |   if (this->address_ != msg.address) { | ||||||
|     ESP_LOGW(TAG, "Address mismatch for service list request"); |     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; |     return; | ||||||
|   } |   } | ||||||
|   this->send_service_ = 0; |   this->send_service_ = 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) { | 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) { |   if (this->address_ != msg.address) { | ||||||
|     ESP_LOGW(TAG, "Address mismatch for notify"); |     ESP_LOGW(TAG, "Address mismatch for notify"); | ||||||
|  |     api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_WRONG_ADDRESS); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -334,6 +447,7 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest | |||||||
|  |  | ||||||
|   if (characteristic == nullptr) { |   if (characteristic == nullptr) { | ||||||
|     ESP_LOGW(TAG, "Cannot notify GATT characteristic, not found."); |     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; |     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); |     err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, characteristic->handle); | ||||||
|     if (err != ESP_OK) { |     if (err != ESP_OK) { | ||||||
|       ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, err=%d", err); |       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 { |   } else { | ||||||
|     err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, characteristic->handle); |     err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, characteristic->handle); | ||||||
|     if (err != ESP_OK) { |     if (err != ESP_OK) { | ||||||
|       ESP_LOGW(TAG, "esp_ble_gattc_unregister_for_notify failed, err=%d", err); |       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) | BluetoothProxy *global_bluetooth_proxy = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|  |  | ||||||
| }  // namespace bluetooth_proxy | }  // namespace bluetooth_proxy | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
|  |  | ||||||
| #include <map> | #include <map> | ||||||
|  |  | ||||||
|  | #include "esphome/components/api/api_pb2.h" | ||||||
| #include "esphome/components/esp32_ble_client/ble_client_base.h" | #include "esphome/components/esp32_ble_client/ble_client_base.h" | ||||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||||
| #include "esphome/core/automation.h" | #include "esphome/core/automation.h" | ||||||
| @@ -12,10 +13,6 @@ | |||||||
|  |  | ||||||
| #include <map> | #include <map> | ||||||
|  |  | ||||||
| #ifdef USE_API |  | ||||||
| #include "esphome/components/api/api_pb2.h" |  | ||||||
| #endif  // USE_API |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace bluetooth_proxy { | 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, |   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||||
|                            esp_ble_gattc_cb_param_t *param) override; |                            esp_ble_gattc_cb_param_t *param) override; | ||||||
|  |  | ||||||
| #ifdef USE_API |  | ||||||
|   void bluetooth_device_request(const api::BluetoothDeviceRequest &msg); |   void bluetooth_device_request(const api::BluetoothDeviceRequest &msg); | ||||||
|   void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg); |   void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg); | ||||||
|   void bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &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_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg); | ||||||
|   void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg); |   void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg); | ||||||
|   void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &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_free() { return this->state_ == espbt::ClientState::IDLE ? 1 : 0; } | ||||||
|   int get_bluetooth_connections_limit() { return 1; } |   int get_bluetooth_connections_limit() { return 1; } | ||||||
| @@ -50,7 +45,6 @@ class BluetoothProxy : public BLEClientBase { | |||||||
|  protected: |  protected: | ||||||
|   void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); |   void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); | ||||||
|  |  | ||||||
|   std::map<uint64_t, esp_ble_addr_type_t> address_type_map_; |  | ||||||
|   int16_t send_service_{-1}; |   int16_t send_service_{-1}; | ||||||
|   bool active_; |   bool active_; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -64,17 +64,18 @@ BLEDescriptor *BLECharacteristic::get_descriptor_by_handle(uint16_t handle) { | |||||||
|   return nullptr; |   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 *client = this->service->client; | ||||||
|   auto status = esp_ble_gattc_write_char(client->get_gattc_if(), client->get_conn_id(), this->handle, new_val_size, |   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); |                                          new_val, write_type, ESP_GATT_AUTH_REQ_NONE); | ||||||
|   if (status) { |   if (status) { | ||||||
|     ESP_LOGW(TAG, "Error sending write value to BLE gattc server, status=%d", 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) { | esp_err_t 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); |   return write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP); | ||||||
| } | } | ||||||
|  |  | ||||||
| }  // namespace esp32_ble_client | }  // namespace esp32_ble_client | ||||||
|   | |||||||
| @@ -24,8 +24,8 @@ class BLECharacteristic { | |||||||
|   BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid); |   BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid); | ||||||
|   BLEDescriptor *get_descriptor(uint16_t uuid); |   BLEDescriptor *get_descriptor(uint16_t uuid); | ||||||
|   BLEDescriptor *get_descriptor_by_handle(uint16_t handle); |   BLEDescriptor *get_descriptor_by_handle(uint16_t handle); | ||||||
|   void 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); | ||||||
|   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_gatt_write_type_t write_type); | ||||||
|   BLEService *service; |   BLEService *service; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -648,11 +648,17 @@ void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_p | |||||||
|     // (called CSS here) |     // (called CSS here) | ||||||
|  |  | ||||||
|     switch (record_type) { |     switch (record_type) { | ||||||
|  |       case ESP_BLE_AD_TYPE_NAME_SHORT: | ||||||
|       case ESP_BLE_AD_TYPE_NAME_CMPL: { |       case ESP_BLE_AD_TYPE_NAME_CMPL: { | ||||||
|         // CSS 1.2 LOCAL NAME |         // 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 |         // "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. |         // device." CSS 1: Optional in this context; shall not appear more than once in a block. | ||||||
|  |         // 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<const char *>(record), record_length); |           this->name_ = std::string(reinterpret_cast<const char *>(record), record_length); | ||||||
|  |         } | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       case ESP_BLE_AD_TYPE_TX_PWR: { |       case ESP_BLE_AD_TYPE_TX_PWR: { | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| import logging | import logging | ||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| from typing import List |  | ||||||
|  |  | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
| @@ -200,7 +199,7 @@ async def esp8266_pin_to_code(config): | |||||||
| @coroutine_with_priority(-999.0) | @coroutine_with_priority(-999.0) | ||||||
| async def add_pin_initial_states_array(): | async def add_pin_initial_states_array(): | ||||||
|     # Add includes at the very end, so that they override everything |     # 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 |         KEY_PIN_INITIAL_STATES | ||||||
|     ] |     ] | ||||||
|     initial_modes_s = ", ".join(str(x.mode) for x in initial_states) |     initial_modes_s = ", ".join(str(x.mode) for x in initial_states) | ||||||
|   | |||||||
| @@ -98,7 +98,7 @@ async def to_code(config): | |||||||
|  |  | ||||||
|  |  | ||||||
| def _process_git_config(config: dict, refresh) -> str: | def _process_git_config(config: dict, refresh) -> str: | ||||||
|     repo_dir = git.clone_or_update( |     repo_dir, _ = git.clone_or_update( | ||||||
|         url=config[CONF_URL], |         url=config[CONF_URL], | ||||||
|         ref=config.get(CONF_REF), |         ref=config.get(CONF_REF), | ||||||
|         refresh=refresh, |         refresh=refresh, | ||||||
|   | |||||||
| @@ -58,6 +58,7 @@ PROTOCOLS = { | |||||||
|     "sharp": Protocol.PROTOCOL_SHARP, |     "sharp": Protocol.PROTOCOL_SHARP, | ||||||
|     "toshiba_daiseikai": Protocol.PROTOCOL_TOSHIBA_DAISEIKAI, |     "toshiba_daiseikai": Protocol.PROTOCOL_TOSHIBA_DAISEIKAI, | ||||||
|     "toshiba": Protocol.PROTOCOL_TOSHIBA, |     "toshiba": Protocol.PROTOCOL_TOSHIBA, | ||||||
|  |     "zhlt01": Protocol.PROTOCOL_ZHLT01, | ||||||
| } | } | ||||||
|  |  | ||||||
| CONF_HORIZONTAL_DEFAULT = "horizontal_default" | CONF_HORIZONTAL_DEFAULT = "horizontal_default" | ||||||
|   | |||||||
| @@ -53,6 +53,7 @@ const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP | |||||||
|     {PROTOCOL_SHARP, []() { return new SharpHeatpumpIR(); }},                                // NOLINT |     {PROTOCOL_SHARP, []() { return new SharpHeatpumpIR(); }},                                // NOLINT | ||||||
|     {PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }},         // NOLINT |     {PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }},         // NOLINT | ||||||
|     {PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }},                            // NOLINT |     {PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }},                            // NOLINT | ||||||
|  |     {PROTOCOL_ZHLT01, []() { return new ZHLT01HeatpumpIR(); }},                              // NOLINT | ||||||
| }; | }; | ||||||
|  |  | ||||||
| void HeatpumpIRClimate::setup() { | void HeatpumpIRClimate::setup() { | ||||||
|   | |||||||
| @@ -53,6 +53,7 @@ enum Protocol { | |||||||
|   PROTOCOL_SHARP, |   PROTOCOL_SHARP, | ||||||
|   PROTOCOL_TOSHIBA_DAISEIKAI, |   PROTOCOL_TOSHIBA_DAISEIKAI, | ||||||
|   PROTOCOL_TOSHIBA, |   PROTOCOL_TOSHIBA, | ||||||
|  |   PROTOCOL_ZHLT01, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // Simple enum to represent horizontal directios | // Simple enum to represent horizontal directios | ||||||
|   | |||||||
| @@ -23,6 +23,13 @@ void MCP23S17::setup() { | |||||||
|   this->transfer_byte(0b00011000);  // Enable HAEN pins for addressing |   this->transfer_byte(0b00011000);  // Enable HAEN pins for addressing | ||||||
|   this->disable(); |   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 |   // Read current output register state | ||||||
|   this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_); |   this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_); | ||||||
|   this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); |   this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); | ||||||
|   | |||||||
| @@ -571,24 +571,16 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens | |||||||
|           static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask); |           static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask); | ||||||
|     } break; |     } break; | ||||||
|     case SensorValueType::U_QWORD: |     case SensorValueType::U_QWORD: | ||||||
|       // Ignore bitmask for U_QWORD |  | ||||||
|       value = get_data<uint64_t>(data, offset); |  | ||||||
|       break; |  | ||||||
|     case SensorValueType::S_QWORD: |     case SensorValueType::S_QWORD: | ||||||
|       // Ignore bitmask for S_QWORD |       // Ignore bitmask for QWORD | ||||||
|       value = get_data<int64_t>(data, offset); |       value = get_data<uint64_t>(data, offset); | ||||||
|       break; |       break; | ||||||
|     case SensorValueType::U_QWORD_R: |     case SensorValueType::U_QWORD_R: | ||||||
|       // Ignore bitmask for U_QWORD |     case SensorValueType::S_QWORD_R: { | ||||||
|       value = get_data<uint64_t>(data, offset); |       // Ignore bitmask for QWORD | ||||||
|       value = static_cast<uint64_t>(value & 0xFFFF) << 48 | (value & 0xFFFF000000000000) >> 48 | |       uint64_t tmp = get_data<uint64_t>(data, offset); | ||||||
|               static_cast<uint64_t>(value & 0xFFFF0000) << 32 | (value & 0x0000FFFF00000000) >> 32 | |       value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000); | ||||||
|               static_cast<uint64_t>(value & 0xFFFF00000000) << 16 | (value & 0x00000000FFFF0000) >> 16; |     } break; | ||||||
|       break; |  | ||||||
|     case SensorValueType::S_QWORD_R: |  | ||||||
|       // Ignore bitmask for S_QWORD |  | ||||||
|       value = get_data<int64_t>(data, offset); |  | ||||||
|       break; |  | ||||||
|     case SensorValueType::RAW: |     case SensorValueType::RAW: | ||||||
|     default: |     default: | ||||||
|       break; |       break; | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| from typing import Any, List | from typing import Any | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
| @@ -349,7 +349,7 @@ def _spi_extra_validate(config): | |||||||
| class MethodDescriptor: | class MethodDescriptor: | ||||||
|     method_schema: Any |     method_schema: Any | ||||||
|     to_code: Any |     to_code: Any | ||||||
|     supported_chips: List[str] |     supported_chips: list[str] | ||||||
|     extra_validate: Any = None |     extra_validate: Any = None | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,14 +5,17 @@ from esphome.config_helpers import merge_config | |||||||
|  |  | ||||||
| from esphome import git, yaml_util | from esphome import git, yaml_util | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|  |     CONF_ESPHOME, | ||||||
|     CONF_FILE, |     CONF_FILE, | ||||||
|     CONF_FILES, |     CONF_FILES, | ||||||
|  |     CONF_MIN_VERSION, | ||||||
|     CONF_PACKAGES, |     CONF_PACKAGES, | ||||||
|     CONF_REF, |     CONF_REF, | ||||||
|     CONF_REFRESH, |     CONF_REFRESH, | ||||||
|     CONF_URL, |     CONF_URL, | ||||||
|     CONF_USERNAME, |     CONF_USERNAME, | ||||||
|     CONF_PASSWORD, |     CONF_PASSWORD, | ||||||
|  |     __version__ as ESPHOME_VERSION, | ||||||
| ) | ) | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
|  |  | ||||||
| @@ -104,7 +107,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|  |  | ||||||
|  |  | ||||||
| def _process_base_package(config: dict) -> dict: | def _process_base_package(config: dict) -> dict: | ||||||
|     repo_dir = git.clone_or_update( |     repo_dir, revert = git.clone_or_update( | ||||||
|         url=config[CONF_URL], |         url=config[CONF_URL], | ||||||
|         ref=config.get(CONF_REF), |         ref=config.get(CONF_REF), | ||||||
|         refresh=config[CONF_REFRESH], |         refresh=config[CONF_REFRESH], | ||||||
| @@ -112,21 +115,51 @@ def _process_base_package(config: dict) -> dict: | |||||||
|         username=config.get(CONF_USERNAME), |         username=config.get(CONF_USERNAME), | ||||||
|         password=config.get(CONF_PASSWORD), |         password=config.get(CONF_PASSWORD), | ||||||
|     ) |     ) | ||||||
|     files: str = config[CONF_FILES] |     files: list[str] = config[CONF_FILES] | ||||||
|  |  | ||||||
|  |     def get_packages(files) -> dict: | ||||||
|         packages = {} |         packages = {} | ||||||
|         for file in files: |         for file in files: | ||||||
|             yaml_file: Path = repo_dir / file |             yaml_file: Path = repo_dir / file | ||||||
|  |  | ||||||
|             if not yaml_file.is_file(): |             if not yaml_file.is_file(): | ||||||
|             raise cv.Invalid(f"{file} does not exist in repository", path=[CONF_FILES]) |                 raise cv.Invalid( | ||||||
|  |                     f"{file} does not exist in repository", path=[CONF_FILES] | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|             try: |             try: | ||||||
|             packages[file] = yaml_util.load_yaml(yaml_file) |                 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: |             except EsphomeError as e: | ||||||
|                 raise cv.Invalid( |                 raise cv.Invalid( | ||||||
|                     f"{file} is not a valid YAML file. Please check the file contents." |                     f"{file} is not a valid YAML file. Please check the file contents." | ||||||
|                 ) from e |                 ) from e | ||||||
|  |         return packages | ||||||
|  |  | ||||||
|  |     packages = {} | ||||||
|  |  | ||||||
|  |     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") | ||||||
|  |  | ||||||
|     return {"packages": packages} |     return {"packages": packages} | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,62 +11,43 @@ void PulseMeterSensor::setup() { | |||||||
|   this->isr_pin_ = pin_->to_isr(); |   this->isr_pin_ = pin_->to_isr(); | ||||||
|   this->pin_->attach_interrupt(PulseMeterSensor::gpio_intr, this, gpio::INTERRUPT_ANY_EDGE); |   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_detected_edge_us_ = 0; | ||||||
|   this->last_valid_low_edge_us_ = 0; |  | ||||||
|   this->last_valid_high_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->sensor_is_high_ = this->isr_pin_.digital_read(); | ||||||
|  |   this->has_valid_high_edge_ = false; | ||||||
|  |   this->has_valid_low_edge_ = false; | ||||||
| } | } | ||||||
|  |  | ||||||
| void PulseMeterSensor::loop() { | 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(); |   const uint32_t now = micros(); | ||||||
|  |  | ||||||
|   // Check to see if we should filter this edge out |   // If we've exceeded our timeout interval without receiving any pulses, assume | ||||||
|   if (this->filter_mode_ == FILTER_EDGE) { |   // 0 pulses/min until we get at least two valid pulses. | ||||||
|     if ((this->last_detected_edge_us_ - this->last_valid_high_edge_us_) >= this->filter_us_) { |   const uint32_t time_since_valid_edge_us = now - last_valid_high_edge_us; | ||||||
|       // Don't measure the first valid pulse (we need at least two pulses to measure the width) |   if ((has_valid_high_edge) && (time_since_valid_edge_us > this->timeout_us_)) { | ||||||
|       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)) { |  | ||||||
|     ESP_LOGD(TAG, "No pulse detected for %us, assuming 0 pulses/min", time_since_valid_edge_us / 1000000); |     ESP_LOGD(TAG, "No pulse detected for %us, assuming 0 pulses/min", time_since_valid_edge_us / 1000000); | ||||||
|  |  | ||||||
|     this->pulse_width_us_ = 0; |     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 |   // We quantize our pulse widths to 1 ms to avoid unnecessary jitter | ||||||
|   const uint32_t pulse_width_ms = this->pulse_width_us_ / 1000; |   const uint32_t pulse_width_ms = this->pulse_width_us_ / 1000; | ||||||
|   if (this->pulse_width_dedupe_.next(pulse_width_ms)) { |   if (this->pulse_width_dedupe_.next(pulse_width_ms)) { | ||||||
|     if (pulse_width_ms == 0) { |     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); |       this->publish_state(0); | ||||||
|     } else { |     } else { | ||||||
|       // Calculate pulses/min from the pulse width in ms |       // 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) { | 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(); |   const uint32_t now = micros(); | ||||||
|  |  | ||||||
|   // We only look at rising edges in EDGE mode, and all edges in PULSE mode |   // 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()) { |     if (sensor->isr_pin_.digital_read()) { | ||||||
|       sensor->last_detected_edge_us_ = now; |       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 { |   } 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; |     sensor->last_detected_edge_us_ = now; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| #include "esphome/components/sensor/sensor.h" |  | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -42,11 +42,14 @@ class PulseMeterSensor : public sensor::Sensor, public Component { | |||||||
|   Deduplicator<uint32_t> total_dedupe_; |   Deduplicator<uint32_t> total_dedupe_; | ||||||
|  |  | ||||||
|   volatile uint32_t last_detected_edge_us_ = 0; |   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_high_edge_us_ = 0; | ||||||
|  |   volatile uint32_t last_valid_low_edge_us_ = 0; | ||||||
|   volatile uint32_t pulse_width_us_ = 0; |   volatile uint32_t pulse_width_us_ = 0; | ||||||
|   volatile uint32_t total_pulses_ = 0; |   volatile uint32_t total_pulses_ = 0; | ||||||
|   volatile bool sensor_is_high_ = false; |   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 | }  // namespace pulse_meter | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| from typing import List |  | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome import automation | 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) |     await setup_entity(var, config) | ||||||
|  |  | ||||||
|     cg.add(var.traits.set_options(options)) |     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) |         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]): |     if not CORE.has_id(config[CONF_ID]): | ||||||
|         var = cg.Pvariable(config[CONF_ID], var) |         var = cg.Pvariable(config[CONF_ID], var) | ||||||
|     cg.add(cg.App.register_select(var)) |     cg.add(cg.App.register_select(var)) | ||||||
|     await setup_select_core_(var, config, options=options) |     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]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     await register_select(var, config, options=options) |     await register_select(var, config, options=options) | ||||||
|     return var |     return var | ||||||
|   | |||||||
| @@ -29,6 +29,7 @@ from esphome.const import ( | |||||||
|     CONF_WINDOW_SIZE, |     CONF_WINDOW_SIZE, | ||||||
|     CONF_MQTT_ID, |     CONF_MQTT_ID, | ||||||
|     CONF_FORCE_UPDATE, |     CONF_FORCE_UPDATE, | ||||||
|  |     DEVICE_CLASS_DISTANCE, | ||||||
|     DEVICE_CLASS_DURATION, |     DEVICE_CLASS_DURATION, | ||||||
|     DEVICE_CLASS_EMPTY, |     DEVICE_CLASS_EMPTY, | ||||||
|     DEVICE_CLASS_APPARENT_POWER, |     DEVICE_CLASS_APPARENT_POWER, | ||||||
| @@ -43,6 +44,7 @@ from esphome.const import ( | |||||||
|     DEVICE_CLASS_GAS, |     DEVICE_CLASS_GAS, | ||||||
|     DEVICE_CLASS_HUMIDITY, |     DEVICE_CLASS_HUMIDITY, | ||||||
|     DEVICE_CLASS_ILLUMINANCE, |     DEVICE_CLASS_ILLUMINANCE, | ||||||
|  |     DEVICE_CLASS_MOISTURE, | ||||||
|     DEVICE_CLASS_MONETARY, |     DEVICE_CLASS_MONETARY, | ||||||
|     DEVICE_CLASS_NITROGEN_DIOXIDE, |     DEVICE_CLASS_NITROGEN_DIOXIDE, | ||||||
|     DEVICE_CLASS_NITROGEN_MONOXIDE, |     DEVICE_CLASS_NITROGEN_MONOXIDE, | ||||||
| @@ -56,11 +58,14 @@ from esphome.const import ( | |||||||
|     DEVICE_CLASS_PRESSURE, |     DEVICE_CLASS_PRESSURE, | ||||||
|     DEVICE_CLASS_REACTIVE_POWER, |     DEVICE_CLASS_REACTIVE_POWER, | ||||||
|     DEVICE_CLASS_SIGNAL_STRENGTH, |     DEVICE_CLASS_SIGNAL_STRENGTH, | ||||||
|  |     DEVICE_CLASS_SPEED, | ||||||
|     DEVICE_CLASS_SULPHUR_DIOXIDE, |     DEVICE_CLASS_SULPHUR_DIOXIDE, | ||||||
|     DEVICE_CLASS_TEMPERATURE, |     DEVICE_CLASS_TEMPERATURE, | ||||||
|     DEVICE_CLASS_TIMESTAMP, |     DEVICE_CLASS_TIMESTAMP, | ||||||
|     DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, |     DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, | ||||||
|     DEVICE_CLASS_VOLTAGE, |     DEVICE_CLASS_VOLTAGE, | ||||||
|  |     DEVICE_CLASS_VOLUME, | ||||||
|  |     DEVICE_CLASS_WEIGHT, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, coroutine_with_priority | ||||||
| from esphome.cpp_generator import MockObjClass | from esphome.cpp_generator import MockObjClass | ||||||
| @@ -77,12 +82,14 @@ DEVICE_CLASSES = [ | |||||||
|     DEVICE_CLASS_CARBON_MONOXIDE, |     DEVICE_CLASS_CARBON_MONOXIDE, | ||||||
|     DEVICE_CLASS_CURRENT, |     DEVICE_CLASS_CURRENT, | ||||||
|     DEVICE_CLASS_DATE, |     DEVICE_CLASS_DATE, | ||||||
|  |     DEVICE_CLASS_DISTANCE, | ||||||
|     DEVICE_CLASS_DURATION, |     DEVICE_CLASS_DURATION, | ||||||
|     DEVICE_CLASS_ENERGY, |     DEVICE_CLASS_ENERGY, | ||||||
|     DEVICE_CLASS_FREQUENCY, |     DEVICE_CLASS_FREQUENCY, | ||||||
|     DEVICE_CLASS_GAS, |     DEVICE_CLASS_GAS, | ||||||
|     DEVICE_CLASS_HUMIDITY, |     DEVICE_CLASS_HUMIDITY, | ||||||
|     DEVICE_CLASS_ILLUMINANCE, |     DEVICE_CLASS_ILLUMINANCE, | ||||||
|  |     DEVICE_CLASS_MOISTURE, | ||||||
|     DEVICE_CLASS_MONETARY, |     DEVICE_CLASS_MONETARY, | ||||||
|     DEVICE_CLASS_NITROGEN_DIOXIDE, |     DEVICE_CLASS_NITROGEN_DIOXIDE, | ||||||
|     DEVICE_CLASS_NITROGEN_MONOXIDE, |     DEVICE_CLASS_NITROGEN_MONOXIDE, | ||||||
| @@ -96,11 +103,14 @@ DEVICE_CLASSES = [ | |||||||
|     DEVICE_CLASS_PRESSURE, |     DEVICE_CLASS_PRESSURE, | ||||||
|     DEVICE_CLASS_REACTIVE_POWER, |     DEVICE_CLASS_REACTIVE_POWER, | ||||||
|     DEVICE_CLASS_SIGNAL_STRENGTH, |     DEVICE_CLASS_SIGNAL_STRENGTH, | ||||||
|  |     DEVICE_CLASS_SPEED, | ||||||
|     DEVICE_CLASS_SULPHUR_DIOXIDE, |     DEVICE_CLASS_SULPHUR_DIOXIDE, | ||||||
|     DEVICE_CLASS_TEMPERATURE, |     DEVICE_CLASS_TEMPERATURE, | ||||||
|     DEVICE_CLASS_TIMESTAMP, |     DEVICE_CLASS_TIMESTAMP, | ||||||
|     DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, |     DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, | ||||||
|     DEVICE_CLASS_VOLTAGE, |     DEVICE_CLASS_VOLTAGE, | ||||||
|  |     DEVICE_CLASS_VOLUME, | ||||||
|  |     DEVICE_CLASS_WEIGHT, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| sensor_ns = cg.esphome_ns.namespace("sensor") | sensor_ns = cg.esphome_ns.namespace("sensor") | ||||||
|   | |||||||
| @@ -76,6 +76,8 @@ void SSD1327::setup() { | |||||||
|   this->command(0x55); |   this->command(0x55); | ||||||
|   this->command(SSD1327_SETVCOMHVOLTAGE);  // Set High Voltage Level of COM Pin |   this->command(SSD1327_SETVCOMHVOLTAGE);  // Set High Voltage Level of COM Pin | ||||||
|   this->command(0x1C); |   this->command(0x1C); | ||||||
|  |   this->command(SSD1327_SETGPIO);  // Switch voltage converter on (for Aliexpress display) | ||||||
|  |   this->command(0x03); | ||||||
|   this->command(SSD1327_NORMALDISPLAY);  // set display mode |   this->command(SSD1327_NORMALDISPLAY);  // set display mode | ||||||
|   set_brightness(this->brightness_); |   set_brightness(this->brightness_); | ||||||
|   this->fill(Color::BLACK);  // clear display - ensures we do not see garbage at power-on |   this->fill(Color::BLACK);  // clear display - ensures we do not see garbage at power-on | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ from esphome import pins | |||||||
| from esphome.components import display, spi | from esphome.components import display, spi | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_BACKLIGHT_PIN, |     CONF_BACKLIGHT_PIN, | ||||||
|     CONF_CS_PIN, |  | ||||||
|     CONF_DC_PIN, |     CONF_DC_PIN, | ||||||
|     CONF_HEIGHT, |     CONF_HEIGHT, | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
| @@ -69,7 +68,6 @@ CONFIG_SCHEMA = cv.All( | |||||||
|             cv.Required(CONF_MODEL): ST7789V_MODEL, |             cv.Required(CONF_MODEL): ST7789V_MODEL, | ||||||
|             cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, |             cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, | ||||||
|             cv.Required(CONF_DC_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_BACKLIGHT_PIN): pins.gpio_output_pin_schema, | ||||||
|             cv.Optional(CONF_EIGHTBITCOLOR, default=False): cv.boolean, |             cv.Optional(CONF_EIGHTBITCOLOR, default=False): cv.boolean, | ||||||
|             cv.Optional(CONF_HEIGHT): cv.int_, |             cv.Optional(CONF_HEIGHT): cv.int_, | ||||||
| @@ -79,7 +77,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     .extend(cv.polling_component_schema("5s")) |     .extend(cv.polling_component_schema("5s")) | ||||||
|     .extend(spi.spi_device_schema()), |     .extend(spi.spi_device_schema(cs_pin_required=False)), | ||||||
|     validate_st7789v, |     validate_st7789v, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -69,6 +69,8 @@ from esphome.const import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| CONF_PRESET_CHANGE = "preset_change" | CONF_PRESET_CHANGE = "preset_change" | ||||||
|  | CONF_DEFAULT_PRESET = "default_preset" | ||||||
|  | CONF_ON_BOOT_RESTORE_FROM = "on_boot_restore_from" | ||||||
|  |  | ||||||
| CODEOWNERS = ["@kbx81"] | CODEOWNERS = ["@kbx81"] | ||||||
|  |  | ||||||
| @@ -80,6 +82,13 @@ ThermostatClimate = thermostat_ns.class_( | |||||||
| ThermostatClimateTargetTempConfig = thermostat_ns.struct( | ThermostatClimateTargetTempConfig = thermostat_ns.struct( | ||||||
|     "ThermostatClimateTargetTempConfig" |     "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") | ClimateMode = climate_ns.enum("ClimateMode") | ||||||
| CLIMATE_MODES = { | CLIMATE_MODES = { | ||||||
|     "OFF": ClimateMode.CLIMATE_MODE_OFF, |     "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): | def validate_thermostat(config): | ||||||
|     # verify corresponding action(s) exist(s) for any defined climate mode or action |     # verify corresponding action(s) exist(s) for any defined climate mode or action | ||||||
|     requirements = { |     requirements = { | ||||||
| @@ -277,13 +297,32 @@ def validate_thermostat(config): | |||||||
|             CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION], |             CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION], | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     # Validate temperature requirements for default configuraation |     # Legacy high/low configs | ||||||
|     validate_temperature_preset(config, config, "default", requirements) |     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: |     if CONF_AWAY_CONFIG in config: | ||||||
|         away = config[CONF_AWAY_CONFIG] |         comparable_preset = generate_comparable_preset(config[CONF_AWAY_CONFIG], "Away") | ||||||
|         validate_temperature_preset(away, config, "away", requirements) |  | ||||||
|  |         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 |     # Validate temperature requirements for presets | ||||||
|     if CONF_PRESET in config: |     if CONF_PRESET in config: | ||||||
| @@ -292,7 +331,12 @@ def validate_thermostat(config): | |||||||
|                 preset_config, config, preset_config[CONF_NAME], requirements |                 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] |     default_mode = config[CONF_DEFAULT_MODE] | ||||||
|     requirements = { |     requirements = { | ||||||
|         "HEAT_COOL": [CONF_COOL_ACTION, CONF_HEAT_ACTION], |         "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" |                         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: |     if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config: | ||||||
|         raise cv.Invalid( |         raise cv.Invalid( | ||||||
|             f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_COOLING}" |             f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_COOLING}" | ||||||
| @@ -502,9 +578,8 @@ CONFIG_SCHEMA = cv.All( | |||||||
|             cv.Optional( |             cv.Optional( | ||||||
|                 CONF_TARGET_TEMPERATURE_CHANGE_ACTION |                 CONF_TARGET_TEMPERATURE_CHANGE_ACTION | ||||||
|             ): automation.validate_automation(single=True), |             ): automation.validate_automation(single=True), | ||||||
|             cv.Optional(CONF_DEFAULT_MODE, default="OFF"): cv.templatable( |             cv.Optional(CONF_DEFAULT_MODE, default=None): cv.valid, | ||||||
|                 validate_climate_mode |             cv.Optional(CONF_DEFAULT_PRESET): cv.templatable(cv.string), | ||||||
|             ), |  | ||||||
|             cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, |             cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, | ||||||
|             cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, |             cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, | ||||||
|             cv.Optional( |             cv.Optional( | ||||||
| @@ -542,6 +617,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|                 } |                 } | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA), |             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( |             cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation( | ||||||
|                 single=True |                 single=True | ||||||
|             ), |             ), | ||||||
| @@ -564,9 +640,10 @@ async def to_code(config): | |||||||
|         CONF_COOL_ACTION in config |         CONF_COOL_ACTION in config | ||||||
|         or (config[CONF_FAN_ONLY_COOLING] and CONF_FAN_ONLY_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]) |     sens = await cg.get_variable(config[CONF_SENSOR]) | ||||||
|     cg.add(var.set_default_mode(config[CONF_DEFAULT_MODE])) |  | ||||||
|     cg.add( |     cg.add( | ||||||
|         var.set_set_point_minimum_differential( |         var.set_set_point_minimum_differential( | ||||||
|             config[CONF_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_deadband(config[CONF_HEAT_DEADBAND])) | ||||||
|     cg.add(var.set_heat_overrun(config[CONF_HEAT_OVERRUN])) |     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: |     if CONF_MAX_COOLING_RUN_TIME in config: | ||||||
|         cg.add( |         cg.add( | ||||||
|             var.set_cooling_maximum_run_time_in_sec(config[CONF_MAX_COOLING_RUN_TIME]) |             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_supports_fan_with_heating(config[CONF_FAN_WITH_HEATING])) | ||||||
|  |  | ||||||
|     cg.add(var.set_use_startup_delay(config[CONF_STARTUP_DELAY])) |     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( |     await automation.build_automation( | ||||||
|         var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION] |         var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION] | ||||||
| @@ -808,27 +867,8 @@ async def to_code(config): | |||||||
|             config[CONF_TARGET_TEMPERATURE_CHANGE_ACTION], |             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: |     if CONF_PRESET in config: | ||||||
|         for preset_config in config[CONF_PRESET]: |         for preset_config in config[CONF_PRESET]: | ||||||
|  |  | ||||||
|             name = preset_config[CONF_NAME] |             name = preset_config[CONF_NAME] | ||||||
|             standard_preset = None |             standard_preset = None | ||||||
|             if name.upper() in climate.CLIMATE_PRESETS: |             if name.upper() in climate.CLIMATE_PRESETS: | ||||||
| @@ -872,6 +912,19 @@ async def to_code(config): | |||||||
|             else: |             else: | ||||||
|                 cg.add(var.set_custom_preset_config(name, preset_target_variable)) |                 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: |     if CONF_PRESET_CHANGE in config: | ||||||
|         await automation.build_automation( |         await automation.build_automation( | ||||||
|             var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE] |             var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE] | ||||||
|   | |||||||
| @@ -25,15 +25,27 @@ void ThermostatClimate::setup() { | |||||||
|     this->publish_state(); |     this->publish_state(); | ||||||
|   }); |   }); | ||||||
|   this->current_temperature = this->sensor_->state; |   this->current_temperature = this->sensor_->state; | ||||||
|  |  | ||||||
|  |   auto use_default_preset = true; | ||||||
|  |  | ||||||
|  |   if (this->on_boot_restore_from_ == thermostat::OnBootRestoreFrom::MEMORY) { | ||||||
|     // restore all climate data, if possible |     // restore all climate data, if possible | ||||||
|     auto restore = this->restore_state_(); |     auto restore = this->restore_state_(); | ||||||
|     if (restore.has_value()) { |     if (restore.has_value()) { | ||||||
|  |       use_default_preset = false; | ||||||
|       restore->to_call(this).perform(); |       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); |  | ||||||
|     } |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // 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 |   // 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_action_(this->compute_action_(), false); | ||||||
|   this->switch_to_supplemental_action_(this->compute_supplemental_action_()); |   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)); |           (this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING)); | ||||||
| } | } | ||||||
|  |  | ||||||
| void ThermostatClimate::dump_preset_config_(const std::string &preset, | void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, | ||||||
|                                             const ThermostatClimateTargetTempConfig &config) { |                                             bool is_default_preset) { | ||||||
|   const auto *preset_name = preset.c_str(); |   ESP_LOGCONFIG(TAG, "      %s Is Default: %s", preset_name, YESNO(is_default_preset)); | ||||||
|  |  | ||||||
|   if (this->supports_heat_) { |   if (this->supports_heat_) { | ||||||
|     if (this->supports_two_points_) { |     if (this->supports_two_points_) { | ||||||
| @@ -962,9 +974,19 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { | |||||||
|   auto config = this->preset_config_.find(preset); |   auto config = this->preset_config_.find(preset); | ||||||
|  |  | ||||||
|   if (config != this->preset_config_.end()) { |   if (config != this->preset_config_.end()) { | ||||||
|     ESP_LOGI(TAG, "Switching to preset  %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); |     ESP_LOGI(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); | ||||||
|     this->change_preset_internal_(config->second); |     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->custom_preset.reset(); | ||||||
|     this->preset = preset; |     this->preset = preset; | ||||||
|   } else { |   } else { | ||||||
| @@ -976,9 +998,19 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) | |||||||
|   auto config = this->custom_preset_config_.find(custom_preset); |   auto config = this->custom_preset_config_.find(custom_preset); | ||||||
|  |  | ||||||
|   if (config != this->custom_preset_config_.end()) { |   if (config != this->custom_preset_config_.end()) { | ||||||
|     ESP_LOGI(TAG, "Switching to custom preset  %s", custom_preset.c_str()); |     ESP_LOGI(TAG, "Custom preset %s requested", custom_preset.c_str()); | ||||||
|     this->change_preset_internal_(config->second); |     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->preset.reset(); | ||||||
|     this->custom_preset = custom_preset; |     this->custom_preset = custom_preset; | ||||||
|   } else { |   } 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_) { |   if (this->supports_two_points_) { | ||||||
|  |     if (this->target_temperature_low != config.default_temperature_low) { | ||||||
|       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; |       this->target_temperature_high = config.default_temperature_high; | ||||||
|  |       something_changed = true; | ||||||
|  |     } | ||||||
|   } else { |   } else { | ||||||
|  |     if (this->target_temperature != config.default_temperature) { | ||||||
|       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 |   // 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 control's version will override these for that 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()) { |   if (config.mode_.has_value() && (this->mode != config.mode_.value())) { | ||||||
|     this->mode = *config.mode_; |  | ||||||
|     ESP_LOGV(TAG, "Setting mode to %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); |     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()) { |   if (config.fan_mode_.has_value() && (this->fan_mode != config.fan_mode_.value())) { | ||||||
|     this->fan_mode = *config.fan_mode_; |  | ||||||
|     ESP_LOGV(TAG, "Setting fan mode to %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_))); |     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_))); |     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_; |     this->swing_mode = *config.swing_mode_; | ||||||
|  |     something_changed = true; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Fire any preset changed trigger if defined |   return something_changed; | ||||||
|   if (this->preset != preset) { |  | ||||||
|     Trigger<> *trig = this->preset_change_trigger_; |  | ||||||
|     assert(trig != nullptr); |  | ||||||
|     trig->trigger(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   this->refresh(); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void ThermostatClimate::set_preset_config(climate::ClimatePreset preset, | void ThermostatClimate::set_preset_config(climate::ClimatePreset preset, | ||||||
| @@ -1061,7 +1100,15 @@ ThermostatClimate::ThermostatClimate() | |||||||
|       temperature_change_trigger_(new Trigger<>()), |       temperature_change_trigger_(new Trigger<>()), | ||||||
|       preset_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) { | void ThermostatClimate::set_set_point_minimum_differential(float differential) { | ||||||
|   this->set_point_minimum_differential_ = 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() { | void ThermostatClimate::dump_config() { | ||||||
|   LOG_CLIMATE("", "Thermostat", this); |   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, "  Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); | ||||||
|  |   } | ||||||
|   ESP_LOGCONFIG(TAG, "  Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_)); |   ESP_LOGCONFIG(TAG, "  Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_)); | ||||||
|   if (this->supports_cool_) { |   if (this->supports_cool_) { | ||||||
|     ESP_LOGCONFIG(TAG, "  Cooling Parameters:"); |     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)); |     const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); | ||||||
|  |  | ||||||
|     ESP_LOGCONFIG(TAG, "    Supports %s: %s", preset_name, YESNO(true)); |     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: "); |   ESP_LOGCONFIG(TAG, "  Supported CUSTOM PRESETS: "); | ||||||
| @@ -1292,8 +1340,10 @@ void ThermostatClimate::dump_config() { | |||||||
|     const auto *preset_name = it.first.c_str(); |     const auto *preset_name = it.first.c_str(); | ||||||
|  |  | ||||||
|     ESP_LOGCONFIG(TAG, "    Supports %s: %s", preset_name, YESNO(true)); |     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; | ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ enum ThermostatClimateTimerIndex : size_t { | |||||||
|   TIMER_IDLE_ON = 9, |   TIMER_IDLE_ON = 9, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | enum OnBootRestoreFrom : size_t { MEMORY = 0, DEFAULT_PRESET = 1 }; | ||||||
| struct ThermostatClimateTimer { | struct ThermostatClimateTimer { | ||||||
|   const std::string name; |   const std::string name; | ||||||
|   bool active; |   bool active; | ||||||
| @@ -57,7 +58,9 @@ class ThermostatClimate : public climate::Climate, public Component { | |||||||
|   void setup() override; |   void setup() override; | ||||||
|   void dump_config() 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_set_point_minimum_differential(float differential); | ||||||
|   void set_cool_deadband(float deadband); |   void set_cool_deadband(float deadband); | ||||||
|   void set_cool_overrun(float overrun); |   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. |   /// Applies the temperature, mode, fan, and swing modes of the provided config. | ||||||
|   /// This is agnostic of custom vs built in preset |   /// 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. |   /// Return the traits of this controller. | ||||||
|   climate::ClimateTraits traits() override; |   climate::ClimateTraits traits() override; | ||||||
| @@ -225,7 +229,8 @@ class ThermostatClimate : public climate::Climate, public Component { | |||||||
|   bool supplemental_cooling_required_(); |   bool supplemental_cooling_required_(); | ||||||
|   bool supplemental_heating_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 |   /// The sensor used for getting the current temperature | ||||||
|   sensor::Sensor *sensor_{nullptr}; |   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 |   /// These are used to determine when a trigger/action needs to be called | ||||||
|   climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; |   climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; | ||||||
|   climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; |   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::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; | ||||||
|   climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; |   climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; | ||||||
|  |  | ||||||
| @@ -441,6 +445,15 @@ class ThermostatClimate : public climate::Climate, public Component { | |||||||
|   std::map<climate::ClimatePreset, ThermostatClimateTargetTempConfig> preset_config_{}; |   std::map<climate::ClimatePreset, ThermostatClimateTargetTempConfig> preset_config_{}; | ||||||
|   /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") |   /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") | ||||||
|   std::map<std::string, ThermostatClimateTargetTempConfig> custom_preset_config_{}; |   std::map<std::string, ThermostatClimateTargetTempConfig> 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 | }  // namespace thermostat | ||||||
|   | |||||||
| @@ -6,6 +6,8 @@ namespace esphome { | |||||||
| namespace time { | namespace time { | ||||||
|  |  | ||||||
| static const char *const TAG = "automation"; | 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_second(uint8_t second) { this->seconds_[second] = true; } | ||||||
| void CronTrigger::add_minute(uint8_t minute) { this->minutes_[minute] = true; } | void CronTrigger::add_minute(uint8_t minute) { this->minutes_[minute] = true; } | ||||||
| @@ -23,12 +25,17 @@ void CronTrigger::loop() { | |||||||
|     return; |     return; | ||||||
|  |  | ||||||
|   if (this->last_check_.has_value()) { |   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 |       // We went back in time (a lot), probably caused by time synchronization | ||||||
|       ESP_LOGW(TAG, "Time has jumped back!"); |       ESP_LOGW(TAG, "Time has jumped back!"); | ||||||
|     } else if (*this->last_check_ >= time) { |     } else if (*this->last_check_ >= time) { | ||||||
|       // already handled this one |       // already handled this one | ||||||
|       return; |       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) { |     while (true) { | ||||||
|   | |||||||
| @@ -23,6 +23,12 @@ class TouchscreenBinarySensor : public binary_sensor::BinarySensor, | |||||||
|     this->y_min_ = y_min; |     this->y_min_ = y_min; | ||||||
|     this->y_max_ = y_max; |     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; } |   void set_page(display::DisplayPage *page) { this->page_ = page; } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ namespace esphome { | |||||||
| namespace web_server { | namespace web_server { | ||||||
|  |  | ||||||
| const uint8_t INDEX_GZ[] PROGMEM = { | 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, |     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, |     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, |     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, |     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, |     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, |     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, |     0x5f, 0xee, 0x57, 0xf4, 0xd0, 0x49, 0xe1, 0x85, 0xf1, 0xfb, 0x57, 0x16, 0x79, 0xa2, 0xaf, 0xa8, 0x65, 0x23, 0xca, | ||||||
|     0xc2, 0x16, 0xb9, 0x80, 0xef, 0x3e, 0xe7, 0x74, 0x40, 0x64, 0x15, 0x87, 0xb6, 0x0c, 0xc0, 0xcc, 0xf1, 0xdb, 0xb4, |     0xf4, 0xf4, 0x91, 0x97, 0xe8, 0x9e, 0x20, 0x54, 0x6e, 0x86, 0xf0, 0x9f, 0xf1, 0x09, 0xb0, 0x2d, 0xfc, 0xd4, 0x9c, | ||||||
|     0xea, 0xe5, 0x54, 0xdc, 0x5c, 0x09, 0xe9, 0xda, 0xd9, 0x8e, 0x0e, 0xde, 0x60, 0xa2, 0x77, 0xb8, 0xcc, 0x78, 0x84, |     0x1d, 0xc0, 0x4f, 0xaa, 0xb5, 0x19, 0xc6, 0x0a, 0x0a, 0xbb, 0xac, 0x85, 0xf3, 0x29, 0x1c, 0x41, 0x51, 0x84, 0x7d, | ||||||
|     0xbf, 0xfc, 0x88, 0xc7, 0x3c, 0xc1, 0x4b, 0xb1, 0xf2, 0x02, 0x19, 0xe6, 0x25, 0x7f, 0x87, 0x39, 0xd5, 0xea, 0x90, |     0x7a, 0x77, 0x70, 0x46, 0x91, 0x63, 0xf8, 0xee, 0x73, 0x4e, 0x1d, 0x44, 0xb6, 0x72, 0x68, 0xcb, 0xc0, 0xce, 0x1c, | ||||||
|     0x60, 0x86, 0x01, 0x83, 0x57, 0x6c, 0x1c, 0x47, 0x8e, 0xed, 0xcc, 0x61, 0xc7, 0xc2, 0x98, 0xad, 0x5a, 0x42, 0x33, |     0xbf, 0x79, 0xab, 0x5e, 0x4e, 0xc5, 0x8d, 0x98, 0x90, 0xae, 0xb3, 0xed, 0xe8, 0xa0, 0x10, 0x26, 0x90, 0x87, 0xcb, | ||||||
|     0xe5, 0x32, 0xbb, 0xb6, 0xfa, 0x7d, 0x3b, 0x39, 0x7e, 0xbf, 0x2c, 0x3c, 0x94, 0x01, 0x46, 0x5b, 0x7a, 0x00, 0x30, |     0x8c, 0x47, 0xf8, 0x4b, 0x95, 0x78, 0xcc, 0x13, 0xbc, 0x6c, 0x2b, 0x2f, 0xa6, 0x61, 0xbe, 0xf3, 0x77, 0x98, 0xab, | ||||||
|     0xbe, 0x2a, 0xc9, 0x51, 0xd8, 0x57, 0x56, 0x83, 0x2d, 0xcc, 0x86, 0x8e, 0xdf, 0x05, 0x37, 0x82, 0x8a, 0xf1, 0x7b, |     0xad, 0x30, 0x9e, 0x61, 0x20, 0xe2, 0x15, 0x1b, 0xc7, 0x91, 0x63, 0x3b, 0x73, 0x90, 0x04, 0x30, 0x66, 0xab, 0x96, | ||||||
|     0x50, 0x3f, 0x38, 0xad, 0x6d, 0x30, 0x6b, 0x8c, 0x6e, 0x7a, 0xa0, 0xe1, 0x4a, 0x18, 0x49, 0x04, 0x07, 0x1a, 0xa5, |     0x28, 0x4d, 0x39, 0xd2, 0xae, 0xad, 0x7e, 0x8f, 0x4f, 0x8e, 0xdf, 0x45, 0x0b, 0x0f, 0x65, 0xe0, 0xd2, 0x96, 0x9e, | ||||||
|     0x9e, 0xfe, 0x05, 0x64, 0x55, 0xb8, 0xa8, 0x78, 0x7c, 0x71, 0x20, 0xef, 0x7c, 0xdb, 0x18, 0xb9, 0xa5, 0x88, 0x7d, |     0x05, 0x8c, 0xaf, 0x4a, 0x72, 0x54, 0x22, 0x95, 0x35, 0x62, 0x0b, 0x73, 0xa4, 0xe3, 0x77, 0xc1, 0x3d, 0xa1, 0x62, | ||||||
|     0xf5, 0xbd, 0xa9, 0x4d, 0x50, 0x17, 0xf4, 0x5b, 0x20, 0xe9, 0xdc, 0x1b, 0x35, 0x02, 0xa6, 0x5c, 0x5b, 0xd2, 0x73, |     0xfc, 0xce, 0xd4, 0x0f, 0x4e, 0x6b, 0x1b, 0xcc, 0x25, 0xa3, 0x9b, 0x1e, 0x68, 0xb8, 0x12, 0x9e, 0x12, 0x41, 0x87, | ||||||
|     0x08, 0x6d, 0xa1, 0x0f, 0xc6, 0xec, 0x34, 0x1e, 0x49, 0xb1, 0xee, 0x59, 0xf2, 0xaa, 0x48, 0x8b, 0xb0, 0x08, 0x3b, |     0x46, 0xa9, 0xa7, 0x7f, 0xb1, 0x59, 0x15, 0x86, 0x2a, 0x1e, 0x5f, 0x1c, 0xc8, 0xbb, 0xe4, 0x36, 0x46, 0x84, 0xe9, | ||||||
|     0x9e, 0xf0, 0x9d, 0xe1, 0x05, 0xb5, 0x5a, 0x98, 0x66, 0x76, 0xff, 0x5e, 0x4f, 0x43, 0x52, 0xcf, 0x56, 0xb7, 0xf1, |     0x24, 0xa0, 0xfa, 0x8e, 0xd5, 0x26, 0xa8, 0x21, 0xfa, 0xed, 0x92, 0x74, 0x9e, 0x8e, 0x9a, 0x06, 0x53, 0xb9, 0x2d, | ||||||
|     0x57, 0x52, 0x1e, 0x82, 0xaf, 0xf6, 0xf7, 0xe1, 0x3d, 0xfc, 0xa5, 0x94, 0xf7, 0x86, 0xb6, 0xeb, 0x93, 0x50, 0xbc, |     0xe9, 0x91, 0x84, 0xb6, 0xd0, 0x33, 0x63, 0x76, 0x1a, 0x8f, 0xa4, 0xba, 0xf0, 0x2c, 0x79, 0x05, 0xa5, 0x45, 0x58, | ||||||
|     0x57, 0xfd, 0x66, 0x4a, 0x94, 0x08, 0x9b, 0xa0, 0xbf, 0xbc, 0xdb, 0x2a, 0x32, 0xa9, 0xb4, 0xba, 0x3b, 0x95, 0xd2, |     0x84, 0x1d, 0x4f, 0xf8, 0xe4, 0xf0, 0x82, 0xda, 0x32, 0x4c, 0x33, 0xbb, 0x7f, 0xaf, 0xa7, 0x21, 0xa9, 0x67, 0xc1, | ||||||
|     0x82, 0x67, 0x43, 0x4a, 0x81, 0x00, 0xed, 0xfa, 0x3b, 0x86, 0x28, 0x3c, 0x6d, 0xe1, 0xcf, 0x9a, 0x30, 0xbc, 0x0f, |     0xdb, 0xf8, 0xab, 0x2e, 0x0f, 0xc1, 0x07, 0xfc, 0xfb, 0xf0, 0x1e, 0xfe, 0xb2, 0xcb, 0x7b, 0x43, 0xdb, 0xf5, 0x49, | ||||||
|     0x0d, 0x94, 0x34, 0x7c, 0x09, 0xcd, 0xb7, 0x85, 0xe0, 0x85, 0x7e, 0x3f, 0x92, 0xa8, 0x12, 0x62, 0xaa, 0xce, 0x31, |     0xd8, 0xde, 0xab, 0x7e, 0xe3, 0x25, 0x4a, 0x9a, 0x4d, 0xd0, 0x8b, 0xde, 0x6d, 0x15, 0xa4, 0x54, 0x86, 0xdd, 0x9d, | ||||||
|     0x6b, 0x0e, 0x91, 0x44, 0x8e, 0x80, 0xed, 0x19, 0xf1, 0x26, 0xc1, 0xae, 0x32, 0x9a, 0xf2, 0x14, 0xfa, 0x3a, 0xfa, |     0x4a, 0x19, 0xc2, 0xb3, 0x21, 0xfd, 0x40, 0x30, 0x77, 0xfd, 0x1d, 0x43, 0xc4, 0x9e, 0xb6, 0xf0, 0x67, 0x4d, 0xc8, | ||||||
|     0x33, 0xce, 0xeb, 0xea, 0xbc, 0xda, 0xce, 0x59, 0x33, 0x05, 0x32, 0x7c, 0xe3, 0xa0, 0x8a, 0xae, 0x2e, 0x88, 0xcf, |     0xde, 0x87, 0x06, 0x4a, 0xca, 0xbe, 0x84, 0xe6, 0xdb, 0x42, 0xa0, 0x43, 0xbf, 0x1f, 0x49, 0x04, 0x0a, 0xf1, 0x57, | ||||||
|     0x99, 0x89, 0x6d, 0x5c, 0x7d, 0xf0, 0x6d, 0x4d, 0xf6, 0xad, 0xb9, 0x29, 0x58, 0xc5, 0x34, 0xb4, 0x2f, 0x30, 0x65, |     0xe7, 0x98, 0x35, 0x87, 0x53, 0x22, 0xf7, 0xc0, 0xf6, 0x8c, 0x38, 0x96, 0x60, 0x57, 0x19, 0xa5, 0x79, 0x0a, 0x7d, | ||||||
|     0x06, 0x7f, 0x56, 0xc5, 0xea, 0x41, 0x32, 0x94, 0x9f, 0x44, 0xf8, 0xdb, 0x58, 0xe8, 0x47, 0x59, 0x6d, 0x40, 0x4e, |     0x1d, 0xfd, 0x79, 0xe8, 0x75, 0x75, 0x5e, 0x6d, 0xd3, 0xac, 0x99, 0x02, 0x19, 0xbe, 0x71, 0x00, 0x46, 0x57, 0x22, | ||||||
|     0xdf, 0xab, 0x24, 0x48, 0x5f, 0x8c, 0xcb, 0x26, 0x12, 0x60, 0x2f, 0xe0, 0x2f, 0xf7, 0xab, 0xae, 0x4a, 0xc8, 0x3b, |     0xc4, 0x67, 0xd2, 0x84, 0x78, 0xa8, 0x3e, 0x24, 0xb7, 0x26, 0xab, 0xd7, 0xdc, 0x14, 0xac, 0x62, 0x1a, 0xda, 0x17, | ||||||
|     0x90, 0x98, 0x53, 0x30, 0x8e, 0x73, 0xba, 0x5a, 0xab, 0xf0, 0xaf, 0x45, 0x34, 0x2b, 0x52, 0xd3, 0xae, 0x64, 0xc5, |     0x98, 0x8a, 0x83, 0x3f, 0xab, 0x62, 0xf5, 0x20, 0x19, 0xca, 0x4f, 0x22, 0xfc, 0x2d, 0x2f, 0xf4, 0xa3, 0xac, 0x36, | ||||||
|     0xc0, 0xc6, 0x22, 0x3b, 0x90, 0xc9, 0x68, 0xe6, 0x07, 0x9b, 0xcd, 0xbb, 0x8f, 0x63, 0x91, 0x87, 0x86, 0x1f, 0xb4, |     0x20, 0xa7, 0xef, 0x60, 0x12, 0xa4, 0x2f, 0xc6, 0x65, 0x13, 0x09, 0xb0, 0x43, 0xf0, 0x97, 0x06, 0x56, 0x57, 0x30, | ||||||
|     0xb7, 0x05, 0x91, 0x6d, 0x10, 0x63, 0x57, 0xe2, 0x44, 0xc6, 0x0d, 0x5e, 0x19, 0xac, 0x7e, 0x43, 0x91, 0xb9, 0xe1, |     0xe4, 0xdd, 0x4a, 0xcc, 0x55, 0x18, 0xc7, 0x39, 0x5d, 0xd9, 0x55, 0xf8, 0xd7, 0x22, 0xa5, 0x15, 0xa9, 0x69, 0x57, | ||||||
|     0x6d, 0x73, 0xb5, 0xf4, 0xb8, 0xb4, 0x0e, 0xae, 0x8c, 0xdf, 0x1d, 0xb3, 0x88, 0xfb, 0x51, 0x4a, 0xb9, 0x49, 0x8e, |     0xb2, 0x62, 0x60, 0x63, 0x11, 0x88, 0x1a, 0x91, 0xe4, 0x66, 0x7e, 0x08, 0xda, 0xbc, 0x53, 0x39, 0x16, 0xf9, 0x6d, | ||||||
|     0x21, 0x16, 0xbc, 0x0e, 0xdb, 0x76, 0x4b, 0x90, 0x3c, 0xc6, 0xaf, 0x70, 0x12, 0xa4, 0xf7, 0xa1, 0xb0, 0x4a, 0xd8, |     0xf8, 0xa1, 0x7c, 0x5b, 0x10, 0xd9, 0x06, 0xf1, 0x78, 0x25, 0x4e, 0x64, 0x34, 0xe1, 0x55, 0xc4, 0xea, 0x37, 0x1f, | ||||||
|     0xda, 0x9d, 0x76, 0xfb, 0x6f, 0x0e, 0xf6, 0x2c, 0xb1, 0x9b, 0x77, 0xb7, 0xe0, 0x75, 0x97, 0xdc, 0x61, 0x91, 0x9f, |     0x99, 0x1b, 0xde, 0x36, 0x57, 0x4b, 0x8f, 0x4b, 0xeb, 0xe0, 0xca, 0xb8, 0xe0, 0x31, 0x8b, 0xb8, 0x1f, 0xa5, 0x94, | ||||||
|     0x11, 0x8a, 0xfc, 0x0c, 0x4b, 0x24, 0x74, 0x85, 0xf6, 0x96, 0x40, 0xd3, 0xb6, 0x58, 0x3a, 0x12, 0x31, 0xbc, 0x19, |     0xf3, 0xe4, 0x18, 0x62, 0xc1, 0xeb, 0xb0, 0x6d, 0xb7, 0x04, 0xc9, 0x63, 0xfc, 0x6a, 0x28, 0x41, 0x7a, 0x1f, 0x0a, | ||||||
|     0xb8, 0x0b, 0x31, 0x7e, 0xd4, 0x6b, 0x0b, 0xbb, 0xb5, 0x70, 0xa5, 0x6d, 0x95, 0xe1, 0xa2, 0x0c, 0x04, 0x9e, 0xaa, |     0xab, 0x44, 0xb0, 0xdd, 0x69, 0xb7, 0xff, 0xe6, 0x60, 0xcf, 0x12, 0xbb, 0x79, 0x77, 0x0b, 0x5e, 0x77, 0xc9, 0xcd, | ||||||
|     0x88, 0x1f, 0xa8, 0x75, 0xa6, 0x92, 0x5d, 0xe4, 0x50, 0x3a, 0x27, 0x75, 0xb5, 0x75, 0xb1, 0x38, 0x9e, 0x81, 0x1c, |     0x16, 0x79, 0x1f, 0xa1, 0xc8, 0xfb, 0xb0, 0x44, 0xa2, 0x58, 0x68, 0x6f, 0x09, 0x34, 0x6d, 0x8b, 0xa5, 0x23, 0x11, | ||||||
|     0x52, 0x09, 0x2a, 0xef, 0x65, 0x87, 0x5d, 0x9a, 0x0a, 0x93, 0x62, 0x57, 0x23, 0x92, 0xd3, 0x4e, 0x7f, 0x37, 0x92, |     0x1b, 0x9c, 0x81, 0x1b, 0x12, 0xe3, 0xc7, 0xc2, 0xb6, 0xb0, 0x5b, 0x0b, 0x57, 0xda, 0x56, 0x99, 0x33, 0xca, 0xf0, | ||||||
|     0xf6, 0x0e, 0xee, 0xdd, 0x02, 0x36, 0x2f, 0xa8, 0x39, 0x34, 0x2a, 0xfc, 0x38, 0xdb, 0x3a, 0x63, 0xc7, 0xad, 0x68, |     0xe0, 0xa9, 0x8a, 0x24, 0x82, 0xb9, 0xc0, 0x54, 0x12, 0x8d, 0x1c, 0x4a, 0xe7, 0xba, 0xae, 0xb6, 0x2e, 0x16, 0xc7, | ||||||
|     0x1e, 0x57, 0xe1, 0x3f, 0xd4, 0x7e, 0xfd, 0x5d, 0xa5, 0x08, 0x65, 0x9a, 0xa5, 0x7c, 0x8c, 0x8c, 0x2c, 0x0e, 0x24, |     0x33, 0x90, 0x43, 0x2a, 0xf1, 0xe5, 0xbd, 0xec, 0xb0, 0x4b, 0x53, 0x61, 0xb2, 0xed, 0x6a, 0xa4, 0x73, 0xda, 0xe9, | ||||||
|     0x1c, 0x31, 0x68, 0x29, 0x63, 0x8b, 0x64, 0x34, 0x02, 0xf1, 0x01, 0x56, 0xe2, 0x5f, 0x15, 0x83, 0x94, 0x9a, 0xa0, |     0xef, 0x46, 0xd2, 0x8e, 0xc2, 0xbd, 0x5b, 0xc0, 0xe6, 0x05, 0xf5, 0x89, 0xc6, 0x8a, 0x1f, 0x67, 0x5b, 0x67, 0xec, | ||||||
|     0xb4, 0xfb, 0x7f, 0xfd, 0x5f, 0xff, 0x5b, 0x86, 0x15, 0x81, 0xac, 0x00, 0x16, 0xa6, 0xc1, 0x54, 0x27, 0x8c, 0xec, |     0xb8, 0x15, 0xcd, 0xe3, 0x2a, 0xac, 0x88, 0x5a, 0xb5, 0xbf, 0xab, 0x14, 0xac, 0x4c, 0xdf, 0x94, 0x8f, 0x91, 0x91, | ||||||
|     0x1c, 0x1c, 0xd1, 0x78, 0xdc, 0x9a, 0x46, 0xc9, 0x04, 0x20, 0x28, 0x98, 0xb8, 0xca, 0x24, 0xeb, 0x81, 0x0b, 0x24, |     0x1d, 0x82, 0x84, 0x23, 0x06, 0x2d, 0x65, 0xcc, 0x92, 0x8c, 0x51, 0x20, 0x3e, 0xc0, 0x4a, 0xfc, 0xab, 0x62, 0x9b, | ||||||
|     0x58, 0xe6, 0xe1, 0xbc, 0x04, 0xaf, 0x5e, 0x84, 0x2b, 0xf6, 0xbb, 0xf2, 0x56, 0x55, 0xbe, 0x30, 0x31, 0xb4, 0x91, |     0x52, 0x13, 0x94, 0x76, 0xff, 0xaf, 0xff, 0xeb, 0x7f, 0xcb, 0x70, 0x25, 0x90, 0x15, 0xc0, 0xc2, 0xf4, 0x9a, 0xea, | ||||||
|     0xc5, 0x6a, 0xf0, 0x5c, 0x2d, 0x93, 0x55, 0xfd, 0x82, 0x24, 0x29, 0x3c, 0x58, 0x2d, 0x8d, 0x15, 0x5a, 0xea, 0x83, |     0xe4, 0x92, 0x9d, 0x83, 0x83, 0x1b, 0x8f, 0x5b, 0xd3, 0x28, 0x99, 0x00, 0x04, 0x05, 0x13, 0xda, 0x50, 0xd6, 0x03, | ||||||
|     0x90, 0x7f, 0xfb, 0xe7, 0xff, 0xfc, 0xdf, 0xd5, 0x2b, 0x9e, 0x6f, 0xfc, 0xf5, 0x9f, 0xfe, 0xe1, 0xff, 0xfe, 0x9f, |     0x17, 0x48, 0xb0, 0xcc, 0x43, 0x7f, 0x09, 0x5e, 0xbd, 0x08, 0x57, 0xec, 0x77, 0xe5, 0xc3, 0xaa, 0x3c, 0x64, 0x62, | ||||||
|     0xff, 0x82, 0x59, 0xc2, 0xf2, 0x0c, 0x84, 0xb6, 0x92, 0x55, 0x1d, 0x80, 0x88, 0x3d, 0x65, 0x55, 0x0e, 0x47, 0x3d, |     0x68, 0x23, 0x3b, 0xd6, 0xe0, 0xb9, 0x5a, 0x86, 0xac, 0xfa, 0xc5, 0x4b, 0x52, 0x78, 0xb0, 0x5a, 0x7a, 0x2c, 0xb4, | ||||||
|     0xdd, 0x75, 0x9f, 0x26, 0x24, 0xde, 0x94, 0xd0, 0x11, 0x5f, 0x53, 0x7a, 0x34, 0x51, 0xed, 0x1a, 0xf2, 0xc1, 0x52, |     0xd4, 0x07, 0x2c, 0xff, 0xf6, 0xcf, 0xff, 0xf9, 0xbf, 0xab, 0x57, 0x3c, 0x37, 0xf9, 0xeb, 0x3f, 0xfd, 0xc3, 0xff, | ||||||
|     0x5a, 0x74, 0xac, 0x6f, 0xef, 0xb4, 0xed, 0x6a, 0x79, 0xfb, 0x46, 0xdf, 0x2d, 0x5c, 0x98, 0x5b, 0x65, 0xe0, 0xf8, |     0xfd, 0x3f, 0xff, 0x05, 0xb3, 0x8f, 0xe5, 0xd9, 0x0a, 0x6d, 0x25, 0xab, 0x3a, 0x58, 0x11, 0x7b, 0xca, 0xaa, 0x1c, | ||||||
|     0x7a, 0xd9, 0x96, 0x2a, 0x8c, 0x85, 0x25, 0x65, 0x55, 0x6e, 0x61, 0x7c, 0x79, 0x89, 0xaf, 0x41, 0xd7, 0x28, 0xa6, |     0x99, 0x7a, 0x1a, 0xed, 0x3e, 0x4d, 0x48, 0xbc, 0x29, 0xa1, 0x23, 0xbe, 0xa6, 0xb4, 0x6b, 0xa2, 0xda, 0x35, 0xe4, | ||||||
|     0x55, 0xae, 0xf5, 0xe9, 0xfd, 0xb2, 0x00, 0x44, 0x27, 0xb8, 0x34, 0x22, 0x58, 0x46, 0x67, 0xa7, 0x2d, 0xb4, 0x4e, |     0x83, 0xa5, 0xb4, 0x28, 0x5d, 0xc0, 0xde, 0x69, 0xdb, 0xd5, 0xf2, 0xf6, 0x8d, 0xbe, 0x5b, 0xb8, 0x30, 0xb7, 0xca, | ||||||
|     0x92, 0x8b, 0x92, 0x46, 0x11, 0xde, 0xcc, 0xfd, 0x47, 0x7f, 0x57, 0xfe, 0x69, 0x86, 0x56, 0x81, 0xe5, 0xcc, 0xa2, |     0xec, 0xf1, 0xf5, 0xb2, 0x2d, 0x55, 0x78, 0x0c, 0x4b, 0xca, 0xaa, 0xdc, 0xc2, 0xb8, 0xf5, 0x12, 0x5f, 0x83, 0xae, | ||||||
|     0x73, 0xe9, 0xe3, 0x3c, 0x68, 0xb7, 0xe7, 0xe7, 0xee, 0xb2, 0x9a, 0xc1, 0xbb, 0x6a, 0x32, 0x0a, 0xb0, 0x99, 0x03, |     0x51, 0x4c, 0xab, 0x5c, 0xeb, 0xd3, 0xfb, 0x65, 0x01, 0x88, 0x4e, 0x70, 0x69, 0x44, 0x10, 0x8e, 0xce, 0x64, 0x5b, | ||||||
|     0xd2, 0xa1, 0xab, 0x8e, 0xe5, 0x81, 0x59, 0xdf, 0xc6, 0xd0, 0x4f, 0x59, 0x7e, 0xb9, 0xa4, 0x70, 0x52, 0xfc, 0x1b, |     0x68, 0xc0, 0x24, 0x17, 0x25, 0x8d, 0x22, 0xbc, 0xa4, 0xfb, 0x8f, 0xfe, 0xae, 0xfc, 0xd3, 0x0c, 0xad, 0x02, 0xcb, | ||||||
|     0x1e, 0x8e, 0xca, 0xc8, 0x1b, 0x94, 0x18, 0x58, 0x2c, 0x8d, 0x5e, 0x5d, 0xd1, 0x6b, 0xda, 0x59, 0xcd, 0x4d, 0x31, |     0x99, 0x45, 0xe7, 0xd2, 0x77, 0x7a, 0xd0, 0x6e, 0xcf, 0xcf, 0xdd, 0x65, 0x35, 0x83, 0x77, 0xd5, 0x64, 0x14, 0xb8, | ||||||
|     0x0f, 0x77, 0xcd, 0x63, 0xd9, 0xfb, 0x78, 0xd0, 0x3a, 0xed, 0x78, 0xd3, 0xee, 0x52, 0x0f, 0xcf, 0x79, 0x36, 0x33, |     0x33, 0x07, 0xa4, 0xc3, 0x5c, 0x1d, 0x23, 0x04, 0x77, 0xa1, 0x8d, 0x21, 0xa5, 0xb2, 0xfc, 0x72, 0x49, 0x61, 0xaa, | ||||||
|     0x4f, 0x73, 0x59, 0xc4, 0x46, 0x6c, 0xa2, 0x22, 0x96, 0xb2, 0x5e, 0x9c, 0xd4, 0x96, 0x5f, 0xe0, 0x76, 0x03, 0xda, |     0xf8, 0x37, 0x3c, 0x74, 0x95, 0x11, 0x3d, 0x28, 0x31, 0xb0, 0x58, 0x1a, 0xbd, 0xba, 0xa2, 0xd7, 0xb4, 0xb3, 0x9a, | ||||||
|     0x66, 0x11, 0x0f, 0x88, 0x69, 0x7b, 0xe6, 0x79, 0x6f, 0x84, 0x27, 0xe9, 0xd9, 0xd2, 0x98, 0xab, 0x27, 0x9a, 0x62, |     0xf3, 0x62, 0x1e, 0x1a, 0x9b, 0xc7, 0xbd, 0xf7, 0xf1, 0x00, 0x77, 0xda, 0xf1, 0xa6, 0xdd, 0xa5, 0x1e, 0x9e, 0xf3, | ||||||
|     0x5c, 0xb0, 0x9e, 0xf7, 0x53, 0xfa, 0xd4, 0xdd, 0x1c, 0x4a, 0x84, 0x15, 0x5e, 0xc8, 0x63, 0xd4, 0x77, 0x35, 0x7f, |     0x6c, 0x66, 0x9e, 0x12, 0xb3, 0x88, 0x8d, 0xd8, 0x44, 0x45, 0x42, 0x65, 0xbd, 0x38, 0x01, 0x2e, 0xbf, 0xc0, 0xed, | ||||||
|     0x5c, 0x8a, 0x62, 0x70, 0x81, 0xd7, 0xd6, 0x0b, 0xb5, 0x28, 0x6a, 0x5f, 0x80, 0xb5, 0x43, 0x60, 0xda, 0xcd, 0x56, |     0x06, 0xb4, 0xcd, 0x22, 0x1e, 0x10, 0xd3, 0xf6, 0xcc, 0x73, 0xe4, 0x08, 0x4f, 0xe8, 0xb3, 0xa5, 0x31, 0x57, 0x4f, | ||||||
|     0x54, 0x88, 0xad, 0xde, 0x85, 0x2f, 0xb4, 0xed, 0x1d, 0xcd, 0xe7, 0xd4, 0xd0, 0x05, 0x6e, 0x24, 0x1b, 0x1a, 0x25, |     0x34, 0xc5, 0x78, 0x63, 0x3d, 0x9f, 0xa8, 0xf4, 0xa9, 0xbb, 0x39, 0x94, 0x08, 0x57, 0xbc, 0x90, 0xc7, 0xb3, 0xef, | ||||||
|     0x05, 0xa5, 0x08, 0x88, 0x13, 0x79, 0xd9, 0x46, 0xb2, 0xad, 0x78, 0x92, 0x67, 0xf5, 0xf4, 0xfb, 0xb6, 0xff, 0x1f, |     0x6a, 0x7e, 0xbe, 0x14, 0xc5, 0xe0, 0x5a, 0xaf, 0xad, 0x17, 0x6a, 0x51, 0xd4, 0xbe, 0x00, 0x6b, 0x87, 0xc0, 0xb4, | ||||||
|     0x22, 0x28, 0x4d, 0x5d, 0x85, 0x7b, 0x00, 0x00}; |     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 web_server | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -81,6 +81,7 @@ class WebServerBase : public Component { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     this->server_ = std::make_shared<AsyncWebServer>(this->port_); |     this->server_ = std::make_shared<AsyncWebServer>(this->port_); | ||||||
|  |     DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); | ||||||
|     this->server_->begin(); |     this->server_->begin(); | ||||||
|  |  | ||||||
|     for (auto *handler : this->handlers_) |     for (auto *handler : this->handlers_) | ||||||
|   | |||||||
| @@ -332,8 +332,7 @@ def manual_ip(config): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def wifi_network(config, static_ip): | def wifi_network(config, ap, static_ip): | ||||||
|     ap = cg.variable(config[CONF_ID], WiFiAP()) |  | ||||||
|     if CONF_SSID in config: |     if CONF_SSID in config: | ||||||
|         cg.add(ap.set_ssid(config[CONF_SSID])) |         cg.add(ap.set_ssid(config[CONF_SSID])) | ||||||
|     if CONF_PASSWORD in config: |     if CONF_PASSWORD in config: | ||||||
| @@ -360,14 +359,21 @@ async def to_code(config): | |||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) |     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)) |         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: |     if CONF_AP in config: | ||||||
|         conf = config[CONF_AP] |         conf = config[CONF_AP] | ||||||
|         ip_config = conf.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) |         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_ap_timeout(conf[CONF_AP_TIMEOUT])) | ||||||
|  |  | ||||||
|     cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) |     cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) | ||||||
|   | |||||||
| @@ -1,129 +1,5 @@ | |||||||
| import esphome.codegen as cg |  | ||||||
| import esphome.config_validation as cv | 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"] | CONFIG_SCHEMA = cv.invalid( | ||||||
| AUTO_LOAD = ["binary_sensor"] |     "This component sould now be used as platform of the Touchscreen component." | ||||||
| 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 |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| 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, |  | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -1,55 +1,3 @@ | |||||||
| import esphome.codegen as cg |  | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import binary_sensor |  | ||||||
|  |  | ||||||
| from . import ( | CONFIG_SCHEMA = cv.invalid("Rename this platform component to Touchscreen.") | ||||||
|     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)) |  | ||||||
|   | |||||||
							
								
								
									
										116
									
								
								esphome/components/xpt2046/touchscreen.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								esphome/components/xpt2046/touchscreen.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||||
| @@ -9,31 +9,38 @@ namespace xpt2046 { | |||||||
|  |  | ||||||
| static const char *const TAG = "xpt2046"; | static const char *const TAG = "xpt2046"; | ||||||
|  |  | ||||||
|  | void XPT2046TouchscreenStore::gpio_intr(XPT2046TouchscreenStore *store) { store->touch = true; } | ||||||
|  |  | ||||||
| void XPT2046Component::setup() { | void XPT2046Component::setup() { | ||||||
|   if (this->irq_pin_ != nullptr) { |   if (this->irq_pin_ != nullptr) { | ||||||
|     // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state |     // 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 |     // 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. |     // need careful masking. A GPIO poll is cheap so we'll just use that. | ||||||
|  |  | ||||||
|     this->irq_pin_->setup();  // INPUT |     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(); |   spi_setup(); | ||||||
|   read_adc_(0xD0);  // ADC powerdown, enable PENIRQ pin |   read_adc_(0xD0);  // ADC powerdown, enable PENIRQ pin | ||||||
| } | } | ||||||
|  |  | ||||||
| void XPT2046Component::loop() { | void XPT2046Component::loop() { | ||||||
|   if (this->irq_pin_ != nullptr) { |   if ((this->irq_pin_ == nullptr) || (!this->store_.touch)) | ||||||
|     // Force immediate update if a falling edge (= touched is seen) Ignore if still active |     return; | ||||||
|     // (that would mean that we missed the release because of a too long update interval) |   this->store_.touch = false; | ||||||
|     bool val = this->irq_pin_->digital_read(); |   check_touch_(); | ||||||
|     if (!val && this->last_irq_ && !this->touched) { |  | ||||||
|       ESP_LOGD(TAG, "Falling penirq edge, forcing update"); |  | ||||||
|       update(); |  | ||||||
|     } |  | ||||||
|     this->last_irq_ = val; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void XPT2046Component::update() { | void XPT2046Component::update() { | ||||||
|  |   if (this->irq_pin_ == nullptr) | ||||||
|  |     check_touch_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void XPT2046Component::check_touch_() { | ||||||
|   int16_t data[6]; |   int16_t data[6]; | ||||||
|   bool touch = false; |   bool touch = false; | ||||||
|   uint32_t now = millis(); |   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). |   // 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 |   // 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(); |     enable(); | ||||||
|  |  | ||||||
|     int16_t z1 = read_adc_(0xB1 /* Z1 */); |     int16_t touch_pressure_1 = read_adc_(0xB1 /* touch_pressure_1 */); | ||||||
|     int16_t z2 = read_adc_(0xC1 /* Z2 */); |     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_); |     touch = (this->z_raw >= this->threshold_); | ||||||
|     if (touch) { |     if (touch) { | ||||||
| @@ -63,64 +70,73 @@ void XPT2046Component::update() { | |||||||
|     data[5] = read_adc_(0x90 /* Y */);  // Last Y touch power down |     data[5] = read_adc_(0x90 /* Y */);  // Last Y touch power down | ||||||
|  |  | ||||||
|     disable(); |     disable(); | ||||||
|   } |  | ||||||
|  |  | ||||||
|     if (touch) { |     if (touch) { | ||||||
|       this->x_raw = best_two_avg(data[0], data[2], data[4]); |       this->x_raw = best_two_avg(data[0], data[2], data[4]); | ||||||
|       this->y_raw = best_two_avg(data[1], data[3], data[5]); |       this->y_raw = best_two_avg(data[1], data[3], data[5]); | ||||||
|   } else { |  | ||||||
|     this->x_raw = this->y_raw = 0; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   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) { |       TouchPoint touchpoint; | ||||||
|     // Normalize raw data according to calibration min and max |  | ||||||
|  |  | ||||||
|     int16_t x_val = normalize(this->x_raw, this->x_raw_min_, this->x_raw_max_); |       touchpoint.x = 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.y = normalize(this->y_raw, this->y_raw_min_, this->y_raw_max_); | ||||||
|  |  | ||||||
|       if (this->swap_x_y_) { |       if (this->swap_x_y_) { | ||||||
|       std::swap(x_val, y_val); |         std::swap(touchpoint.x, touchpoint.y); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (this->invert_x_) { |       if (this->invert_x_) { | ||||||
|       x_val = 0x7fff - x_val; |         touchpoint.x = 0xfff - touchpoint.x; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (this->invert_y_) { |       if (this->invert_y_) { | ||||||
|       y_val = 0x7fff - y_val; |         touchpoint.y = 0xfff - touchpoint.y; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|     x_val = (int16_t)((int) x_val * this->x_dim_ / 0x7fff); |       switch (static_cast<TouchRotation>(this->display_->get_rotation())) { | ||||||
|     y_val = (int16_t)((int) y_val * this->y_dim_ / 0x7fff); |         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; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       touchpoint.x = (int16_t)((int) touchpoint.x * this->display_->get_width() / 0xfff); | ||||||
|  |       touchpoint.y = (int16_t)((int) touchpoint.y * this->display_->get_height() / 0xfff); | ||||||
|  |  | ||||||
|       if (!this->touched || (now - this->last_pos_ms_) >= this->report_millis_) { |       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); |         ESP_LOGV(TAG, "Touching at [%03X, %03X] => [%3d, %3d]", this->x_raw, this->y_raw, touchpoint.x, touchpoint.y); | ||||||
|  |  | ||||||
|       this->x = x_val; |         this->defer([this, touchpoint]() { this->send_touch_(touchpoint); }); | ||||||
|       this->y = y_val; |  | ||||||
|  |         this->x = touchpoint.x; | ||||||
|  |         this->y = touchpoint.y; | ||||||
|         this->touched = true; |         this->touched = true; | ||||||
|         this->last_pos_ms_ = now; |         this->last_pos_ms_ = now; | ||||||
|  |  | ||||||
|       this->on_state_trigger_->process(this->x, this->y, true); |  | ||||||
|       for (auto *button : this->buttons_) |  | ||||||
|         button->touch(this->x, this->y); |  | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|  |       this->x_raw = this->y_raw = 0; | ||||||
|       if (this->touched) { |       if (this->touched) { | ||||||
|       ESP_LOGD(TAG, "Released [%d, %d]", this->x, this->y); |         ESP_LOGV(TAG, "Released [%d, %d]", this->x, this->y); | ||||||
|  |  | ||||||
|         this->touched = false; |         this->touched = false; | ||||||
|  |         for (auto *listener : this->touch_listeners_) | ||||||
|       this->on_state_trigger_->process(this->x, this->y, false); |           listener->release(); | ||||||
|       for (auto *button : this->buttons_) |       } | ||||||
|         button->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_min_ = std::min(x_min, x_max); | ||||||
|   this->x_raw_max_ = std::max(x_min, x_max); |   this->x_raw_max_ = std::max(x_min, x_max); | ||||||
|   this->y_raw_min_ = std::min(y_min, y_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, "  X max: %d", this->x_raw_max_); | ||||||
|   ESP_LOGCONFIG(TAG, "  Y min: %d", this->y_raw_min_); |   ESP_LOGCONFIG(TAG, "  Y min: %d", this->y_raw_min_); | ||||||
|   ESP_LOGCONFIG(TAG, "  Y max: %d", this->y_raw_max_); |   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_); |   ESP_LOGCONFIG(TAG, "  Swap X/Y: %s", YESNO(this->swap_x_y_)); | ||||||
|   if (this->swap_x_y_) { |   ESP_LOGCONFIG(TAG, "  Invert X: %s", YESNO(this->invert_x_)); | ||||||
|     ESP_LOGCONFIG(TAG, "  Swap X/Y"); |   ESP_LOGCONFIG(TAG, "  Invert Y: %s", YESNO(this->invert_y_)); | ||||||
|   } |  | ||||||
|   ESP_LOGCONFIG(TAG, "  threshold: %d", this->threshold_); |   ESP_LOGCONFIG(TAG, "  threshold: %d", this->threshold_); | ||||||
|   ESP_LOGCONFIG(TAG, "  Report interval: %u", this->report_millis_); |   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; } | 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 XPT2046Component::best_two_avg(int16_t x, int16_t y, int16_t z) {  // NOLINT | ||||||
|   int16_t da, db, dc; |   int16_t da, db, dc;                                                      // NOLINT | ||||||
|   int16_t reta = 0; |   int16_t reta = 0; | ||||||
|  |  | ||||||
|   da = (x > y) ? x - y : y - x; |   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) { |   if (val <= min_val) { | ||||||
|     ret = 0; |     ret = 0; | ||||||
|   } else if (val >= max_val) { |   } else if (val >= max_val) { | ||||||
|     ret = 0x7fff; |     ret = 0xfff; | ||||||
|   } else { |   } 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; |   return ret; | ||||||
| } | } | ||||||
|  |  | ||||||
| int16_t XPT2046Component::read_adc_(uint8_t ctrl) { | int16_t XPT2046Component::read_adc_(uint8_t ctrl) {  // NOLINT | ||||||
|   uint8_t data[2]; |   uint8_t data[2]; | ||||||
|  |  | ||||||
|   write_byte(ctrl); |   write_byte(ctrl); | ||||||
| @@ -193,25 +209,5 @@ int16_t XPT2046Component::read_adc_(uint8_t ctrl) { | |||||||
|   return ((data[0] << 8) | data[1]) >> 3; |   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 xpt2046 | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -3,42 +3,31 @@ | |||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/automation.h" | #include "esphome/core/automation.h" | ||||||
| #include "esphome/components/spi/spi.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 esphome { | ||||||
| namespace xpt2046 { | namespace xpt2046 { | ||||||
|  |  | ||||||
| class XPT2046OnStateTrigger : public Trigger<int, int, bool> { | using namespace touchscreen; | ||||||
|  public: |  | ||||||
|   void process(int x, int y, bool touched); | struct XPT2046TouchscreenStore { | ||||||
|  |   volatile bool touch; | ||||||
|  |   ISRInternalGPIOPin pin; | ||||||
|  |  | ||||||
|  |   static void gpio_intr(XPT2046TouchscreenStore *store); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class XPT2046Button : public binary_sensor::BinarySensor { | class XPT2046Component : public Touchscreen, | ||||||
|  public: |                          public PollingComponent, | ||||||
|   /// 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, |  | ||||||
|                          public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, |                          public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, | ||||||
|                                                spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> { |                                                spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> { | ||||||
|  public: |  public: | ||||||
|   /// Set the logical touch screen dimensions. |   /// Set the logical touch screen dimensions. | ||||||
|   void set_dimensions(int16_t x, int16_t y) { |   void set_dimensions(int16_t x, int16_t y) { | ||||||
|     this->x_dim_ = x; |     this->display_width_ = x; | ||||||
|     this->y_dim_ = y; |     this->display_height_ = y; | ||||||
|   } |   } | ||||||
|   /// Set the coordinates for the touch screen edges. |   /// 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); |   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. |   /// Set the interval to report the touch point perodically. | ||||||
|   void set_report_interval(uint32_t interval) { this->report_millis_ = interval; } |   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. |   /// Set the threshold for the touch detection. | ||||||
|   void set_threshold(int16_t threshold) { this->threshold_ = threshold; } |   void set_threshold(int16_t threshold) { this->threshold_ = threshold; } | ||||||
|   /// Set the pin used to detect the touch. |   /// Set the pin used to detect the touch. | ||||||
|   void set_irq_pin(GPIOPin *pin) { this->irq_pin_ = pin; } |   void set_irq_pin(InternalGPIOPin *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 setup() override; |   void setup() override; | ||||||
|   void dump_config() 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); |   static int16_t normalize(int16_t val, int16_t min_val, int16_t max_val); | ||||||
|  |  | ||||||
|   int16_t read_adc_(uint8_t ctrl); |   int16_t read_adc_(uint8_t ctrl); | ||||||
|  |   void check_touch_(); | ||||||
|  |  | ||||||
|   int16_t threshold_; |   int16_t threshold_; | ||||||
|   int16_t x_raw_min_, x_raw_max_, y_raw_min_, y_raw_max_; |   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 invert_x_, invert_y_; | ||||||
|   bool swap_x_y_; |   bool swap_x_y_; | ||||||
|  |  | ||||||
|   uint32_t report_millis_; |   uint32_t report_millis_; | ||||||
|   uint32_t last_pos_ms_{0}; |   uint32_t last_pos_ms_{0}; | ||||||
|  |  | ||||||
|   GPIOPin *irq_pin_{nullptr}; |   InternalGPIOPin *irq_pin_{nullptr}; | ||||||
|   bool last_irq_{true}; |   XPT2046TouchscreenStore store_; | ||||||
|  |  | ||||||
|   XPT2046OnStateTrigger *on_state_trigger_{new XPT2046OnStateTrigger()}; |  | ||||||
|   std::vector<XPT2046Button *> buttons_{}; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace xpt2046 | }  // namespace xpt2046 | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ from esphome.core import CORE, EsphomeError | |||||||
| from esphome.helpers import indent | from esphome.helpers import indent | ||||||
| from esphome.util import safe_print, OrderedDict | 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.loader import get_component, get_platform, ComponentManifest | ||||||
| from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue | from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue | ||||||
| from esphome.voluptuous_schema import ExtraKeysInvalid | from esphome.voluptuous_schema import ExtraKeysInvalid | ||||||
| @@ -50,10 +50,10 @@ def iter_components(config): | |||||||
|                 yield p_name, platform, p_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): |     if len(path) < len(other): | ||||||
|         return False |         return False | ||||||
|     return path[: len(other)] == other |     return path[: len(other)] == other | ||||||
| @@ -67,7 +67,7 @@ class _ValidationStepTask: | |||||||
|         self.step = step |         self.step = step | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def _cmp_tuple(self) -> Tuple[float, int]: |     def _cmp_tuple(self) -> tuple[float, int]: | ||||||
|         return (-self.priority, self.id_number) |         return (-self.priority, self.id_number) | ||||||
|  |  | ||||||
|     def __eq__(self, other): |     def __eq__(self, other): | ||||||
| @@ -84,21 +84,20 @@ class Config(OrderedDict, fv.FinalValidateConfig): | |||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         super().__init__() |         super().__init__() | ||||||
|         # A list of voluptuous errors |         # 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 |         # A list of paths that should be fully outputted | ||||||
|         # The values will be the paths to all "domain", for example (['logger'], 'logger') |         # The values will be the paths to all "domain", for example (['logger'], 'logger') | ||||||
|         # or (['sensor', 'ultrasonic'], 'sensor.ultrasonic') |         # 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 |         # 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 = {} |         self._data = {} | ||||||
|         # Store pending validation tasks (in heap order) |         # 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 |         # ID to ensure stable order for keys with equal priority | ||||||
|         self._validation_tasks_id = 0 |         self._validation_tasks_id = 0 | ||||||
|  |  | ||||||
|     def add_error(self, error): |     def add_error(self, error: vol.Invalid) -> None: | ||||||
|         # type: (vol.Invalid) -> None |  | ||||||
|         if isinstance(error, vol.MultipleInvalid): |         if isinstance(error, vol.MultipleInvalid): | ||||||
|             for err in error.errors: |             for err in error.errors: | ||||||
|                 self.add_error(err) |                 self.add_error(err) | ||||||
| @@ -132,20 +131,16 @@ class Config(OrderedDict, fv.FinalValidateConfig): | |||||||
|             e.prepend(path) |             e.prepend(path) | ||||||
|             self.add_error(e) |             self.add_error(e) | ||||||
|  |  | ||||||
|     def add_str_error(self, message, path): |     def add_str_error(self, message: str, path: ConfigPath) -> None: | ||||||
|         # type: (str, ConfigPath) -> None |  | ||||||
|         self.add_error(vol.Invalid(message, path)) |         self.add_error(vol.Invalid(message, path)) | ||||||
|  |  | ||||||
|     def add_output_path(self, path, domain): |     def add_output_path(self, path: ConfigPath, domain: str) -> None: | ||||||
|         # type: (ConfigPath, str) -> None |  | ||||||
|         self.output_paths.append((path, domain)) |         self.output_paths.append((path, domain)) | ||||||
|  |  | ||||||
|     def remove_output_path(self, path, domain): |     def remove_output_path(self, path: ConfigPath, domain: str) -> None: | ||||||
|         # type: (ConfigPath, str) -> None |  | ||||||
|         self.output_paths.remove((path, domain)) |         self.output_paths.remove((path, domain)) | ||||||
|  |  | ||||||
|     def is_in_error_path(self, path): |     def is_in_error_path(self, path: ConfigPath) -> bool: | ||||||
|         # type: (ConfigPath) -> bool |  | ||||||
|         for err in self.errors: |         for err in self.errors: | ||||||
|             if _path_begins_with(err.path, path): |             if _path_begins_with(err.path, path): | ||||||
|                 return True |                 return True | ||||||
| @@ -157,16 +152,16 @@ class Config(OrderedDict, fv.FinalValidateConfig): | |||||||
|             conf = conf[key] |             conf = conf[key] | ||||||
|         conf[path[-1]] = value |         conf[path[-1]] = value | ||||||
|  |  | ||||||
|     def get_error_for_path(self, path): |     def get_error_for_path(self, path: ConfigPath) -> Optional[vol.Invalid]: | ||||||
|         # type: (ConfigPath) -> Optional[vol.Invalid] |  | ||||||
|         for err in self.errors: |         for err in self.errors: | ||||||
|             if self.get_deepest_path(err.path) == path: |             if self.get_deepest_path(err.path) == path: | ||||||
|                 self.errors.remove(err) |                 self.errors.remove(err) | ||||||
|                 return err |                 return err | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
|     def get_deepest_document_range_for_path(self, path, get_key=False): |     def get_deepest_document_range_for_path( | ||||||
|         # type: (ConfigPath, bool) -> Optional[ESPHomeDataBase] |         self, path: ConfigPath, get_key: bool = False | ||||||
|  |     ) -> Optional[ESPHomeDataBase]: | ||||||
|         data = self |         data = self | ||||||
|         doc_range = None |         doc_range = None | ||||||
|         for index, path_item in enumerate(path): |         for index, path_item in enumerate(path): | ||||||
| @@ -207,8 +202,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): | |||||||
|                 return {} |                 return {} | ||||||
|         return data |         return data | ||||||
|  |  | ||||||
|     def get_deepest_path(self, path): |     def get_deepest_path(self, path: ConfigPath) -> ConfigPath: | ||||||
|         # type: (ConfigPath) -> ConfigPath |  | ||||||
|         """Return the path that is the deepest reachable by following path.""" |         """Return the path that is the deepest reachable by following path.""" | ||||||
|         data = self |         data = self | ||||||
|         part = [] |         part = [] | ||||||
| @@ -532,7 +526,7 @@ class IDPassValidationStep(ConfigValidationStep): | |||||||
|             # because the component that did not validate doesn't have any IDs set |             # because the component that did not validate doesn't have any IDs set | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         searching_ids = []  # type: List[Tuple[core.ID, ConfigPath]] |         searching_ids: list[tuple[core.ID, ConfigPath]] = [] | ||||||
|         for id, path in iter_ids(result): |         for id, path in iter_ids(result): | ||||||
|             if id.is_declaration: |             if id.is_declaration: | ||||||
|                 if id.id is not None: |                 if id.id is not None: | ||||||
| @@ -780,8 +774,7 @@ def _get_parent_name(path, config): | |||||||
|     return path[-1] |     return path[-1] | ||||||
|  |  | ||||||
|  |  | ||||||
| def _format_vol_invalid(ex, config): | def _format_vol_invalid(ex: vol.Invalid, config: Config) -> str: | ||||||
|     # type: (vol.Invalid, Config) -> str |  | ||||||
|     message = "" |     message = "" | ||||||
|  |  | ||||||
|     paren = _get_parent_name(ex.path[:-1], config) |     paren = _get_parent_name(ex.path[:-1], config) | ||||||
| @@ -862,8 +855,9 @@ def _print_on_next_line(obj): | |||||||
|     return False |     return False | ||||||
|  |  | ||||||
|  |  | ||||||
| def dump_dict(config, path, at_root=True): | def dump_dict( | ||||||
|     # type: (Config, ConfigPath, bool) -> Tuple[str, bool] |     config: Config, path: ConfigPath, at_root: bool = True | ||||||
|  | ) -> tuple[str, bool]: | ||||||
|     conf = config.get_nested_item(path) |     conf = config.get_nested_item(path) | ||||||
|     ret = "" |     ret = "" | ||||||
|     multiline = False |     multiline = False | ||||||
|   | |||||||
| @@ -5,8 +5,7 @@ from esphome.core import CORE | |||||||
| from esphome.helpers import read_file | from esphome.helpers import read_file | ||||||
|  |  | ||||||
|  |  | ||||||
| def read_config_file(path): | def read_config_file(path: str) -> str: | ||||||
|     # type: (str) -> str |  | ||||||
|     if CORE.vscode and ( |     if CORE.vscode and ( | ||||||
|         not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path) |         not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path) | ||||||
|     ): |     ): | ||||||
|   | |||||||
| @@ -1689,7 +1689,7 @@ class Version: | |||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def parse(cls, value: str) -> "Version": |     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: |         if match is None: | ||||||
|             raise ValueError(f"Not a valid version number {value}") |             raise ValueError(f"Not a valid version number {value}") | ||||||
|         major = int(match[1]) |         major = int(match[1]) | ||||||
| @@ -1703,7 +1703,7 @@ def version_number(value): | |||||||
|     try: |     try: | ||||||
|         return str(Version.parse(value)) |         return str(Version.parse(value)) | ||||||
|     except ValueError as e: |     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): | def platformio_version_constraint(value): | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| """Constants used by esphome.""" | """Constants used by esphome.""" | ||||||
|  |  | ||||||
| __version__ = "2022.9.4" | __version__ = "2022.10.0b2" | ||||||
|  |  | ||||||
| ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" | ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" | ||||||
|  |  | ||||||
| @@ -396,6 +396,7 @@ CONF_MIN_POWER = "min_power" | |||||||
| CONF_MIN_RANGE = "min_range" | CONF_MIN_RANGE = "min_range" | ||||||
| CONF_MIN_TEMPERATURE = "min_temperature" | CONF_MIN_TEMPERATURE = "min_temperature" | ||||||
| CONF_MIN_VALUE = "min_value" | CONF_MIN_VALUE = "min_value" | ||||||
|  | CONF_MIN_VERSION = "min_version" | ||||||
| CONF_MINUTE = "minute" | CONF_MINUTE = "minute" | ||||||
| CONF_MINUTES = "minutes" | CONF_MINUTES = "minutes" | ||||||
| CONF_MISO_PIN = "miso_pin" | CONF_MISO_PIN = "miso_pin" | ||||||
| @@ -904,7 +905,6 @@ DEVICE_CLASS_GARAGE_DOOR = "garage_door" | |||||||
| DEVICE_CLASS_HEAT = "heat" | DEVICE_CLASS_HEAT = "heat" | ||||||
| DEVICE_CLASS_LIGHT = "light" | DEVICE_CLASS_LIGHT = "light" | ||||||
| DEVICE_CLASS_LOCK = "lock" | DEVICE_CLASS_LOCK = "lock" | ||||||
| DEVICE_CLASS_MOISTURE = "moisture" |  | ||||||
| DEVICE_CLASS_MOTION = "motion" | DEVICE_CLASS_MOTION = "motion" | ||||||
| DEVICE_CLASS_MOVING = "moving" | DEVICE_CLASS_MOVING = "moving" | ||||||
| DEVICE_CLASS_OCCUPANCY = "occupancy" | DEVICE_CLASS_OCCUPANCY = "occupancy" | ||||||
| @@ -922,15 +922,17 @@ DEVICE_CLASS_WINDOW = "window" | |||||||
| # device classes of both binary_sensor and sensor component | # device classes of both binary_sensor and sensor component | ||||||
| DEVICE_CLASS_EMPTY = "" | DEVICE_CLASS_EMPTY = "" | ||||||
| DEVICE_CLASS_BATTERY = "battery" | DEVICE_CLASS_BATTERY = "battery" | ||||||
|  | DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" | ||||||
| DEVICE_CLASS_GAS = "gas" | DEVICE_CLASS_GAS = "gas" | ||||||
|  | DEVICE_CLASS_MOISTURE = "moisture" | ||||||
| DEVICE_CLASS_POWER = "power" | DEVICE_CLASS_POWER = "power" | ||||||
| # device classes of sensor component | # device classes of sensor component | ||||||
| DEVICE_CLASS_APPARENT_POWER = "apparent_power" | DEVICE_CLASS_APPARENT_POWER = "apparent_power" | ||||||
| DEVICE_CLASS_AQI = "aqi" | DEVICE_CLASS_AQI = "aqi" | ||||||
| DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" | DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" | ||||||
| DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" |  | ||||||
| DEVICE_CLASS_CURRENT = "current" | DEVICE_CLASS_CURRENT = "current" | ||||||
| DEVICE_CLASS_DATE = "date" | DEVICE_CLASS_DATE = "date" | ||||||
|  | DEVICE_CLASS_DISTANCE = "distance" | ||||||
| DEVICE_CLASS_DURATION = "duration" | DEVICE_CLASS_DURATION = "duration" | ||||||
| DEVICE_CLASS_ENERGY = "energy" | DEVICE_CLASS_ENERGY = "energy" | ||||||
| DEVICE_CLASS_FREQUENCY = "frequency" | DEVICE_CLASS_FREQUENCY = "frequency" | ||||||
| @@ -948,11 +950,14 @@ DEVICE_CLASS_POWER_FACTOR = "power_factor" | |||||||
| DEVICE_CLASS_PRESSURE = "pressure" | DEVICE_CLASS_PRESSURE = "pressure" | ||||||
| DEVICE_CLASS_REACTIVE_POWER = "reactive_power" | DEVICE_CLASS_REACTIVE_POWER = "reactive_power" | ||||||
| DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" | DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" | ||||||
|  | DEVICE_CLASS_SPEED = "speed" | ||||||
| DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide" | DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide" | ||||||
| DEVICE_CLASS_TEMPERATURE = "temperature" | DEVICE_CLASS_TEMPERATURE = "temperature" | ||||||
| DEVICE_CLASS_TIMESTAMP = "timestamp" | DEVICE_CLASS_TIMESTAMP = "timestamp" | ||||||
| DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" | DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" | ||||||
| DEVICE_CLASS_VOLTAGE = "voltage" | DEVICE_CLASS_VOLTAGE = "voltage" | ||||||
|  | DEVICE_CLASS_VOLUME = "volume" | ||||||
|  | DEVICE_CLASS_WEIGHT = "weight" | ||||||
| # device classes of both binary_sensor and button component | # device classes of both binary_sensor and button component | ||||||
| DEVICE_CLASS_UPDATE = "update" | DEVICE_CLASS_UPDATE = "update" | ||||||
| # device classes of button component | # device classes of button component | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import logging | |||||||
| import math | import math | ||||||
| import os | import os | ||||||
| import re | import re | ||||||
| from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union | from typing import TYPE_CHECKING, Optional, Union | ||||||
|  |  | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_COMMENT, |     CONF_COMMENT, | ||||||
| @@ -469,19 +469,19 @@ class EsphomeCore: | |||||||
|         # Task counter for pending tasks |         # Task counter for pending tasks | ||||||
|         self.task_counter = 0 |         self.task_counter = 0 | ||||||
|         # The variable cache, for each ID this holds a MockObj of the variable obj |         # 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 |         # 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) |         # 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 |         # 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 |         # 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 |         # 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 |         # 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 |         # A set of strings of names of loaded integrations, used to find namespace ID conflicts | ||||||
|         self.loaded_integrations = set() |         self.loaded_integrations = set() | ||||||
|         # A set of component IDs to track what Component subclasses are declared |         # A set of component IDs to track what Component subclasses are declared | ||||||
| @@ -701,7 +701,7 @@ class EsphomeCore: | |||||||
|         _LOGGER.debug("Adding define: %s", define) |         _LOGGER.debug("Adding define: %s", define) | ||||||
|         return 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 |         new_val = value | ||||||
|         old_val = self.platformio_options.get(key) |         old_val = self.platformio_options.get(key) | ||||||
|         if isinstance(old_val, list): |         if isinstance(old_val, list): | ||||||
| @@ -734,7 +734,7 @@ class EsphomeCore: | |||||||
|             _LOGGER.debug("Waiting for variable %s", id) |             _LOGGER.debug("Waiting for variable %s", id) | ||||||
|             yield |             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): |         if not isinstance(id, ID): | ||||||
|             raise ValueError(f"ID {id!r} must be of type ID!") |             raise ValueError(f"ID {id!r} must be of type ID!") | ||||||
|         return await _FakeAwaitable(self._get_variable_with_full_id_generator(id)) |         return await _FakeAwaitable(self._get_variable_with_full_id_generator(id)) | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ from esphome.const import ( | |||||||
|     CONF_FRAMEWORK, |     CONF_FRAMEWORK, | ||||||
|     CONF_INCLUDES, |     CONF_INCLUDES, | ||||||
|     CONF_LIBRARIES, |     CONF_LIBRARIES, | ||||||
|  |     CONF_MIN_VERSION, | ||||||
|     CONF_NAME, |     CONF_NAME, | ||||||
|     CONF_ON_BOOT, |     CONF_ON_BOOT, | ||||||
|     CONF_ON_LOOP, |     CONF_ON_LOOP, | ||||||
| @@ -30,6 +31,7 @@ from esphome.const import ( | |||||||
|     KEY_CORE, |     KEY_CORE, | ||||||
|     TARGET_PLATFORMS, |     TARGET_PLATFORMS, | ||||||
|     PLATFORM_ESP8266, |     PLATFORM_ESP8266, | ||||||
|  |     __version__ as ESPHOME_VERSION, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, coroutine_with_priority | ||||||
| from esphome.helpers import copy_file_if_changed, walk_files | from esphome.helpers import copy_file_if_changed, walk_files | ||||||
| @@ -96,6 +98,16 @@ def valid_project_name(value: str): | |||||||
|     return value |     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" | CONF_ESP8266_RESTORE_FROM_FLASH = "esp8266_restore_from_flash" | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = cv.All( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
| @@ -136,6 +148,9 @@ CONFIG_SCHEMA = cv.All( | |||||||
|                     cv.Required(CONF_VERSION): cv.string_strict, |                     cv.Required(CONF_VERSION): cv.string_strict, | ||||||
|                 } |                 } | ||||||
|             ), |             ), | ||||||
|  |             cv.Optional(CONF_MIN_VERSION, default=ESPHOME_VERSION): cv.All( | ||||||
|  |                 cv.version_number, validate_version | ||||||
|  |             ), | ||||||
|         } |         } | ||||||
|     ), |     ), | ||||||
|     validate_hostname, |     validate_hostname, | ||||||
|   | |||||||
| @@ -48,7 +48,8 @@ import heapq | |||||||
| import inspect | import inspect | ||||||
| import logging | import logging | ||||||
| import types | 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__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -177,7 +178,7 @@ class _Task: | |||||||
|         return _Task(priority, self.id_number, self.iterator, self.original_function) |         return _Task(priority, self.id_number, self.iterator, self.original_function) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def _cmp_tuple(self) -> Tuple[float, int]: |     def _cmp_tuple(self) -> tuple[float, int]: | ||||||
|         return (-self.priority, self.id_number) |         return (-self.priority, self.id_number) | ||||||
|  |  | ||||||
|     def __eq__(self, other): |     def __eq__(self, other): | ||||||
| @@ -194,7 +195,7 @@ class FakeEventLoop: | |||||||
|     """Emulate an asyncio EventLoop to run some registered coroutine jobs in sequence.""" |     """Emulate an asyncio EventLoop to run some registered coroutine jobs in sequence.""" | ||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self._pending_tasks: List[_Task] = [] |         self._pending_tasks: list[_Task] = [] | ||||||
|         self._task_counter = 0 |         self._task_counter = 0 | ||||||
|  |  | ||||||
|     def add_job(self, func, *args, **kwargs): |     def add_job(self, func, *args, **kwargs): | ||||||
|   | |||||||
| @@ -5,7 +5,13 @@ import re | |||||||
| from esphome.yaml_util import ESPHomeDataBase | from esphome.yaml_util import ESPHomeDataBase | ||||||
|  |  | ||||||
| # pylint: disable=unused-import, wrong-import-order | # 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 | from esphome.core import (  # noqa | ||||||
|     CORE, |     CORE, | ||||||
| @@ -44,9 +50,9 @@ SafeExpType = Union[ | |||||||
|     int, |     int, | ||||||
|     float, |     float, | ||||||
|     TimePeriod, |     TimePeriod, | ||||||
|     Type[bool], |     type[bool], | ||||||
|     Type[int], |     type[int], | ||||||
|     Type[float], |     type[float], | ||||||
|     Sequence[Any], |     Sequence[Any], | ||||||
| ] | ] | ||||||
|  |  | ||||||
| @@ -140,7 +146,7 @@ class CallExpression(Expression): | |||||||
| class StructInitializer(Expression): | class StructInitializer(Expression): | ||||||
|     __slots__ = ("base", "args") |     __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 |         self.base = base | ||||||
|         # TODO: args is always a Tuple, is this check required? |         # TODO: args is always a Tuple, is this check required? | ||||||
|         if not isinstance(args, OrderedDict): |         if not isinstance(args, OrderedDict): | ||||||
| @@ -200,7 +206,7 @@ class ParameterListExpression(Expression): | |||||||
|     __slots__ = ("parameters",) |     __slots__ = ("parameters",) | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, *parameters: Union[ParameterExpression, Tuple[SafeExpType, str]] |         self, *parameters: Union[ParameterExpression, tuple[SafeExpType, str]] | ||||||
|     ): |     ): | ||||||
|         self.parameters = [] |         self.parameters = [] | ||||||
|         for parameter in parameters: |         for parameter in parameters: | ||||||
| @@ -468,7 +474,9 @@ def statement(expression: Union[Expression, Statement]) -> Statement: | |||||||
|     return ExpressionStatement(expression) |     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. |     """Declare a new variable, not pointer type, in the code generation. | ||||||
|  |  | ||||||
|     :param id_: The ID used to declare the variable. |     :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_ |         id_.type = type_ | ||||||
|     assignment = AssignmentExpression(id_.type, "", id_, rhs) |     assignment = AssignmentExpression(id_.type, "", id_, rhs) | ||||||
|     CORE.add(assignment) |     CORE.add(assignment) | ||||||
|  |     if register: | ||||||
|         CORE.register_variable(id_, obj) |         CORE.register_variable(id_, obj) | ||||||
|     return 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": | def new_variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": | ||||||
|     """Declare and define a new variable, not pointer type, in the code generation. |     """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))) |         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) |     CORE.add_platformio_option(key, value) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -607,7 +642,7 @@ async def get_variable(id_: ID) -> "MockObj": | |||||||
|     return await CORE.get_variable(id_) |     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 |     Wait for the given ID to be defined in the code generation and | ||||||
|     return it as a MockObj. |     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( | async def process_lambda( | ||||||
|     value: Lambda, |     value: Lambda, | ||||||
|     parameters: List[Tuple[SafeExpType, str]], |     parameters: list[tuple[SafeExpType, str]], | ||||||
|     capture: str = "=", |     capture: str = "=", | ||||||
|     return_type: SafeExpType = None, |     return_type: SafeExpType = None, | ||||||
| ) -> Generator[LambdaExpression, None, None]: | ) -> Generator[LambdaExpression, None, None]: | ||||||
| @@ -676,7 +711,7 @@ def is_template(value): | |||||||
|  |  | ||||||
| async def templatable( | async def templatable( | ||||||
|     value: Any, |     value: Any, | ||||||
|     args: List[Tuple[SafeExpType, str]], |     args: list[tuple[SafeExpType, str]], | ||||||
|     output_type: Optional[SafeExpType], |     output_type: Optional[SafeExpType], | ||||||
|     to_exp: Any = None, |     to_exp: Any = None, | ||||||
| ): | ): | ||||||
| @@ -724,7 +759,7 @@ class MockObj(Expression): | |||||||
|             attr = attr[1:] |             attr = attr[1:] | ||||||
|         return MockObj(f"{self.base}{self.op}{attr}", next_op) |         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) |         call = CallExpression(self.base, *args) | ||||||
|         return MockObj(call, self.op) |         return MockObj(call, self.op) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ from esphome.const import ( | |||||||
|  |  | ||||||
| # pylint: disable=unused-import | # pylint: disable=unused-import | ||||||
| from esphome.core import coroutine, ID, CORE | 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_generator import add, get_variable | ||||||
| from esphome.cpp_types import App | from esphome.cpp_types import App | ||||||
| from esphome.util import Registry, RegistryEntry | 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])) |         add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) | ||||||
|  |  | ||||||
|  |  | ||||||
| def extract_registry_entry_config(registry, full_config): | def extract_registry_entry_config( | ||||||
|     # type: (Registry, ConfigType) -> RegistryEntry |     registry: Registry, | ||||||
|  |     full_config: ConfigType, | ||||||
|  | ) -> tuple[RegistryEntry, ConfigFragmentType]: | ||||||
|     key, config = next((k, v) for k, v in full_config.items() if k in registry) |     key, config = next((k, v) for k, v in full_config.items() if k in registry) | ||||||
|     return registry[key], config |     return registry[key], config | ||||||
|  |  | ||||||
|   | |||||||
| @@ -533,7 +533,7 @@ class DashboardEntry: | |||||||
|         return os.path.basename(self.path) |         return os.path.basename(self.path) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def storage(self):  # type: () -> Optional[StorageJSON] |     def storage(self) -> Optional[StorageJSON]: | ||||||
|         if not self._loaded_storage: |         if not self._loaded_storage: | ||||||
|             self._storage = StorageJSON.load( |             self._storage = StorageJSON.load( | ||||||
|                 ext_storage_path(settings.config_dir, self.filename) |                 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) |         shutil.move(os.path.join(trash_path, configuration), config_file) | ||||||
|  |  | ||||||
|  |  | ||||||
| PING_RESULT = {}  # type: dict | PING_RESULT: dict = {} | ||||||
| IMPORT_RESULT = {} | IMPORT_RESULT = {} | ||||||
| STOP_EVENT = threading.Event() | STOP_EVENT = threading.Event() | ||||||
| PING_REQUEST = 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) |     return os.path.join(get_base_frontend_path(), "static", *args) | ||||||
|  |  | ||||||
|  |  | ||||||
| @functools.lru_cache(maxsize=None) | @functools.cache | ||||||
| def get_static_file_url(name): | def get_static_file_url(name): | ||||||
|     base = f"./static/{name}" |     base = f"./static/{name}" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| from abc import ABC, abstractmethod | from abc import ABC, abstractmethod | ||||||
| from typing import Dict, Any | from typing import Any | ||||||
| import contextvars | import contextvars | ||||||
|  |  | ||||||
| from esphome.types import ConfigFragmentType, ID, ConfigPathType | from esphome.types import ConfigFragmentType, ID, ConfigPathType | ||||||
| @@ -9,7 +9,7 @@ import esphome.config_validation as cv | |||||||
| class FinalValidateConfig(ABC): | class FinalValidateConfig(ABC): | ||||||
|     @property |     @property | ||||||
|     @abstractmethod |     @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 |         """A dictionary that can be used by post validation functions to store | ||||||
|         global data during the validation phase. Each component should store its |         global data during the validation phase. Each component should store its | ||||||
|         data under a unique key |         data under a unique key | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ from pathlib import Path | |||||||
| import subprocess | import subprocess | ||||||
| import hashlib | import hashlib | ||||||
| import logging | import logging | ||||||
|  | from typing import Callable, Optional | ||||||
| import urllib.parse | import urllib.parse | ||||||
|  |  | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| @@ -12,7 +13,7 @@ import esphome.config_validation as cv | |||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| def run_git_command(cmd, cwd=None): | def run_git_command(cmd, cwd=None) -> str: | ||||||
|     try: |     try: | ||||||
|         ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) |         ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) | ||||||
|     except FileNotFoundError as err: |     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(lines[-1][len("fatal: ") :]) | ||||||
|         raise cv.Invalid(err_str) |         raise cv.Invalid(err_str) | ||||||
|  |  | ||||||
|  |     return ret.stdout.decode("utf-8").strip() | ||||||
|  |  | ||||||
|  |  | ||||||
| def _compute_destination_path(key: str, domain: str) -> Path: | def _compute_destination_path(key: str, domain: str) -> Path: | ||||||
|     base_dir = Path(CORE.config_dir) / ".esphome" / domain |     base_dir = Path(CORE.config_dir) / ".esphome" / domain | ||||||
| @@ -44,7 +47,7 @@ def clone_or_update( | |||||||
|     domain: str, |     domain: str, | ||||||
|     username: str = None, |     username: str = None, | ||||||
|     password: str = None, |     password: str = None, | ||||||
| ) -> Path: | ) -> tuple[Path, Optional[Callable[[], None]]]: | ||||||
|     key = f"{url}@{ref}" |     key = f"{url}@{ref}" | ||||||
|  |  | ||||||
|     if username is not None and password is not None: |     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") |             file_timestamp = Path(repo_dir / ".git" / "HEAD") | ||||||
|         age = datetime.now() - datetime.fromtimestamp(file_timestamp.stat().st_mtime) |         age = datetime.now() - datetime.fromtimestamp(file_timestamp.stat().st_mtime) | ||||||
|         if age.total_seconds() > refresh.total_seconds: |         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.info("Updating %s", key) | ||||||
|             _LOGGER.debug("Location: %s", repo_dir) |             _LOGGER.debug("Location: %s", repo_dir) | ||||||
|             # Stash local changes (if any) |             # 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) |             # 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)) |             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 | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import os | |||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import Union | from typing import Union | ||||||
| import tempfile | import tempfile | ||||||
|  | from urllib.parse import urlparse | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -40,7 +41,7 @@ def indent(text, padding="  "): | |||||||
|  |  | ||||||
| # From https://stackoverflow.com/a/14945195/8924614 | # From https://stackoverflow.com/a/14945195/8924614 | ||||||
| def cpp_string_escape(string, encoding="utf-8"): | 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: |         if not 32 <= byte < 127: | ||||||
|             return True |             return True | ||||||
|         if byte in (ord("\\"), ord('"')): |         if byte in (ord("\\"), ord('"')): | ||||||
| @@ -134,7 +135,8 @@ def resolve_ip_address(host): | |||||||
|             errs.append(str(err)) |             errs.append(str(err)) | ||||||
|  |  | ||||||
|     try: |     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: |     except OSError as err: | ||||||
|         errs.append(str(err)) |         errs.append(str(err)) | ||||||
|         raise EsphomeError(f"Error resolving IP address: {', '.join(errs)}") from err |         raise EsphomeError(f"Error resolving IP address: {', '.join(errs)}") from err | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import logging | import logging | ||||||
| from typing import Callable, List, Optional, Any, ContextManager | from typing import Callable, Optional, Any, ContextManager | ||||||
| from types import ModuleType | from types import ModuleType | ||||||
| import importlib | import importlib | ||||||
| import importlib.util | import importlib.util | ||||||
| @@ -62,19 +62,19 @@ class ComponentManifest: | |||||||
|         return getattr(self.module, "to_code", None) |         return getattr(self.module, "to_code", None) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def dependencies(self) -> List[str]: |     def dependencies(self) -> list[str]: | ||||||
|         return getattr(self.module, "DEPENDENCIES", []) |         return getattr(self.module, "DEPENDENCIES", []) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def conflicts_with(self) -> List[str]: |     def conflicts_with(self) -> list[str]: | ||||||
|         return getattr(self.module, "CONFLICTS_WITH", []) |         return getattr(self.module, "CONFLICTS_WITH", []) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def auto_load(self) -> List[str]: |     def auto_load(self) -> list[str]: | ||||||
|         return getattr(self.module, "AUTO_LOAD", []) |         return getattr(self.module, "AUTO_LOAD", []) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def codeowners(self) -> List[str]: |     def codeowners(self) -> list[str]: | ||||||
|         return getattr(self.module, "CODEOWNERS", []) |         return getattr(self.module, "CODEOWNERS", []) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
| @@ -87,7 +87,7 @@ class ComponentManifest: | |||||||
|         return getattr(self.module, "FINAL_VALIDATE_SCHEMA", None) |         return getattr(self.module, "FINAL_VALIDATE_SCHEMA", None) | ||||||
|  |  | ||||||
|     @property |     @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. |         """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 |         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): | class ComponentMetaFinder(importlib.abc.MetaPathFinder): | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, components_path: Path, allowed_components: Optional[List[str]] = None |         self, components_path: Path, allowed_components: Optional[list[str]] = None | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         self._allowed_components = allowed_components |         self._allowed_components = allowed_components | ||||||
|         self._finders = [] |         self._finders = [] | ||||||
| @@ -117,7 +117,7 @@ class ComponentMetaFinder(importlib.abc.MetaPathFinder): | |||||||
|                 continue |                 continue | ||||||
|             self._finders.append(finder) |             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."): |         if not fullname.startswith("esphome.components."): | ||||||
|             return None |             return None | ||||||
|         parts = fullname.split(".") |         parts = fullname.split(".") | ||||||
| @@ -144,7 +144,7 @@ def clear_component_meta_finders(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def install_meta_finder( | 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)) |     sys.meta_path.insert(0, ComponentMetaFinder(components_path, allowed_components)) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| import json | import json | ||||||
| from typing import List, Union | from typing import Union | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
| @@ -310,7 +310,7 @@ class IDEData: | |||||||
|         return str(Path(self.firmware_elf_path).with_suffix(".bin")) |         return str(Path(self.firmware_elf_path).with_suffix(".bin")) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def extra_flash_images(self) -> List[FlashImage]: |     def extra_flash_images(self) -> list[FlashImage]: | ||||||
|         return [ |         return [ | ||||||
|             FlashImage(path=entry["path"], offset=entry["offset"]) |             FlashImage(path=entry["path"], offset=entry["offset"]) | ||||||
|             for entry in self.raw["extra"]["flash_images"] |             for entry in self.raw["extra"]["flash_images"] | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ from datetime import datetime | |||||||
| import json | import json | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
| from typing import Any, Optional, List | from typing import Optional | ||||||
|  |  | ||||||
| from esphome import const | from esphome import const | ||||||
| from esphome.core import CORE | from esphome.core import CORE | ||||||
| @@ -15,19 +15,19 @@ from esphome.types import CoreType | |||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| def storage_path():  # type: () -> str | def storage_path() -> str: | ||||||
|     return CORE.relative_internal_path(f"{CORE.config_filename}.json") |     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") |     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") |     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") |     return os.path.join(base_path, ".esphome", "trash") | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -49,29 +49,29 @@ class StorageJSON: | |||||||
|     ): |     ): | ||||||
|         # Version of the storage JSON schema |         # Version of the storage JSON schema | ||||||
|         assert storage_version is None or isinstance(storage_version, int) |         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 |         # The name of the node | ||||||
|         self.name = name  # type: str |         self.name: str = name | ||||||
|         # The comment of the node |         # The comment of the node | ||||||
|         self.comment = comment  # type: str |         self.comment: str = comment | ||||||
|         # The esphome version this was compiled with |         # 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 |         # The version of the file in src/main.cpp - Used to migrate the file | ||||||
|         assert src_version is None or isinstance(src_version, int) |         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 |         # 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 |         # Web server port of the ESP, for example 80 | ||||||
|         assert web_port is None or isinstance(web_port, int) |         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. |         # 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 |         # 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 |         # 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 |         # 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() |         self.loaded_integrations.sort() | ||||||
|  |  | ||||||
|     def as_dict(self): |     def as_dict(self): | ||||||
| @@ -97,8 +97,8 @@ class StorageJSON: | |||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def from_esphome_core( |     def from_esphome_core( | ||||||
|         esph, old |         esph: CoreType, old: Optional["StorageJSON"] | ||||||
|     ):  # type: (CoreType, Optional[StorageJSON]) -> StorageJSON |     ) -> "StorageJSON": | ||||||
|         hardware = esph.target_platform.upper() |         hardware = esph.target_platform.upper() | ||||||
|         if esph.is_esp32: |         if esph.is_esp32: | ||||||
|             from esphome.components import esp32 |             from esphome.components import esp32 | ||||||
| @@ -135,7 +135,7 @@ class StorageJSON: | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @staticmethod |     @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: |         with codecs.open(path, "r", encoding="utf-8") as f_handle: | ||||||
|             storage = json.load(f_handle) |             storage = json.load(f_handle) | ||||||
|         storage_version = storage["storage_version"] |         storage_version = storage["storage_version"] | ||||||
| @@ -166,13 +166,13 @@ class StorageJSON: | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def load(path):  # type: (str) -> Optional[StorageJSON] |     def load(path: str) -> Optional["StorageJSON"]: | ||||||
|         try: |         try: | ||||||
|             return StorageJSON._load_impl(path) |             return StorageJSON._load_impl(path) | ||||||
|         except Exception:  # pylint: disable=broad-except |         except Exception:  # pylint: disable=broad-except | ||||||
|             return None |             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() |         return isinstance(o, StorageJSON) and self.as_dict() == o.as_dict() | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -182,15 +182,15 @@ class EsphomeStorageJSON: | |||||||
|     ): |     ): | ||||||
|         # Version of the storage JSON schema |         # Version of the storage JSON schema | ||||||
|         assert storage_version is None or isinstance(storage_version, int) |         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 |         # 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 |         # 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 |         # 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 { |         return { | ||||||
|             "storage_version": self.storage_version, |             "storage_version": self.storage_version, | ||||||
|             "cookie_secret": self.cookie_secret, |             "cookie_secret": self.cookie_secret, | ||||||
| @@ -199,24 +199,24 @@ class EsphomeStorageJSON: | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def last_update_check(self):  # type: () -> Optional[datetime] |     def last_update_check(self) -> Optional[datetime]: | ||||||
|         try: |         try: | ||||||
|             return datetime.strptime(self.last_update_check_str, "%Y-%m-%dT%H:%M:%S") |             return datetime.strptime(self.last_update_check_str, "%Y-%m-%dT%H:%M:%S") | ||||||
|         except Exception:  # pylint: disable=broad-except |         except Exception:  # pylint: disable=broad-except | ||||||
|             return None |             return None | ||||||
|  |  | ||||||
|     @last_update_check.setter |     @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") |         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" |         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()) |         write_file_if_changed(path, self.to_json()) | ||||||
|  |  | ||||||
|     @staticmethod |     @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: |         with codecs.open(path, "r", encoding="utf-8") as f_handle: | ||||||
|             storage = json.load(f_handle) |             storage = json.load(f_handle) | ||||||
|         storage_version = storage["storage_version"] |         storage_version = storage["storage_version"] | ||||||
| @@ -228,14 +228,14 @@ class EsphomeStorageJSON: | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def load(path):  # type: (str) -> Optional[EsphomeStorageJSON] |     def load(path: str) -> Optional["EsphomeStorageJSON"]: | ||||||
|         try: |         try: | ||||||
|             return EsphomeStorageJSON._load_impl(path) |             return EsphomeStorageJSON._load_impl(path) | ||||||
|         except Exception:  # pylint: disable=broad-except |         except Exception:  # pylint: disable=broad-except | ||||||
|             return None |             return None | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def get_default():  # type: () -> EsphomeStorageJSON |     def get_default() -> "EsphomeStorageJSON": | ||||||
|         return EsphomeStorageJSON( |         return EsphomeStorageJSON( | ||||||
|             storage_version=1, |             storage_version=1, | ||||||
|             cookie_secret=binascii.hexlify(os.urandom(64)).decode(), |             cookie_secret=binascii.hexlify(os.urandom(64)).decode(), | ||||||
| @@ -243,5 +243,5 @@ class EsphomeStorageJSON: | |||||||
|             remote_version=None, |             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() |         return isinstance(o, EsphomeStorageJSON) and self.as_dict() == o.as_dict() | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| """This helper module tracks commonly used types in the esphome python codebase.""" | """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 | from esphome.core import ID, Lambda, EsphomeCore | ||||||
|  |  | ||||||
| @@ -8,11 +8,11 @@ ConfigFragmentType = Union[ | |||||||
|     int, |     int, | ||||||
|     float, |     float, | ||||||
|     None, |     None, | ||||||
|     Dict[Union[str, int], "ConfigFragmentType"], |     dict[Union[str, int], "ConfigFragmentType"], | ||||||
|     List["ConfigFragmentType"], |     list["ConfigFragmentType"], | ||||||
|     ID, |     ID, | ||||||
|     Lambda, |     Lambda, | ||||||
| ] | ] | ||||||
| ConfigType = Dict[str, ConfigFragmentType] | ConfigType = dict[str, ConfigFragmentType] | ||||||
| CoreType = EsphomeCore | CoreType = EsphomeCore | ||||||
| ConfigPathType = Union[str, int] | ConfigPathType = Union[str, int] | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import typing | from typing import Union | ||||||
| from typing import Union, List |  | ||||||
|  |  | ||||||
| import collections | import collections | ||||||
| import io | import io | ||||||
| @@ -35,7 +34,7 @@ class RegistryEntry: | |||||||
|         return Schema(self.raw_schema) |         return Schema(self.raw_schema) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Registry(dict): | class Registry(dict[str, RegistryEntry]): | ||||||
|     def __init__(self, base_schema=None, type_id_key=None): |     def __init__(self, base_schema=None, type_id_key=None): | ||||||
|         super().__init__() |         super().__init__() | ||||||
|         self.base_schema = base_schema or {} |         self.base_schema = base_schema or {} | ||||||
| @@ -242,7 +241,7 @@ def is_dev_esphome_version(): | |||||||
|     return "dev" in const.__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__) |     match = re.match(r"^(\d+).(\d+).(\d+)(-dev\d*|b\d*)?$", const.__version__) | ||||||
|     if match is None: |     if match is None: | ||||||
|         raise ValueError(f"Failed to parse ESPHome version '{const.__version__}'") |         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 | # 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 |     from serial.tools.list_ports import comports | ||||||
|  |  | ||||||
|     result = [] |     result = [] | ||||||
|   | |||||||
| @@ -10,15 +10,13 @@ import esphome.config_validation as cv | |||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
|  |  | ||||||
| def _get_invalid_range(res, invalid): | def _get_invalid_range(res: Config, invalid: cv.Invalid) -> Optional[DocumentRange]: | ||||||
|     # type: (Config, cv.Invalid) -> Optional[DocumentRange] |  | ||||||
|     return res.get_deepest_document_range_for_path( |     return res.get_deepest_document_range_for_path( | ||||||
|         invalid.path, invalid.error_message == "extra keys not allowed" |         invalid.path, invalid.error_message == "extra keys not allowed" | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def _dump_range(range): | def _dump_range(range: Optional[DocumentRange]) -> Optional[dict]: | ||||||
|     # type: (Optional[DocumentRange]) -> Optional[dict] |  | ||||||
|     if range is None: |     if range is None: | ||||||
|         return None |         return None | ||||||
|     return { |     return { | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import logging | |||||||
| import os | import os | ||||||
| import re | import re | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import Dict, List, Union | from typing import Union | ||||||
|  |  | ||||||
| from esphome.config import iter_components | from esphome.config import iter_components | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
| @@ -98,7 +98,7 @@ def replace_file_content(text, pattern, repl): | |||||||
|     return content_new, count |     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: |     if old is None: | ||||||
|         return True |         return True | ||||||
|  |  | ||||||
| @@ -123,7 +123,7 @@ def update_storage_json(): | |||||||
|     new.save(path) |     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 = "" |     content = "" | ||||||
|     for key, value in sorted(data.items()): |     for key, value in sorted(data.items()): | ||||||
|         if isinstance(value, list): |         if isinstance(value, list): | ||||||
| @@ -226,7 +226,7 @@ the custom_components folder or the external_components feature. | |||||||
|  |  | ||||||
|  |  | ||||||
| def copy_src_tree(): | def copy_src_tree(): | ||||||
|     source_files: List[loader.FileResource] = [] |     source_files: list[loader.FileResource] = [] | ||||||
|     for _, component, _ in iter_components(CORE.config): |     for _, component, _ in iter_components(CORE.config): | ||||||
|         source_files += component.resources |         source_files += component.resources | ||||||
|     source_files_map = { |     source_files_map = { | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import socket | import socket | ||||||
| import threading | import threading | ||||||
| import time | import time | ||||||
| from typing import Dict, Optional | from typing import Optional | ||||||
| import logging | import logging | ||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
|  |  | ||||||
| @@ -71,12 +71,12 @@ class DashboardStatus(threading.Thread): | |||||||
|         threading.Thread.__init__(self) |         threading.Thread.__init__(self) | ||||||
|         self.zc = zc |         self.zc = zc | ||||||
|         self.query_hosts: set[str] = set() |         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.stop_event = threading.Event() | ||||||
|         self.query_event = threading.Event() |         self.query_event = threading.Event() | ||||||
|         self.on_update = on_update |         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.query_hosts = set(hosts.values()) | ||||||
|         self.key_to_host = hosts |         self.key_to_host = hosts | ||||||
|         self.query_event.set() |         self.query_event.set() | ||||||
|   | |||||||
| @@ -1,3 +1,3 @@ | |||||||
| [tool.black] | [tool.black] | ||||||
| target-version = ["py36", "py37", "py38"] | target-version = ["py39", "py310"] | ||||||
| exclude = 'generated' | exclude = 'generated' | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| pylint==2.15.2 | pylint==2.15.3 | ||||||
| flake8==5.0.4 | flake8==5.0.4 | ||||||
| black==22.8.0  # also change in .pre-commit-config.yaml when updating | 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 | pre-commit | ||||||
|  |  | ||||||
| # Unit tests | # Unit tests | ||||||
|   | |||||||
| @@ -109,7 +109,7 @@ def main(): | |||||||
|         print_error(file_, linno, msg) |         print_error(file_, linno, msg) | ||||||
|         errors += 1 |         errors += 1 | ||||||
|  |  | ||||||
|     PYUPGRADE_TARGET = "--py38-plus" |     PYUPGRADE_TARGET = "--py39-plus" | ||||||
|     cmd = ["pyupgrade", PYUPGRADE_TARGET] + files |     cmd = ["pyupgrade", PYUPGRADE_TARGET] + files | ||||||
|     print() |     print() | ||||||
|     print("Running pyupgrade...") |     print("Running pyupgrade...") | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							| @@ -74,7 +74,7 @@ setup( | |||||||
|     zip_safe=False, |     zip_safe=False, | ||||||
|     platforms="any", |     platforms="any", | ||||||
|     test_suite="tests", |     test_suite="tests", | ||||||
|     python_requires=">=3.8,<4.0", |     python_requires=">=3.9.0", | ||||||
|     install_requires=REQUIRES, |     install_requires=REQUIRES, | ||||||
|     keywords=["home", "automation"], |     keywords=["home", "automation"], | ||||||
|     entry_points={"console_scripts": ["esphome = esphome.__main__:main"]}, |     entry_points={"console_scripts": ["esphome = esphome.__main__:main"]}, | ||||||
|   | |||||||
| @@ -290,8 +290,6 @@ adalight: | |||||||
|  |  | ||||||
| esp32_ble_tracker: | esp32_ble_tracker: | ||||||
|  |  | ||||||
| bluetooth_proxy: |  | ||||||
|  |  | ||||||
| ble_client: | ble_client: | ||||||
|   - mac_address: AA:BB:CC:DD:EE:FF |   - mac_address: AA:BB:CC:DD:EE:FF | ||||||
|     id: ble_foo |     id: ble_foo | ||||||
| @@ -321,6 +319,7 @@ mcp23s17: | |||||||
|  |  | ||||||
| sensor: | sensor: | ||||||
|   - platform: ble_client |   - platform: ble_client | ||||||
|  |     type: characteristic | ||||||
|     ble_client_id: ble_foo |     ble_client_id: ble_foo | ||||||
|     name: Green iTag btn |     name: Green iTag btn | ||||||
|     service_uuid: ffe0 |     service_uuid: ffe0 | ||||||
| @@ -335,6 +334,11 @@ sensor: | |||||||
|       then: |       then: | ||||||
|         - lambda: |- |         - lambda: |- | ||||||
|             ESP_LOGD("green_btn", "Button was pressed, val%f", x); |             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 |   - platform: adc | ||||||
|     pin: A0 |     pin: A0 | ||||||
|     name: Living Room Brightness |     name: Living Room Brightness | ||||||
|   | |||||||
| @@ -506,6 +506,9 @@ xiaomi_ble: | |||||||
|  |  | ||||||
| mopeka_ble: | mopeka_ble: | ||||||
|  |  | ||||||
|  | bluetooth_proxy: | ||||||
|  |   active: true | ||||||
|  |  | ||||||
| xiaomi_rtcgq02lm: | xiaomi_rtcgq02lm: | ||||||
|   - id: motion_rtcgq02lm |   - id: motion_rtcgq02lm | ||||||
|     mac_address: 01:02:03:04:05:06 |     mac_address: 01:02:03:04:05:06 | ||||||
|   | |||||||
| @@ -1061,8 +1061,13 @@ climate: | |||||||
|   - platform: thermostat |   - platform: thermostat | ||||||
|     name: Thermostat Climate |     name: Thermostat Climate | ||||||
|     sensor: ha_hello_world |     sensor: ha_hello_world | ||||||
|  |     preset: | ||||||
|  |     - name: Default Preset | ||||||
|       default_target_temperature_low: 18°C |       default_target_temperature_low: 18°C | ||||||
|       default_target_temperature_high: 24°C |       default_target_temperature_high: 24°C | ||||||
|  |     - name: Away | ||||||
|  |       default_target_temperature_low: 16°C | ||||||
|  |       default_target_temperature_high: 20°C | ||||||
|     idle_action: |     idle_action: | ||||||
|       - switch.turn_on: gpio_switch1 |       - switch.turn_on: gpio_switch1 | ||||||
|     cool_action: |     cool_action: | ||||||
| @@ -1137,9 +1142,6 @@ climate: | |||||||
|     fan_only_cooling: true |     fan_only_cooling: true | ||||||
|     fan_with_cooling: true |     fan_with_cooling: true | ||||||
|     fan_with_heating: true |     fan_with_heating: true | ||||||
|     away_config: |  | ||||||
|       default_target_temperature_low: 16°C |  | ||||||
|       default_target_temperature_high: 20°C |  | ||||||
|   - platform: pid |   - platform: pid | ||||||
|     id: pid_climate |     id: pid_climate | ||||||
|     name: PID Climate Controller |     name: PID Climate Controller | ||||||
|   | |||||||
| @@ -348,15 +348,16 @@ binary_sensor: | |||||||
|     on_state: |     on_state: | ||||||
|       then: |       then: | ||||||
|         - lambda: 'ESP_LOGI("ar1:", "%d", x);' |         - lambda: 'ESP_LOGI("ar1:", "%d", x);' | ||||||
|   - platform: xpt2046 |   - platform: touchscreen | ||||||
|     xpt2046_id: xpt_touchscreen |     touchscreen_id: xpt_touchscreen | ||||||
|     id: touch_key0 |     id: touch_key0 | ||||||
|     x_min: 80 |     x_min: 80 | ||||||
|     x_max: 160 |     x_max: 160 | ||||||
|     y_min: 106 |     y_min: 106 | ||||||
|     y_max: 212 |     y_max: 212 | ||||||
|     on_state: |     on_press: | ||||||
|       - lambda: 'ESP_LOGI("main", "key0: %s", (x ? "touch" : "release"));' |       - logger.log: Touched | ||||||
|  |  | ||||||
|   - platform: gpio |   - platform: gpio | ||||||
|     name: GPIO SX1509 test |     name: GPIO SX1509 test | ||||||
|     pin: |     pin: | ||||||
| @@ -598,33 +599,6 @@ external_components: | |||||||
|     components: [bh1750] |     components: [bh1750] | ||||||
|   - source: ../esphome/components |   - source: ../esphome/components | ||||||
|     components: [sntp] |     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: | button: | ||||||
|   - platform: restart |   - platform: restart | ||||||
| @@ -648,6 +622,25 @@ touchscreen: | |||||||
|           format: Touch at (%d, %d) |           format: Touch at (%d, %d) | ||||||
|           args: [touch.x, touch.y] |           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 |   - platform: lilygo_t5_47 | ||||||
|     id: lilygo_touchscreen |     id: lilygo_touchscreen | ||||||
|     interrupt_pin: GPIO36 |     interrupt_pin: GPIO36 | ||||||
|   | |||||||
| @@ -1,12 +1,9 @@ | |||||||
| from typing import Text |  | ||||||
|  |  | ||||||
| import hypothesis.strategies._internal.core as st | import hypothesis.strategies._internal.core as st | ||||||
| from hypothesis.strategies._internal.strategies import SearchStrategy | from hypothesis.strategies._internal.strategies import SearchStrategy | ||||||
|  |  | ||||||
|  |  | ||||||
| @st.defines_strategy(force_reusable_values=True) | @st.defines_strategy(force_reusable_values=True) | ||||||
| def mac_addr_strings(): | def mac_addr_strings() -> SearchStrategy[str]: | ||||||
|     # type: () -> SearchStrategy[Text] |  | ||||||
|     """A strategy for MAC address strings. |     """A strategy for MAC address strings. | ||||||
|  |  | ||||||
|     This consists of six strings representing integers [0..255], |     This consists of six strings representing integers [0..255], | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user