mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[aranet] Add Aranet BLE sensor support (#18263)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
J. Nick Koston
parent
80fa6efd1e
commit
8aab968136
@@ -44,6 +44,7 @@ esphome/components/apds9306/* @aodrenah
|
||||
esphome/components/apds9930/* @iyesin
|
||||
esphome/components/api/* @esphome/core
|
||||
esphome/components/aqi/* @freekode @jasstrong @ximex
|
||||
esphome/components/aranet/* @patrickod
|
||||
esphome/components/as5600/* @ammmze
|
||||
esphome/components/as5600/sensor/* @ammmze
|
||||
esphome/components/as7341/* @mrgnr
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
CODEOWNERS = ["@patrickod"]
|
||||
DOMAIN = "aranet"
|
||||
@@ -0,0 +1,157 @@
|
||||
#include "aranet.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::aranet {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "aranet");
|
||||
static constexpr uint16_t ARANET_MANUFACTURER_ID = 0x0702;
|
||||
static constexpr size_t ARANET4_DATA_LENGTH = 22;
|
||||
static constexpr size_t OTHER_ARANET_DATA_LENGTH = 24;
|
||||
|
||||
enum class AranetDeviceType : uint8_t {
|
||||
ARANET_DEVICE_TYPE_ARANET4 = 0,
|
||||
ARANET_DEVICE_TYPE_ARANET2 = 1,
|
||||
ARANET_DEVICE_TYPE_RADIATION = 2,
|
||||
ARANET_DEVICE_TYPE_RADON = 3,
|
||||
};
|
||||
|
||||
static inline void publish_if_set(sensor::Sensor *sensor, float value) {
|
||||
if (sensor != nullptr)
|
||||
sensor->publish_state(value);
|
||||
}
|
||||
|
||||
// Aranet does not publish an official advertisement specification. The layout below is corroborated by these
|
||||
// independently maintained implementations:
|
||||
// https://github.com/Anrijs/Aranet4-Python/blob/master/aranet4/client.py
|
||||
// https://github.com/Anrijs/Aranet4-ESP32/blob/main/src/Aranet4.h
|
||||
|
||||
void Aranet::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Aranet");
|
||||
LOG_SENSOR(" ", "Carbon Dioxide", this->co2_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_sensor_);
|
||||
LOG_SENSOR(" ", "Measurement Interval", this->measurement_interval_sensor_);
|
||||
LOG_SENSOR(" ", "Measurement Age", this->measurement_age_sensor_);
|
||||
LOG_SENSOR(" ", "Signal Strength", this->signal_strength_sensor_);
|
||||
LOG_SENSOR(" ", "Radon Concentration", this->radon_sensor_);
|
||||
LOG_SENSOR(" ", "Radiation Dose Rate", this->radiation_rate_sensor_);
|
||||
LOG_SENSOR(" ", "Radiation Total Dose", this->radiation_total_sensor_);
|
||||
LOG_SENSOR(" ", "Radiation Dose Duration", this->radiation_duration_sensor_);
|
||||
}
|
||||
|
||||
bool Aranet::parse_device(const ble_device_base::ESPBTDevice &device) {
|
||||
if (device.address_uint64() != this->address_)
|
||||
return false;
|
||||
|
||||
const auto manufacturer_uuid = ble_device_base::ESPBTUUID::from_uint16(ARANET_MANUFACTURER_ID);
|
||||
for (const auto &manufacturer_data : device.get_manufacturer_datas()) {
|
||||
if (manufacturer_data.uuid != manufacturer_uuid)
|
||||
continue;
|
||||
|
||||
const auto &data = manufacturer_data.data;
|
||||
if (data.size() != ARANET4_DATA_LENGTH && data.size() != OTHER_ARANET_DATA_LENGTH) {
|
||||
if ((this->warned_ & WARNED_INVALID_LENGTH) == 0) {
|
||||
ESP_LOGW(TAG, "Unexpected manufacturer data length: %u", static_cast<unsigned>(data.size()));
|
||||
this->warned_ |= WARNED_INVALID_LENGTH;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Aranet4 has no device type byte, so its flags and trailer start one byte earlier
|
||||
const bool is_aranet4 = data.size() == ARANET4_DATA_LENGTH;
|
||||
if ((data[is_aranet4 ? 0 : 1] & 0x20) == 0) {
|
||||
if ((this->warned_ & WARNED_INTEGRATION_DISABLED) == 0) {
|
||||
ESP_LOGW(TAG, "Smart Home integration is disabled on the Aranet device");
|
||||
this->warned_ |= WARNED_INTEGRATION_DISABLED;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_aranet4 && (data[0] == 0 || data[0] > 3)) {
|
||||
if ((this->warned_ & WARNED_UNSUPPORTED_TYPE) == 0) {
|
||||
ESP_LOGW(TAG, "Unsupported Aranet device type: %u", data[0]);
|
||||
this->warned_ |= WARNED_UNSUPPORTED_TYPE;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const auto device_type =
|
||||
is_aranet4 ? AranetDeviceType::ARANET_DEVICE_TYPE_ARANET4 : static_cast<AranetDeviceType>(data[0]);
|
||||
|
||||
// Battery, measurement interval, measurement age and measurement counter
|
||||
const uint8_t *trailer = &data[is_aranet4 ? 15 : 17];
|
||||
const uint16_t measurement_interval = encode_uint16(trailer[3], trailer[2]);
|
||||
const uint16_t measurement_age = encode_uint16(trailer[5], trailer[4]);
|
||||
const uint8_t measurement_counter = trailer[6];
|
||||
|
||||
publish_if_set(this->measurement_age_sensor_, measurement_age);
|
||||
publish_if_set(this->signal_strength_sensor_, device.get_rssi());
|
||||
|
||||
if (measurement_counter == this->last_measurement_counter_)
|
||||
return true;
|
||||
this->last_measurement_counter_ = measurement_counter;
|
||||
|
||||
// Not meaningful for Aranet Radiation, which stores its dose duration in these bytes
|
||||
const float temperature = encode_uint16(data[11], data[10]) / 20.0f;
|
||||
const float pressure = encode_uint16(data[13], data[12]) / 10.0f;
|
||||
switch (device_type) {
|
||||
case AranetDeviceType::ARANET_DEVICE_TYPE_ARANET4: {
|
||||
const uint16_t co2 = encode_uint16(data[9], data[8]);
|
||||
publish_if_set(this->co2_sensor_, co2);
|
||||
publish_if_set(this->temperature_sensor_, temperature);
|
||||
publish_if_set(this->pressure_sensor_, pressure);
|
||||
publish_if_set(this->humidity_sensor_, data[14]);
|
||||
ESP_LOGD(TAG, "Aranet4 CO2: %u ppm, temperature: %.1f C, humidity: %u%%, pressure: %.1f hPa", co2, temperature,
|
||||
data[14], pressure);
|
||||
break;
|
||||
}
|
||||
case AranetDeviceType::ARANET_DEVICE_TYPE_ARANET2: {
|
||||
const float humidity = encode_uint16(data[15], data[14]) / 10.0f;
|
||||
publish_if_set(this->temperature_sensor_, temperature);
|
||||
publish_if_set(this->humidity_sensor_, humidity);
|
||||
ESP_LOGD(TAG, "Aranet2 temperature: %.1f C, humidity: %.1f%%", temperature, humidity);
|
||||
break;
|
||||
}
|
||||
case AranetDeviceType::ARANET_DEVICE_TYPE_RADIATION: {
|
||||
const uint32_t radiation_total = encode_uint32(data[9], data[8], data[7], data[6]);
|
||||
const uint32_t radiation_duration = encode_uint32(data[13], data[12], data[11], data[10]);
|
||||
const uint16_t radiation_rate = encode_uint16(data[15], data[14]);
|
||||
publish_if_set(this->radiation_rate_sensor_, radiation_rate / 1000.0f);
|
||||
publish_if_set(this->radiation_total_sensor_, radiation_total);
|
||||
publish_if_set(this->radiation_duration_sensor_, radiation_duration);
|
||||
ESP_LOGD(TAG, "Aranet Radiation dose rate: %u nSv/h, total dose: %" PRIu32 " nSv", radiation_rate,
|
||||
radiation_total);
|
||||
break;
|
||||
}
|
||||
case AranetDeviceType::ARANET_DEVICE_TYPE_RADON: {
|
||||
const uint16_t radon = encode_uint16(data[9], data[8]);
|
||||
const float humidity = encode_uint16(data[15], data[14]) / 10.0f;
|
||||
publish_if_set(this->radon_sensor_, radon);
|
||||
// Aranet Radon One uses zero temperature and humidity to indicate fields not supported by that model.
|
||||
if (temperature != 0)
|
||||
publish_if_set(this->temperature_sensor_, temperature);
|
||||
publish_if_set(this->pressure_sensor_, pressure);
|
||||
if (humidity != 0)
|
||||
publish_if_set(this->humidity_sensor_, humidity);
|
||||
ESP_LOGD(TAG, "Aranet Radon: %u Bq/m3, temperature: %.1f C, humidity: %.1f%%, pressure: %.1f hPa", radon,
|
||||
temperature, humidity, pressure);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
publish_if_set(this->battery_level_sensor_, trailer[0]);
|
||||
publish_if_set(this->measurement_interval_sensor_, measurement_interval);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace esphome::aranet
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_device.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::aranet {
|
||||
|
||||
class Aranet final : public Component, public ble_device_base::ESPBTDeviceListener {
|
||||
public:
|
||||
explicit Aranet(uint64_t address) : address_(address) {}
|
||||
|
||||
bool parse_device(const ble_device_base::ESPBTDevice &device) override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
static constexpr uint8_t WARNED_INVALID_LENGTH = 1 << 0;
|
||||
static constexpr uint8_t WARNED_INTEGRATION_DISABLED = 1 << 1;
|
||||
static constexpr uint8_t WARNED_UNSUPPORTED_TYPE = 1 << 2;
|
||||
|
||||
// Small state first so it fills the padding before the first pointer
|
||||
uint16_t last_measurement_counter_{0x100}; // Out of range of the 8-bit counter until the first measurement
|
||||
uint8_t warned_{0};
|
||||
|
||||
SUB_SENSOR(co2)
|
||||
SUB_SENSOR(temperature)
|
||||
SUB_SENSOR(humidity)
|
||||
SUB_SENSOR(pressure)
|
||||
SUB_SENSOR(battery_level)
|
||||
SUB_SENSOR(measurement_interval)
|
||||
SUB_SENSOR(measurement_age)
|
||||
SUB_SENSOR(signal_strength)
|
||||
SUB_SENSOR(radon)
|
||||
SUB_SENSOR(radiation_rate)
|
||||
SUB_SENSOR(radiation_total)
|
||||
SUB_SENSOR(radiation_duration)
|
||||
|
||||
protected:
|
||||
uint64_t address_;
|
||||
};
|
||||
|
||||
} // namespace esphome::aranet
|
||||
@@ -0,0 +1,150 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import ble_device_base, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_CO2,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_MAC_ADDRESS,
|
||||
CONF_PRESSURE,
|
||||
CONF_RADON,
|
||||
CONF_SIGNAL_STRENGTH,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_RADON,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_RADIOACTIVE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_DECIBEL_MILLIWATT,
|
||||
UNIT_HECTOPASCAL,
|
||||
UNIT_MICROSILVERTS_PER_HOUR,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PERCENT,
|
||||
UNIT_SECOND,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["ble_device_base"]
|
||||
|
||||
CONF_MEASUREMENT_AGE = "measurement_age"
|
||||
CONF_MEASUREMENT_INTERVAL = "measurement_interval"
|
||||
CONF_RADIATION_RATE = "radiation_rate"
|
||||
CONF_RADIATION_TOTAL = "radiation_total"
|
||||
CONF_RADIATION_DURATION = "radiation_duration"
|
||||
UNIT_NANOSIEVERT = "nSv"
|
||||
|
||||
aranet_ns = cg.esphome_ns.namespace("aranet")
|
||||
Aranet = aranet_ns.class_("Aranet", ble_device_base.ESPBTDeviceListener, cg.Component)
|
||||
|
||||
DIAGNOSTIC_DURATION_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_SECOND,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Aranet),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_CO2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HECTOPASCAL,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_MEASUREMENT_INTERVAL): DIAGNOSTIC_DURATION_SCHEMA,
|
||||
cv.Optional(CONF_MEASUREMENT_AGE): DIAGNOSTIC_DURATION_SCHEMA,
|
||||
cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_DECIBEL_MILLIWATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_RADON): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_RADON,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_RADIATION_RATE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROSILVERTS_PER_HOUR,
|
||||
accuracy_decimals=3,
|
||||
icon=ICON_RADIOACTIVE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_RADIATION_TOTAL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_NANOSIEVERT,
|
||||
accuracy_decimals=0,
|
||||
icon=ICON_RADIOACTIVE,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional(CONF_RADIATION_DURATION): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_SECOND,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(ble_device_base.BLE_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_MAC_ADDRESS].as_hex)
|
||||
await cg.register_component(var, config)
|
||||
await ble_device_base.register_ble_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_CO2, var.set_co2_sensor)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
|
||||
await sensors(CONF_PRESSURE, var.set_pressure_sensor)
|
||||
await sensors(CONF_BATTERY_LEVEL, var.set_battery_level_sensor)
|
||||
await sensors(CONF_MEASUREMENT_INTERVAL, var.set_measurement_interval_sensor)
|
||||
await sensors(CONF_MEASUREMENT_AGE, var.set_measurement_age_sensor)
|
||||
await sensors(CONF_SIGNAL_STRENGTH, var.set_signal_strength_sensor)
|
||||
await sensors(CONF_RADON, var.set_radon_sensor)
|
||||
await sensors(CONF_RADIATION_RATE, var.set_radiation_rate_sensor)
|
||||
await sensors(CONF_RADIATION_TOTAL, var.set_radiation_total_sensor)
|
||||
await sensors(CONF_RADIATION_DURATION, var.set_radiation_duration_sensor)
|
||||
@@ -0,0 +1,7 @@
|
||||
sensor:
|
||||
- platform: aranet
|
||||
mac_address: 00:11:22:33:44:55
|
||||
co2:
|
||||
name: Aranet4 Carbon Dioxide
|
||||
temperature:
|
||||
name: Aranet4 Temperature
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/aranet/aranet.h"
|
||||
|
||||
namespace esphome::aranet::testing {
|
||||
|
||||
constexpr uint8_t MAC_ADDRESS[6] = {0x55, 0x44, 0x33, 0x22, 0x11, 0x00};
|
||||
constexpr uint64_t ADDRESS = 0x001122334455ULL;
|
||||
|
||||
inline const std::vector<uint8_t> ARANET4_PAYLOAD = {
|
||||
0x20, // Smart Home integrations enabled
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x02, // CO2: 600 ppm
|
||||
0xA4, 0x01, // Temperature: 21.0 C (420 / 20)
|
||||
0x94, 0x27, // Pressure: 1013.2 hPa (10132 / 10)
|
||||
45, // Humidity: 45%
|
||||
87, // Battery: 87%
|
||||
0x00, // Display status
|
||||
0x2C, 0x01, // Measurement interval: 300 s
|
||||
12, 0x00, // Measurement age: 12 s
|
||||
7, // Measurement counter
|
||||
};
|
||||
|
||||
inline const std::vector<uint8_t> ARANET2_PAYLOAD = {0x01, 0x21, 0x04, 0x04, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x99, 0x01, 0x00, 0x00, 0x0A, 0x02,
|
||||
0x00, 0x3B, 0x09, 0x78, 0x00, 0x52, 0x00, 0x64};
|
||||
|
||||
inline const std::vector<uint8_t> RADIATION_PAYLOAD = {0x02, 0x21, 0x26, 0x04, 0x01, 0x00, 0xD0, 0x33,
|
||||
0x00, 0x00, 0x6C, 0x60, 0x06, 0x00, 0x82, 0x00,
|
||||
0x00, 0x63, 0x00, 0x2C, 0x01, 0x58, 0x00, 0x72};
|
||||
|
||||
inline const std::vector<uint8_t> RADON_PAYLOAD = {0x03, 0x21, 0x04, 0x06, 0x01, 0x00, 0x00, 0x00,
|
||||
0x07, 0x00, 0xFE, 0x01, 0xC9, 0x27, 0xCE, 0x01,
|
||||
0x00, 0x64, 0x01, 0x58, 0x02, 0xF6, 0x01, 0x08};
|
||||
|
||||
inline ble_device_base::ESPBTDevice make_device(const std::vector<uint8_t> &payload, int rssi = -61,
|
||||
const uint8_t *mac_address = MAC_ADDRESS) {
|
||||
// Manufacturer specific data AD structure for company id 0x0702
|
||||
std::vector<uint8_t> advertisement{static_cast<uint8_t>(payload.size() + 3), 0xFF, 0x02, 0x07};
|
||||
advertisement.insert(advertisement.end(), payload.begin(), payload.end());
|
||||
|
||||
ble_device_base::ESPBTDevice device;
|
||||
device.from_scan_result(mac_address, rssi, 0, advertisement.data(), advertisement.size());
|
||||
return device;
|
||||
}
|
||||
|
||||
struct Harness {
|
||||
Harness() {
|
||||
this->aranet.set_co2_sensor(&this->co2);
|
||||
this->aranet.set_temperature_sensor(&this->temperature);
|
||||
this->aranet.set_humidity_sensor(&this->humidity);
|
||||
this->aranet.set_pressure_sensor(&this->pressure);
|
||||
this->aranet.set_battery_level_sensor(&this->battery);
|
||||
this->aranet.set_measurement_interval_sensor(&this->interval);
|
||||
this->aranet.set_measurement_age_sensor(&this->age);
|
||||
this->aranet.set_signal_strength_sensor(&this->signal_strength);
|
||||
this->aranet.set_radon_sensor(&this->radon);
|
||||
this->aranet.set_radiation_rate_sensor(&this->radiation_rate);
|
||||
this->aranet.set_radiation_total_sensor(&this->radiation_total);
|
||||
this->aranet.set_radiation_duration_sensor(&this->radiation_duration);
|
||||
}
|
||||
|
||||
Aranet aranet{ADDRESS};
|
||||
sensor::Sensor co2, temperature, humidity, pressure, battery, interval, age, signal_strength, radon, radiation_rate,
|
||||
radiation_total, radiation_duration;
|
||||
};
|
||||
|
||||
} // namespace esphome::aranet::testing
|
||||
@@ -0,0 +1,27 @@
|
||||
sensor:
|
||||
- platform: aranet
|
||||
mac_address: 00:11:22:33:44:55
|
||||
co2:
|
||||
name: Aranet4 Carbon Dioxide
|
||||
temperature:
|
||||
name: Aranet4 Temperature
|
||||
humidity:
|
||||
name: Aranet4 Humidity
|
||||
pressure:
|
||||
name: Aranet4 Pressure
|
||||
battery_level:
|
||||
name: Aranet4 Battery Level
|
||||
measurement_interval:
|
||||
name: Aranet4 Measurement Interval
|
||||
measurement_age:
|
||||
name: Aranet4 Measurement Age
|
||||
signal_strength:
|
||||
name: Aranet4 Signal Strength
|
||||
radon:
|
||||
name: Aranet Radon Concentration
|
||||
radiation_rate:
|
||||
name: Aranet Radiation Dose Rate
|
||||
radiation_total:
|
||||
name: Aranet Radiation Total Dose
|
||||
radiation_duration:
|
||||
name: Aranet Radiation Dose Duration
|
||||
@@ -0,0 +1,144 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::aranet::testing {
|
||||
namespace {
|
||||
|
||||
TEST(Aranet, DecodesAranet4) {
|
||||
Harness h;
|
||||
auto device = make_device(ARANET4_PAYLOAD);
|
||||
ASSERT_EQ(device.address_uint64(), ADDRESS);
|
||||
|
||||
EXPECT_TRUE(h.aranet.parse_device(device));
|
||||
EXPECT_FLOAT_EQ(h.co2.state, 600.0f);
|
||||
EXPECT_FLOAT_EQ(h.temperature.state, 21.0f);
|
||||
EXPECT_FLOAT_EQ(h.humidity.state, 45.0f);
|
||||
EXPECT_FLOAT_EQ(h.pressure.state, 1013.2f);
|
||||
EXPECT_FLOAT_EQ(h.battery.state, 87.0f);
|
||||
EXPECT_FLOAT_EQ(h.interval.state, 300.0f);
|
||||
EXPECT_FLOAT_EQ(h.age.state, 12.0f);
|
||||
EXPECT_FLOAT_EQ(h.signal_strength.state, -61.0f);
|
||||
EXPECT_FALSE(h.radon.has_state());
|
||||
EXPECT_FALSE(h.radiation_rate.has_state());
|
||||
EXPECT_FALSE(h.radiation_total.has_state());
|
||||
EXPECT_FALSE(h.radiation_duration.has_state());
|
||||
}
|
||||
|
||||
TEST(Aranet, UpdatesDiagnosticsForRepeatedMeasurement) {
|
||||
Harness h;
|
||||
int co2_updates = 0;
|
||||
int age_updates = 0;
|
||||
int signal_strength_updates = 0;
|
||||
h.co2.add_on_state_callback([&co2_updates](float) { co2_updates++; });
|
||||
h.age.add_on_state_callback([&age_updates](float) { age_updates++; });
|
||||
h.signal_strength.add_on_state_callback([&signal_strength_updates](float) { signal_strength_updates++; });
|
||||
|
||||
EXPECT_TRUE(h.aranet.parse_device(make_device(ARANET4_PAYLOAD, -61)));
|
||||
auto payload = ARANET4_PAYLOAD;
|
||||
payload[19] = 18;
|
||||
EXPECT_TRUE(h.aranet.parse_device(make_device(payload, -54)));
|
||||
|
||||
EXPECT_EQ(co2_updates, 1);
|
||||
EXPECT_EQ(age_updates, 2);
|
||||
EXPECT_EQ(signal_strength_updates, 2);
|
||||
EXPECT_FLOAT_EQ(h.age.state, 18.0f);
|
||||
EXPECT_FLOAT_EQ(h.signal_strength.state, -54.0f);
|
||||
}
|
||||
|
||||
TEST(Aranet, RejectsDisabledIntegrationAndMalformedPayload) {
|
||||
Harness h;
|
||||
auto payload = ARANET4_PAYLOAD;
|
||||
|
||||
payload[0] = 0;
|
||||
EXPECT_FALSE(h.aranet.parse_device(make_device(payload)));
|
||||
|
||||
payload.pop_back();
|
||||
EXPECT_FALSE(h.aranet.parse_device(make_device(payload)));
|
||||
EXPECT_FALSE(h.co2.has_state());
|
||||
EXPECT_FALSE(h.signal_strength.has_state());
|
||||
}
|
||||
|
||||
TEST(Aranet, RejectsMismatchedAddressAndUnsupportedDeviceType) {
|
||||
Harness h;
|
||||
constexpr uint8_t other_mac_address[6] = {0x66, 0x44, 0x33, 0x22, 0x11, 0x00};
|
||||
EXPECT_FALSE(h.aranet.parse_device(make_device(ARANET4_PAYLOAD, -61, other_mac_address)));
|
||||
|
||||
auto payload = ARANET4_PAYLOAD;
|
||||
payload.insert(payload.begin(), 0x09);
|
||||
payload.push_back(0x00);
|
||||
EXPECT_FALSE(h.aranet.parse_device(make_device(payload)));
|
||||
|
||||
// Type 0 is Aranet4, which never sends the 24 byte layout; the second warning stays quiet
|
||||
payload[0] = 0x00;
|
||||
EXPECT_FALSE(h.aranet.parse_device(make_device(payload)));
|
||||
EXPECT_FALSE(h.signal_strength.has_state());
|
||||
}
|
||||
|
||||
TEST(Aranet, DecodesAranet2) {
|
||||
Harness h;
|
||||
int temperature_updates = 0;
|
||||
h.temperature.add_on_state_callback([&temperature_updates](float) { temperature_updates++; });
|
||||
|
||||
EXPECT_TRUE(h.aranet.parse_device(make_device(ARANET2_PAYLOAD)));
|
||||
EXPECT_NEAR(h.temperature.state, 20.45f, 0.001f);
|
||||
EXPECT_FLOAT_EQ(h.humidity.state, 52.2f);
|
||||
EXPECT_FLOAT_EQ(h.battery.state, 59.0f);
|
||||
EXPECT_FLOAT_EQ(h.interval.state, 120.0f);
|
||||
EXPECT_FLOAT_EQ(h.age.state, 82.0f);
|
||||
EXPECT_FALSE(h.co2.has_state());
|
||||
EXPECT_FALSE(h.pressure.has_state());
|
||||
|
||||
auto repeated_payload = ARANET2_PAYLOAD;
|
||||
repeated_payload[10] = 0x00;
|
||||
repeated_payload[11] = 0x00;
|
||||
EXPECT_TRUE(h.aranet.parse_device(make_device(repeated_payload)));
|
||||
EXPECT_EQ(temperature_updates, 1);
|
||||
}
|
||||
|
||||
TEST(Aranet, DecodesAranetRadiation) {
|
||||
Harness h;
|
||||
EXPECT_TRUE(h.aranet.parse_device(make_device(RADIATION_PAYLOAD)));
|
||||
EXPECT_FLOAT_EQ(h.radiation_rate.state, 0.13f);
|
||||
EXPECT_FLOAT_EQ(h.radiation_total.state, 13264.0f);
|
||||
EXPECT_FLOAT_EQ(h.radiation_duration.state, 417900.0f);
|
||||
EXPECT_FLOAT_EQ(h.battery.state, 99.0f);
|
||||
EXPECT_FLOAT_EQ(h.interval.state, 300.0f);
|
||||
EXPECT_FLOAT_EQ(h.age.state, 88.0f);
|
||||
EXPECT_FALSE(h.co2.has_state());
|
||||
EXPECT_FALSE(h.temperature.has_state());
|
||||
EXPECT_FALSE(h.humidity.has_state());
|
||||
EXPECT_FALSE(h.pressure.has_state());
|
||||
EXPECT_FALSE(h.radon.has_state());
|
||||
}
|
||||
|
||||
TEST(Aranet, DecodesAranetRadon) {
|
||||
Harness h;
|
||||
EXPECT_TRUE(h.aranet.parse_device(make_device(RADON_PAYLOAD)));
|
||||
EXPECT_FLOAT_EQ(h.radon.state, 7.0f);
|
||||
EXPECT_FLOAT_EQ(h.temperature.state, 25.5f);
|
||||
EXPECT_FLOAT_EQ(h.humidity.state, 46.2f);
|
||||
EXPECT_FLOAT_EQ(h.pressure.state, 1018.5f);
|
||||
EXPECT_FALSE(h.co2.has_state());
|
||||
EXPECT_FALSE(h.radiation_rate.has_state());
|
||||
EXPECT_FALSE(h.radiation_total.has_state());
|
||||
EXPECT_FALSE(h.radiation_duration.has_state());
|
||||
}
|
||||
|
||||
TEST(Aranet, RadonOneSkipsZeroTemperatureAndHumidity) {
|
||||
Harness h;
|
||||
auto payload = RADON_PAYLOAD;
|
||||
payload[10] = payload[11] = 0x00;
|
||||
payload[14] = payload[15] = 0x00;
|
||||
EXPECT_TRUE(h.aranet.parse_device(make_device(payload)));
|
||||
EXPECT_FLOAT_EQ(h.radon.state, 7.0f);
|
||||
EXPECT_FLOAT_EQ(h.pressure.state, 1018.5f);
|
||||
EXPECT_FALSE(h.temperature.has_state());
|
||||
EXPECT_FALSE(h.humidity.has_state());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace esphome::aranet::testing
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
ble: !include ../../test_build_components/common/ble/esp32-idf.yaml
|
||||
aranet: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
|
||||
aranet: !include common-ln.yaml
|
||||
@@ -0,0 +1,12 @@
|
||||
# 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_hub
|
||||
|
||||
sensor:
|
||||
- platform: aranet
|
||||
ble_hub_id: ble_hub
|
||||
mac_address: 00:11:22:33:44:55
|
||||
co2:
|
||||
name: BK Aranet4 Carbon Dioxide
|
||||
Reference in New Issue
Block a user