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