Merge branch 'beta' into bump-2022.10.0

This commit is contained in:
Jesse Hills 2022-10-19 10:30:21 +13:00
commit 67c911c37f
No known key found for this signature in database
GPG key ID: BEAAE804EFD8E83A
86 changed files with 1547 additions and 813 deletions

View file

@ -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"]

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -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,

View file

@ -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(
{ {

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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: {

View file

@ -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;

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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_)

View file

@ -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);

View file

@ -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)

View 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

View 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

View file

@ -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;
} }

View file

@ -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,

View file

@ -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"]

View file

@ -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

View file

@ -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_;
}; };

View file

@ -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

View file

@ -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;
}; };

View file

@ -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: {

View file

@ -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)

View file

@ -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,

View file

@ -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"

View file

@ -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() {

View file

@ -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

View file

@ -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_);

View file

@ -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;

View file

@ -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

View file

@ -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}

View file

@ -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;
} }
} }

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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,
) )

View file

@ -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]

View file

@ -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;

View file

@ -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

View file

@ -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) {

View file

@ -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; }

View file

@ -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

View file

@ -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_)

View file

@ -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]))

View file

@ -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,
) )

View file

@ -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))

View 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))

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
): ):

View file

@ -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):

View file

@ -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

View file

@ -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))

View file

@ -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,

View file

@ -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):

View file

@ -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)

View file

@ -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

View file

@ -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}"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -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"]

View file

@ -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()

View file

@ -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]

View file

@ -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 = []

View file

@ -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 {

View file

@ -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 = {

View file

@ -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()

View file

@ -1,3 +1,3 @@
[tool.black] [tool.black]
target-version = ["py36", "py37", "py38"] target-version = ["py39", "py310"]
exclude = 'generated' exclude = 'generated'

View file

@ -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

View file

@ -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...")

View file

@ -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"]},

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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],