mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[ble_device_base] Migrate BLE sensor platforms to the neutral layer (batch 6: thermopro_ble, exposure_notifications, xiaomi_ble) (#18168)
This commit is contained in:
@@ -1,33 +1,59 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import esp32_ble_tracker
|
from esphome.components import ble_device_base
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_TRIGGER_ID
|
from esphome.const import CONF_TRIGGER_ID
|
||||||
|
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
CODEOWNERS = ["@OttoWinter"]
|
CODEOWNERS = ["@OttoWinter"]
|
||||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
AUTO_LOAD = ["ble_device_base"]
|
||||||
|
|
||||||
exposure_notifications_ns = cg.esphome_ns.namespace("exposure_notifications")
|
exposure_notifications_ns = cg.esphome_ns.namespace("exposure_notifications")
|
||||||
ExposureNotification = exposure_notifications_ns.struct("ExposureNotification")
|
ExposureNotification = exposure_notifications_ns.struct("ExposureNotification")
|
||||||
ExposureNotificationTrigger = exposure_notifications_ns.class_(
|
ExposureNotificationTrigger = exposure_notifications_ns.class_(
|
||||||
"ExposureNotificationTrigger",
|
"ExposureNotificationTrigger",
|
||||||
esp32_ble_tracker.ESPBTDeviceListener,
|
ble_device_base.ESPBTDeviceListener,
|
||||||
automation.Trigger.template(ExposureNotification),
|
automation.Trigger.template(ExposureNotification),
|
||||||
)
|
)
|
||||||
|
|
||||||
CONF_ON_EXPOSURE_NOTIFICATION = "on_exposure_notification"
|
CONF_ON_EXPOSURE_NOTIFICATION = "on_exposure_notification"
|
||||||
|
|
||||||
|
_RENAME_HUB_ID = ble_device_base.rename_legacy_hub_id("exposure_notifications")
|
||||||
|
|
||||||
|
_VALIDATE_AUTOMATION = automation.validate_automation(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ExposureNotificationTrigger),
|
||||||
|
}
|
||||||
|
# The trigger is the BLE listener, so the hub id lives on it.
|
||||||
|
).extend(ble_device_base.BLE_DEVICE_SCHEMA)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# validate_automation() needs a dict-based schema, so the rename cannot go
|
||||||
|
# inside it and has to run on the option value first. That value may also be a
|
||||||
|
# list of automations or malformed, and rename_legacy_hub_id() is dict-only, so
|
||||||
|
# map over lists and let validate_automation() report anything else.
|
||||||
|
# schema_extractor keeps the key typed as a trigger in the generated editor
|
||||||
|
# schema; build_language_schema.py recurses into cv.All but not into a plain
|
||||||
|
# function.
|
||||||
|
@schema_extractor("automation")
|
||||||
|
def _validate_on_exposure_notification(value: Any) -> list[ConfigType]:
|
||||||
|
if value is SCHEMA_EXTRACT:
|
||||||
|
return _VALIDATE_AUTOMATION(value)
|
||||||
|
if isinstance(value, dict):
|
||||||
|
value = _RENAME_HUB_ID(value)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
value = [_RENAME_HUB_ID(v) if isinstance(v, dict) else v for v in value]
|
||||||
|
return _VALIDATE_AUTOMATION(value)
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_ON_EXPOSURE_NOTIFICATION): automation.validate_automation(
|
cv.Required(CONF_ON_EXPOSURE_NOTIFICATION): _validate_on_exposure_notification,
|
||||||
cv.Schema(
|
|
||||||
{
|
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
|
||||||
ExposureNotificationTrigger
|
|
||||||
),
|
|
||||||
}
|
|
||||||
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,4 +62,4 @@ async def to_code(config):
|
|||||||
for conf in config.get(CONF_ON_EXPOSURE_NOTIFICATION, []):
|
for conf in config.get(CONF_ON_EXPOSURE_NOTIFICATION, []):
|
||||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||||
await automation.build_automation(trigger, [(ExposureNotification, "x")], conf)
|
await automation.build_automation(trigger, [(ExposureNotification, "x")], conf)
|
||||||
await esp32_ble_tracker.register_ble_device(trigger, conf)
|
await ble_device_base.register_ble_device(trigger, conf)
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
|
||||||
|
|
||||||
namespace esphome::exposure_notifications {
|
namespace esphome::exposure_notifications {
|
||||||
|
|
||||||
using namespace esp32_ble_tracker;
|
using namespace ble_device_base;
|
||||||
|
|
||||||
static const char *const TAG = "exposure_notifications";
|
static const char *const TAG = "exposure_notifications";
|
||||||
|
|
||||||
@@ -43,5 +41,3 @@ bool ExposureNotificationTrigger::parse_device(const ESPBTDevice &device) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace esphome::exposure_notifications
|
} // namespace esphome::exposure_notifications
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
#include "esphome/components/ble_device_base/ble_device.h"
|
||||||
#include <array>
|
#include <array>
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
|
||||||
|
|
||||||
namespace esphome::exposure_notifications {
|
namespace esphome::exposure_notifications {
|
||||||
|
|
||||||
struct ExposureNotification {
|
struct ExposureNotification {
|
||||||
@@ -17,11 +15,9 @@ struct ExposureNotification {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class ExposureNotificationTrigger final : public Trigger<ExposureNotification>,
|
class ExposureNotificationTrigger final : public Trigger<ExposureNotification>,
|
||||||
public esp32_ble_tracker::ESPBTDeviceListener {
|
public ble_device_base::ESPBTDeviceListener {
|
||||||
public:
|
public:
|
||||||
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
|
bool parse_device(const ble_device_base::ESPBTDevice &device) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::exposure_notifications
|
} // namespace esphome::exposure_notifications
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import esp32_ble_tracker, sensor
|
from esphome.components import ble_device_base, sensor
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_BATTERY_LEVEL,
|
CONF_BATTERY_LEVEL,
|
||||||
@@ -22,14 +22,15 @@ from esphome.const import (
|
|||||||
|
|
||||||
CODEOWNERS = ["@sittner"]
|
CODEOWNERS = ["@sittner"]
|
||||||
|
|
||||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
AUTO_LOAD = ["ble_device_base"]
|
||||||
|
|
||||||
thermopro_ble_ns = cg.esphome_ns.namespace("thermopro_ble")
|
thermopro_ble_ns = cg.esphome_ns.namespace("thermopro_ble")
|
||||||
ThermoProBLE = thermopro_ble_ns.class_(
|
ThermoProBLE = thermopro_ble_ns.class_(
|
||||||
"ThermoProBLE", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
|
"ThermoProBLE", ble_device_base.ESPBTDeviceListener, cg.Component
|
||||||
)
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = (
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
ble_device_base.rename_legacy_hub_id("thermopro_ble"),
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(ThermoProBLE),
|
cv.GenerateID(): cv.declare_id(ThermoProBLE),
|
||||||
@@ -68,15 +69,15 @@ CONFIG_SCHEMA = (
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
|
|
||||||
.extend(cv.COMPONENT_SCHEMA)
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
|
.extend(ble_device_base.BLE_DEVICE_SCHEMA),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await esp32_ble_tracker.register_ble_device(var, config)
|
await ble_device_base.register_ble_device(var, config)
|
||||||
|
|
||||||
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
|
||||||
|
|
||||||
namespace esphome::thermopro_ble {
|
namespace esphome::thermopro_ble {
|
||||||
|
|
||||||
// this size must be large enough to hold the largest data frame
|
// this size must be large enough to hold the largest data frame
|
||||||
@@ -34,7 +32,7 @@ void ThermoProBLE::dump_config() {
|
|||||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ThermoProBLE::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
bool ThermoProBLE::parse_device(const ble_device_base::ESPBTDevice &device) {
|
||||||
// check for matching mac address
|
// check for matching mac address
|
||||||
if (device.address_uint64() != this->address_) {
|
if (device.address_uint64() != this->address_) {
|
||||||
ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
|
ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
|
||||||
@@ -66,8 +64,8 @@ bool ThermoProBLE::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// reconstruct whole record from 2 byte uuid and data
|
// reconstruct whole record from 2 byte uuid and data
|
||||||
esp_bt_uuid_t uuid = service_data.uuid.get_uuid();
|
uint16_t svc_uuid16 = service_data.uuid.uuid16();
|
||||||
uint8_t data[MAX_DATA_SIZE] = {static_cast<uint8_t>(uuid.uuid.uuid16), static_cast<uint8_t>(uuid.uuid.uuid16 >> 8)};
|
uint8_t data[MAX_DATA_SIZE] = {static_cast<uint8_t>(svc_uuid16), static_cast<uint8_t>(svc_uuid16 >> 8)};
|
||||||
std::copy(service_data.data.begin(), service_data.data.end(), std::begin(data) + 2);
|
std::copy(service_data.data.begin(), service_data.data.end(), std::begin(data) + 2);
|
||||||
|
|
||||||
// dispatch data to parser
|
// dispatch data to parser
|
||||||
@@ -202,5 +200,3 @@ static optional<ParseResult> parse_tp3(const uint8_t *data, std::size_t data_siz
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace esphome::thermopro_ble
|
} // namespace esphome::thermopro_ble
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/components/sensor/sensor.h"
|
#include "esphome/components/sensor/sensor.h"
|
||||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
#include "esphome/components/ble_device_base/ble_device.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
|
||||||
|
|
||||||
namespace esphome::thermopro_ble {
|
namespace esphome::thermopro_ble {
|
||||||
|
|
||||||
@@ -17,11 +15,11 @@ struct ParseResult {
|
|||||||
|
|
||||||
using DeviceParser = optional<ParseResult> (*)(const uint8_t *data, std::size_t data_size);
|
using DeviceParser = optional<ParseResult> (*)(const uint8_t *data, std::size_t data_size);
|
||||||
|
|
||||||
class ThermoProBLE final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
|
class ThermoProBLE final : public Component, public ble_device_base::ESPBTDeviceListener {
|
||||||
public:
|
public:
|
||||||
void set_address(uint64_t address) { this->address_ = address; };
|
void set_address(uint64_t address) { this->address_ = address; };
|
||||||
|
|
||||||
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
|
bool parse_device(const ble_device_base::ESPBTDevice &device) override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
void set_signal_strength(sensor::Sensor *signal_strength) { this->signal_strength_ = signal_strength; }
|
void set_signal_strength(sensor::Sensor *signal_strength) { this->signal_strength_ = signal_strength; }
|
||||||
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
|
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
|
||||||
@@ -45,5 +43,3 @@ class ThermoProBLE final : public Component, public esp32_ble_tracker::ESPBTDevi
|
|||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::thermopro_ble
|
} // namespace esphome::thermopro_ble
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import esp32_ble_tracker
|
from esphome.components import ble_device_base
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID
|
from esphome.const import CONF_ID
|
||||||
|
|
||||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
AUTO_LOAD = ["ble_device_base"]
|
||||||
|
|
||||||
xiaomi_ble_ns = cg.esphome_ns.namespace("xiaomi_ble")
|
xiaomi_ble_ns = cg.esphome_ns.namespace("xiaomi_ble")
|
||||||
XiaomiListener = xiaomi_ble_ns.class_(
|
XiaomiListener = xiaomi_ble_ns.class_(
|
||||||
"XiaomiListener", esp32_ble_tracker.ESPBTDeviceListener
|
"XiaomiListener", ble_device_base.ESPBTDeviceListener
|
||||||
)
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
CONFIG_SCHEMA = cv.All(
|
||||||
{
|
ble_device_base.rename_legacy_hub_id("xiaomi_ble"),
|
||||||
cv.GenerateID(): cv.declare_id(XiaomiListener),
|
cv.Schema(
|
||||||
}
|
{
|
||||||
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
|
cv.GenerateID(): cv.declare_id(XiaomiListener),
|
||||||
|
}
|
||||||
|
).extend(ble_device_base.BLE_DEVICE_SCHEMA),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await esp32_ble_tracker.register_ble_device(var, config)
|
await ble_device_base.register_ble_device(var, config)
|
||||||
|
|||||||
@@ -2,14 +2,21 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
// AES-CCM backend for encrypted-payload (bindkey) decryption:
|
||||||
|
// - ESP32 + ESP-IDF >= 6.0 -> PSA crypto (psa_aead_decrypt), hardware-backed.
|
||||||
|
// - every other platform -> the portable software AES-CCM in ble_device_base, so
|
||||||
|
// decryption never depends on the SDK exposing mbedtls/PSA to application code.
|
||||||
|
#ifdef USE_ESP32
|
||||||
#include <esp_idf_version.h>
|
#include <esp_idf_version.h>
|
||||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||||
#include <psa/crypto.h>
|
#include <psa/crypto.h>
|
||||||
#else
|
#define XIAOMI_CRYPTO_PSA
|
||||||
#include "mbedtls/ccm.h"
|
#endif
|
||||||
|
#endif
|
||||||
|
#ifndef XIAOMI_CRYPTO_PSA
|
||||||
|
#include "esphome/components/ble_device_base/ble_aes_ccm.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace esphome::xiaomi_ble {
|
namespace esphome::xiaomi_ble {
|
||||||
@@ -166,7 +173,7 @@ bool parse_xiaomi_message(const std::vector<uint8_t> &message, XiaomiParseResult
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data) {
|
optional<XiaomiParseResult> parse_xiaomi_header(const ble_device_base::ServiceData &service_data) {
|
||||||
XiaomiParseResult result;
|
XiaomiParseResult result;
|
||||||
if (!service_data.uuid.contains(0x95, 0xFE)) {
|
if (!service_data.uuid.contains(0x95, 0xFE)) {
|
||||||
ESP_LOGVV(TAG, "parse_xiaomi_header(): no service data UUID magic bytes.");
|
ESP_LOGVV(TAG, "parse_xiaomi_header(): no service data UUID magic bytes.");
|
||||||
@@ -318,7 +325,7 @@ bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, c
|
|||||||
memcpy(vector.iv + 6, v + 2, 3); // sensor type (2) + packet id (1)
|
memcpy(vector.iv + 6, v + 2, 3); // sensor type (2) + packet id (1)
|
||||||
memcpy(vector.iv + 9, v + raw.size() - 7, 3); // payload counter
|
memcpy(vector.iv + 9, v + raw.size() - 7, 3); // payload counter
|
||||||
|
|
||||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
#ifdef XIAOMI_CRYPTO_PSA
|
||||||
// PSA AEAD expects ciphertext + tag concatenated
|
// PSA AEAD expects ciphertext + tag concatenated
|
||||||
uint8_t ct_with_tag[sizeof(vector.ciphertext) + sizeof(vector.tag)];
|
uint8_t ct_with_tag[sizeof(vector.ciphertext) + sizeof(vector.tag)];
|
||||||
memcpy(ct_with_tag, vector.ciphertext, vector.datasize);
|
memcpy(ct_with_tag, vector.ciphertext, vector.datasize);
|
||||||
@@ -344,20 +351,10 @@ bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, c
|
|||||||
psa_destroy_key(key_id);
|
psa_destroy_key(key_id);
|
||||||
bool decrypt_ok = (status == PSA_SUCCESS && plaintext_length == vector.datasize);
|
bool decrypt_ok = (status == PSA_SUCCESS && plaintext_length == vector.datasize);
|
||||||
#else
|
#else
|
||||||
mbedtls_ccm_context ctx;
|
// Portable software AES-CCM (ble_device_base) — no SDK mbedtls/PSA dependency.
|
||||||
mbedtls_ccm_init(&ctx);
|
bool decrypt_ok = ble_device_base::aes_ccm_auth_decrypt(vector.key, vector.iv, vector.ivsize, vector.authdata,
|
||||||
|
vector.authsize, vector.ciphertext, vector.datasize,
|
||||||
int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, vector.key, vector.keysize * 8);
|
vector.plaintext, vector.tag, vector.tagsize);
|
||||||
if (ret) {
|
|
||||||
ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): mbedtls_ccm_setkey() failed.");
|
|
||||||
mbedtls_ccm_free(&ctx);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = mbedtls_ccm_auth_decrypt(&ctx, vector.datasize, vector.iv, vector.ivsize, vector.authdata, vector.authsize,
|
|
||||||
vector.ciphertext, vector.plaintext, vector.tag, vector.tagsize);
|
|
||||||
mbedtls_ccm_free(&ctx);
|
|
||||||
bool decrypt_ok = (ret == 0);
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (!decrypt_ok) {
|
if (!decrypt_ok) {
|
||||||
@@ -448,7 +445,7 @@ bool report_xiaomi_results(const optional<XiaomiParseResult> &result, const char
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
bool XiaomiListener::parse_device(const ble_device_base::ESPBTDevice &device) {
|
||||||
// Previously the message was parsed twice per packet, once by XiaomiListener::parse_device()
|
// Previously the message was parsed twice per packet, once by XiaomiListener::parse_device()
|
||||||
// and then again by the respective device class's parse_device() function. Parsing the header
|
// and then again by the respective device class's parse_device() function. Parsing the header
|
||||||
// here and then for each device seems to be unnecessary and complicates the duplicate packet filtering.
|
// here and then for each device seems to be unnecessary and complicates the duplicate packet filtering.
|
||||||
@@ -460,5 +457,3 @@ bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace esphome::xiaomi_ble
|
} // namespace esphome::xiaomi_ble
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
#include "esphome/components/ble_device_base/ble_device.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
|
||||||
|
|
||||||
namespace esphome::xiaomi_ble {
|
namespace esphome::xiaomi_ble {
|
||||||
|
|
||||||
struct XiaomiParseResult {
|
struct XiaomiParseResult {
|
||||||
@@ -68,15 +66,13 @@ struct XiaomiAESVector {
|
|||||||
|
|
||||||
bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result);
|
bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result);
|
||||||
bool parse_xiaomi_message(const std::vector<uint8_t> &message, XiaomiParseResult &result);
|
bool parse_xiaomi_message(const std::vector<uint8_t> &message, XiaomiParseResult &result);
|
||||||
optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data);
|
optional<XiaomiParseResult> parse_xiaomi_header(const ble_device_base::ServiceData &service_data);
|
||||||
bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, const uint64_t &address);
|
bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, const uint64_t &address);
|
||||||
bool report_xiaomi_results(const optional<XiaomiParseResult> &result, const char *address);
|
bool report_xiaomi_results(const optional<XiaomiParseResult> &result, const char *address);
|
||||||
|
|
||||||
class XiaomiListener final : public esp32_ble_tracker::ESPBTDeviceListener {
|
class XiaomiListener final : public ble_device_base::ESPBTDeviceListener {
|
||||||
public:
|
public:
|
||||||
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
|
bool parse_device(const ble_device_base::ESPBTDevice &device) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::xiaomi_ble
|
} // namespace esphome::xiaomi_ble
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -19,6 +19,33 @@ const uint8_t TAG[4] = {0x48, 0x4d, 0xaa, 0x56};
|
|||||||
const uint8_t PLAINTEXT[7] = {0x02, 0x01, 0x64, 0x03, 0x10, 0x8a, 0x01};
|
const uint8_t PLAINTEXT[7] = {0x02, 0x01, 0x64, 0x03, 0x10, 0x8a, 0x01};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
// Xiaomi's parameters differ from BTHome's: a 12-byte nonce and a 1-byte AAD
|
||||||
|
// (0x11). Both the AAD block and the l = 3 length encoding are only reachable
|
||||||
|
// through this shape, so they need their own vector. Generated the same way.
|
||||||
|
namespace {
|
||||||
|
const uint8_t NONCE_XIAOMI[12] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b};
|
||||||
|
const uint8_t AAD_XIAOMI[1] = {0x11};
|
||||||
|
const uint8_t CIPHERTEXT_XIAOMI[5] = {0xc3, 0x7e, 0x0a, 0x1d, 0x23};
|
||||||
|
const uint8_t TAG_XIAOMI[4] = {0x98, 0x79, 0x87, 0xc6};
|
||||||
|
const uint8_t PLAINTEXT_XIAOMI[5] = {0x04, 0x10, 0x02, 0xd4, 0x00};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(BleAesCcm, DecryptsXiaomiShapedVector) {
|
||||||
|
uint8_t out[sizeof(PLAINTEXT_XIAOMI)] = {};
|
||||||
|
EXPECT_TRUE(aes_ccm_auth_decrypt(KEY, NONCE_XIAOMI, sizeof(NONCE_XIAOMI), AAD_XIAOMI, sizeof(AAD_XIAOMI),
|
||||||
|
CIPHERTEXT_XIAOMI, sizeof(CIPHERTEXT_XIAOMI), out, TAG_XIAOMI, sizeof(TAG_XIAOMI)));
|
||||||
|
EXPECT_EQ(0, memcmp(out, PLAINTEXT_XIAOMI, sizeof(PLAINTEXT_XIAOMI)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(BleAesCcm, RejectsWrongAssociatedData) {
|
||||||
|
uint8_t bad_aad[sizeof(AAD_XIAOMI)];
|
||||||
|
memcpy(bad_aad, AAD_XIAOMI, sizeof(AAD_XIAOMI));
|
||||||
|
bad_aad[0] ^= 0x01;
|
||||||
|
uint8_t out[sizeof(PLAINTEXT_XIAOMI)] = {};
|
||||||
|
EXPECT_FALSE(aes_ccm_auth_decrypt(KEY, NONCE_XIAOMI, sizeof(NONCE_XIAOMI), bad_aad, sizeof(bad_aad),
|
||||||
|
CIPHERTEXT_XIAOMI, sizeof(CIPHERTEXT_XIAOMI), out, TAG_XIAOMI, sizeof(TAG_XIAOMI)));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(BleAesCcm, DecryptsAndAuthenticatesKnownVector) {
|
TEST(BleAesCcm, DecryptsAndAuthenticatesKnownVector) {
|
||||||
uint8_t out[sizeof(PLAINTEXT)] = {};
|
uint8_t out[sizeof(PLAINTEXT)] = {};
|
||||||
EXPECT_TRUE(aes_ccm_auth_decrypt(KEY, NONCE, sizeof(NONCE), nullptr, 0, CIPHERTEXT, sizeof(CIPHERTEXT), out, TAG,
|
EXPECT_TRUE(aes_ccm_auth_decrypt(KEY, NONCE, sizeof(NONCE), nullptr, 0, CIPHERTEXT, sizeof(CIPHERTEXT), out, TAG,
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
exposure_notifications:
|
||||||
|
on_exposure_notification:
|
||||||
|
then:
|
||||||
|
- lambda: |
|
||||||
|
ESP_LOGD("main", "RSSI: %d", x.rssi);
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
esp32_ble_tracker:
|
esp32_ble_tracker:
|
||||||
|
id: ble_tracker_hub
|
||||||
|
|
||||||
exposure_notifications:
|
exposure_notifications:
|
||||||
on_exposure_notification:
|
on_exposure_notification:
|
||||||
|
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||||
|
ble_hub_id: ble_tracker_hub
|
||||||
then:
|
then:
|
||||||
- lambda: |
|
- lambda: |
|
||||||
ESP_LOGD("main", "Got notification:");
|
ESP_LOGD("main", "Got notification:");
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
packages:
|
||||||
|
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
|
||||||
|
exposure_notifications: !include common-ln.yaml
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
|
||||||
|
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
|
||||||
|
# on a non-esp32 platform; codegen and compilation are not exercised here.
|
||||||
|
bk72xx_ble_tracker:
|
||||||
|
id: ble_tracker_hub
|
||||||
|
|
||||||
|
exposure_notifications:
|
||||||
|
on_exposure_notification:
|
||||||
|
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||||
|
ble_hub_id: ble_tracker_hub
|
||||||
|
then:
|
||||||
|
- lambda: |
|
||||||
|
ESP_LOGD("main", "Got notification:");
|
||||||
|
ESP_LOGD("main", " RPI: %s", format_hex(x.rolling_proximity_identifier).c_str());
|
||||||
|
ESP_LOGD("main", " RSSI: %d", x.rssi);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
sensor:
|
||||||
|
- platform: thermopro_ble
|
||||||
|
mac_address: FE:74:B8:6A:97:B7
|
||||||
|
temperature:
|
||||||
|
name: ThermoPro Temperature
|
||||||
|
humidity:
|
||||||
|
name: ThermoPro Humidity
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
esp32_ble_tracker:
|
esp32_ble_tracker:
|
||||||
|
id: ble_tracker_hub
|
||||||
|
|
||||||
sensor:
|
sensor:
|
||||||
|
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||||
- platform: thermopro_ble
|
- platform: thermopro_ble
|
||||||
|
ble_hub_id: ble_tracker_hub
|
||||||
mac_address: FE:74:B8:6A:97:B7
|
mac_address: FE:74:B8:6A:97:B7
|
||||||
temperature:
|
temperature:
|
||||||
name: "ThermoPro Temperature"
|
name: "ThermoPro Temperature"
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
packages:
|
||||||
|
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
|
||||||
|
thermopro_ble: !include common-ln.yaml
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
|
||||||
|
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
|
||||||
|
# on a non-esp32 platform; codegen and compilation are not exercised here.
|
||||||
|
bk72xx_ble_tracker:
|
||||||
|
id: ble_tracker_hub
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||||
|
- platform: thermopro_ble
|
||||||
|
ble_hub_id: ble_tracker_hub
|
||||||
|
mac_address: FE:74:B8:6A:97:B7
|
||||||
|
temperature:
|
||||||
|
name: "ThermoPro Temperature"
|
||||||
|
humidity:
|
||||||
|
name: "ThermoPro Humidity"
|
||||||
|
battery_level:
|
||||||
|
name: "ThermoPro Battery Level"
|
||||||
|
signal_strength:
|
||||||
|
name: "ThermoPro Signal Strength"
|
||||||
|
# No ble_hub_id: exercises the generated binding real configs use.
|
||||||
|
- platform: thermopro_ble
|
||||||
|
mac_address: FE:74:B8:6A:97:B8
|
||||||
|
temperature:
|
||||||
|
name: BK ThermoPro Implicit Temperature
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
xiaomi_ble:
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
esp32_ble_tracker:
|
esp32_ble_tracker:
|
||||||
|
id: ble_tracker_hub
|
||||||
|
|
||||||
|
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||||
xiaomi_ble:
|
xiaomi_ble:
|
||||||
|
ble_hub_id: ble_tracker_hub
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
packages:
|
||||||
|
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
|
||||||
|
xiaomi_ble: !include common-ln.yaml
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
|
||||||
|
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
|
||||||
|
# on a non-esp32 platform; codegen and compilation are not exercised here.
|
||||||
|
bk72xx_ble_tracker:
|
||||||
|
id: ble_tracker_hub
|
||||||
|
|
||||||
|
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||||
|
xiaomi_ble:
|
||||||
|
ble_hub_id: ble_tracker_hub
|
||||||
Reference in New Issue
Block a user