mirror of
https://github.com/esphome/esphome.git
synced 2025-10-30 14:43:51 +00:00
Merge branch 'dev' into bump-1.18.0b1
This commit is contained in:
@@ -19,7 +19,7 @@ from esphome.const import (
|
||||
CONF_PLATFORMIO_OPTIONS,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority
|
||||
from esphome.helpers import color, indent
|
||||
from esphome.helpers import indent
|
||||
from esphome.util import (
|
||||
run_external_command,
|
||||
run_external_process,
|
||||
@@ -27,6 +27,7 @@ from esphome.util import (
|
||||
list_yaml_files,
|
||||
get_serial_ports,
|
||||
)
|
||||
from esphome.log import color, setup_log, Fore
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,7 +58,7 @@ def choose_prompt(options):
|
||||
raise ValueError
|
||||
break
|
||||
except ValueError:
|
||||
safe_print(color("red", f"Invalid option: '{opt}'"))
|
||||
safe_print(color(Fore.RED, f"Invalid option: '{opt}'"))
|
||||
return options[opt - 1][1]
|
||||
|
||||
|
||||
@@ -263,46 +264,6 @@ def clean_mqtt(config, args):
|
||||
)
|
||||
|
||||
|
||||
def setup_log(debug=False, quiet=False):
|
||||
if debug:
|
||||
log_level = logging.DEBUG
|
||||
CORE.verbose = True
|
||||
elif quiet:
|
||||
log_level = logging.CRITICAL
|
||||
else:
|
||||
log_level = logging.INFO
|
||||
logging.basicConfig(level=log_level)
|
||||
fmt = "%(levelname)s %(message)s"
|
||||
colorfmt = f"%(log_color)s{fmt}%(reset)s"
|
||||
datefmt = "%H:%M:%S"
|
||||
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
|
||||
try:
|
||||
import colorama
|
||||
|
||||
colorama.init(strip=True)
|
||||
|
||||
from colorlog import ColoredFormatter
|
||||
|
||||
logging.getLogger().handlers[0].setFormatter(
|
||||
ColoredFormatter(
|
||||
colorfmt,
|
||||
datefmt=datefmt,
|
||||
reset=True,
|
||||
log_colors={
|
||||
"DEBUG": "cyan",
|
||||
"INFO": "green",
|
||||
"WARNING": "yellow",
|
||||
"ERROR": "red",
|
||||
"CRITICAL": "red",
|
||||
},
|
||||
)
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def command_wizard(args):
|
||||
from esphome import wizard
|
||||
|
||||
@@ -442,30 +403,30 @@ def command_update_all(args):
|
||||
click.echo(f"{half_line}{middle_text}{half_line}")
|
||||
|
||||
for f in files:
|
||||
print("Updating {}".format(color("cyan", f)))
|
||||
print("Updating {}".format(color(Fore.CYAN, f)))
|
||||
print("-" * twidth)
|
||||
print()
|
||||
rc = run_external_process(
|
||||
"esphome", "--dashboard", f, "run", "--no-logs", "--upload-port", "OTA"
|
||||
)
|
||||
if rc == 0:
|
||||
print_bar("[{}] {}".format(color("bold_green", "SUCCESS"), f))
|
||||
print_bar("[{}] {}".format(color(Fore.BOLD_GREEN, "SUCCESS"), f))
|
||||
success[f] = True
|
||||
else:
|
||||
print_bar("[{}] {}".format(color("bold_red", "ERROR"), f))
|
||||
print_bar("[{}] {}".format(color(Fore.BOLD_RED, "ERROR"), f))
|
||||
success[f] = False
|
||||
|
||||
print()
|
||||
print()
|
||||
print()
|
||||
|
||||
print_bar("[{}]".format(color("bold_white", "SUMMARY")))
|
||||
print_bar("[{}]".format(color(Fore.BOLD_WHITE, "SUMMARY")))
|
||||
failed = 0
|
||||
for f in files:
|
||||
if success[f]:
|
||||
print(" - {}: {}".format(f, color("green", "SUCCESS")))
|
||||
print(" - {}: {}".format(f, color(Fore.GREEN, "SUCCESS")))
|
||||
else:
|
||||
print(" - {}: {}".format(f, color("bold_red", "FAILED")))
|
||||
print(" - {}: {}".format(f, color(Fore.BOLD_RED, "FAILED")))
|
||||
failed += 1
|
||||
return failed
|
||||
|
||||
@@ -638,10 +599,10 @@ def run_esphome(argv):
|
||||
_LOGGER.error("Missing configuration parameter, see esphome --help.")
|
||||
return 1
|
||||
|
||||
if sys.version_info < (3, 6, 0):
|
||||
if sys.version_info < (3, 7, 0):
|
||||
_LOGGER.error(
|
||||
"You're running ESPHome with Python <3.6. ESPHome is no longer compatible "
|
||||
"with this Python version. Please reinstall ESPHome with Python 3.6+"
|
||||
"You're running ESPHome with Python <3.7. ESPHome is no longer compatible "
|
||||
"with this Python version. Please reinstall ESPHome with Python 3.7+"
|
||||
)
|
||||
return 1
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ from esphome import const
|
||||
import esphome.api.api_pb2 as pb
|
||||
from esphome.const import CONF_PASSWORD, CONF_PORT
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.helpers import resolve_ip_address, indent, color
|
||||
from esphome.helpers import resolve_ip_address, indent
|
||||
from esphome.log import color, Fore
|
||||
from esphome.util import safe_print
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -488,7 +489,7 @@ def run_logs(config, address):
|
||||
text = msg.message
|
||||
if msg.send_failed:
|
||||
text = color(
|
||||
"white",
|
||||
Fore.WHITE,
|
||||
"(Message skipped because it was too big to fit in "
|
||||
"TCP buffer - This is only cosmetic)",
|
||||
)
|
||||
|
||||
@@ -58,6 +58,24 @@ void ATM90E32Component::update() {
|
||||
if (this->phase_[2].power_factor_sensor_ != nullptr) {
|
||||
this->phase_[2].power_factor_sensor_->publish_state(this->get_power_factor_c_());
|
||||
}
|
||||
if (this->phase_[0].forward_active_energy_sensor_ != nullptr) {
|
||||
this->phase_[0].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_a_());
|
||||
}
|
||||
if (this->phase_[1].forward_active_energy_sensor_ != nullptr) {
|
||||
this->phase_[1].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_b_());
|
||||
}
|
||||
if (this->phase_[2].forward_active_energy_sensor_ != nullptr) {
|
||||
this->phase_[2].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_c_());
|
||||
}
|
||||
if (this->phase_[0].reverse_active_energy_sensor_ != nullptr) {
|
||||
this->phase_[0].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_a_());
|
||||
}
|
||||
if (this->phase_[1].reverse_active_energy_sensor_ != nullptr) {
|
||||
this->phase_[1].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_b_());
|
||||
}
|
||||
if (this->phase_[2].reverse_active_energy_sensor_ != nullptr) {
|
||||
this->phase_[2].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_c_());
|
||||
}
|
||||
if (this->freq_sensor_ != nullptr) {
|
||||
this->freq_sensor_->publish_state(this->get_frequency_());
|
||||
}
|
||||
@@ -119,16 +137,22 @@ void ATM90E32Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Power A", this->phase_[0].power_sensor_);
|
||||
LOG_SENSOR(" ", "Reactive Power A", this->phase_[0].reactive_power_sensor_);
|
||||
LOG_SENSOR(" ", "PF A", this->phase_[0].power_factor_sensor_);
|
||||
LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[0].forward_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[0].reverse_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Voltage B", this->phase_[1].voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current B", this->phase_[1].current_sensor_);
|
||||
LOG_SENSOR(" ", "Power B", this->phase_[1].power_sensor_);
|
||||
LOG_SENSOR(" ", "Reactive Power B", this->phase_[1].reactive_power_sensor_);
|
||||
LOG_SENSOR(" ", "PF B", this->phase_[1].power_factor_sensor_);
|
||||
LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[1].forward_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[1].reverse_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Voltage C", this->phase_[2].voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current C", this->phase_[2].current_sensor_);
|
||||
LOG_SENSOR(" ", "Power C", this->phase_[2].power_sensor_);
|
||||
LOG_SENSOR(" ", "Reactive Power C", this->phase_[2].reactive_power_sensor_);
|
||||
LOG_SENSOR(" ", "PF C", this->phase_[2].power_factor_sensor_);
|
||||
LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[2].forward_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[2].reverse_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Frequency", this->freq_sensor_);
|
||||
LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_);
|
||||
}
|
||||
@@ -239,6 +263,30 @@ float ATM90E32Component::get_power_factor_c_() {
|
||||
int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANC);
|
||||
return (float) pf / 1000;
|
||||
}
|
||||
float ATM90E32Component::get_forward_active_energy_a_() {
|
||||
uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYA);
|
||||
return (float) val * 10 / 3200; // convert register value to WattHours
|
||||
}
|
||||
float ATM90E32Component::get_forward_active_energy_b_() {
|
||||
uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYB);
|
||||
return (float) val * 10 / 3200;
|
||||
}
|
||||
float ATM90E32Component::get_forward_active_energy_c_() {
|
||||
uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYC);
|
||||
return (float) val * 10 / 3200;
|
||||
}
|
||||
float ATM90E32Component::get_reverse_active_energy_a_() {
|
||||
uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYA);
|
||||
return (float) val * 10 / 3200;
|
||||
}
|
||||
float ATM90E32Component::get_reverse_active_energy_b_() {
|
||||
uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYB);
|
||||
return (float) val * 10 / 3200;
|
||||
}
|
||||
float ATM90E32Component::get_reverse_active_energy_c_() {
|
||||
uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYC);
|
||||
return (float) val * 10 / 3200;
|
||||
}
|
||||
float ATM90E32Component::get_frequency_() {
|
||||
uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ);
|
||||
return (float) freq / 100;
|
||||
|
||||
@@ -20,6 +20,12 @@ class ATM90E32Component : public PollingComponent,
|
||||
void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; }
|
||||
void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; }
|
||||
void set_reactive_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].reactive_power_sensor_ = obj; }
|
||||
void set_forward_active_energy_sensor(int phase, sensor::Sensor *obj) {
|
||||
this->phase_[phase].forward_active_energy_sensor_ = obj;
|
||||
}
|
||||
void set_reverse_active_energy_sensor(int phase, sensor::Sensor *obj) {
|
||||
this->phase_[phase].reverse_active_energy_sensor_ = obj;
|
||||
}
|
||||
void set_power_factor_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_factor_sensor_ = obj; }
|
||||
void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].volt_gain_ = gain; }
|
||||
void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; }
|
||||
@@ -52,6 +58,12 @@ class ATM90E32Component : public PollingComponent,
|
||||
float get_power_factor_a_();
|
||||
float get_power_factor_b_();
|
||||
float get_power_factor_c_();
|
||||
float get_forward_active_energy_a_();
|
||||
float get_forward_active_energy_b_();
|
||||
float get_forward_active_energy_c_();
|
||||
float get_reverse_active_energy_a_();
|
||||
float get_reverse_active_energy_b_();
|
||||
float get_reverse_active_energy_c_();
|
||||
float get_frequency_();
|
||||
float get_chip_temperature_();
|
||||
|
||||
@@ -63,6 +75,8 @@ class ATM90E32Component : public PollingComponent,
|
||||
sensor::Sensor *power_sensor_{nullptr};
|
||||
sensor::Sensor *reactive_power_sensor_{nullptr};
|
||||
sensor::Sensor *power_factor_sensor_{nullptr};
|
||||
sensor::Sensor *forward_active_energy_sensor_{nullptr};
|
||||
sensor::Sensor *reverse_active_energy_sensor_{nullptr};
|
||||
} phase_[3];
|
||||
sensor::Sensor *freq_sensor_{nullptr};
|
||||
sensor::Sensor *chip_temperature_sensor_{nullptr};
|
||||
|
||||
@@ -8,8 +8,11 @@ from esphome.const import (
|
||||
CONF_POWER,
|
||||
CONF_POWER_FACTOR,
|
||||
CONF_FREQUENCY,
|
||||
CONF_FORWARD_ACTIVE_ENERGY,
|
||||
CONF_REVERSE_ACTIVE_ENERGY,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -24,6 +27,7 @@ from esphome.const import (
|
||||
UNIT_EMPTY,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_VOLT_AMPS_REACTIVE,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
|
||||
CONF_PHASE_A = "phase_a"
|
||||
@@ -73,6 +77,12 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
|
||||
UNIT_EMPTY, ICON_EMPTY, 2, DEVICE_CLASS_POWER_FACTOR
|
||||
),
|
||||
cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema(
|
||||
UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY
|
||||
),
|
||||
cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema(
|
||||
UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY
|
||||
),
|
||||
cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t,
|
||||
cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t,
|
||||
}
|
||||
@@ -129,6 +139,12 @@ def to_code(config):
|
||||
if CONF_POWER_FACTOR in conf:
|
||||
sens = yield sensor.new_sensor(conf[CONF_POWER_FACTOR])
|
||||
cg.add(var.set_power_factor_sensor(i, sens))
|
||||
if CONF_FORWARD_ACTIVE_ENERGY in conf:
|
||||
sens = yield sensor.new_sensor(conf[CONF_FORWARD_ACTIVE_ENERGY])
|
||||
cg.add(var.set_forward_active_energy_sensor(i, sens))
|
||||
if CONF_REVERSE_ACTIVE_ENERGY in conf:
|
||||
sens = yield sensor.new_sensor(conf[CONF_REVERSE_ACTIVE_ENERGY])
|
||||
cg.add(var.set_reverse_active_energy_sensor(i, sens))
|
||||
if CONF_FREQUENCY in config:
|
||||
sens = yield sensor.new_sensor(config[CONF_FREQUENCY])
|
||||
cg.add(var.set_freq_sensor(sens))
|
||||
|
||||
0
esphome/components/b_parasite/__init__.py
Normal file
0
esphome/components/b_parasite/__init__.py
Normal file
82
esphome/components/b_parasite/b_parasite.cpp
Normal file
82
esphome/components/b_parasite/b_parasite.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include "b_parasite.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace b_parasite {
|
||||
|
||||
static const char* TAG = "b_parasite";
|
||||
|
||||
void BParasite::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "b_parasite");
|
||||
LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Soil Moisture", this->soil_moisture_);
|
||||
}
|
||||
|
||||
bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice& device) {
|
||||
if (device.address_uint64() != address_) {
|
||||
ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
|
||||
const auto& service_datas = device.get_service_datas();
|
||||
if (service_datas.size() != 1) {
|
||||
ESP_LOGE(TAG, "Unexpected service_datas size (%d)", service_datas.size());
|
||||
return false;
|
||||
}
|
||||
const auto& service_data = service_datas[0];
|
||||
|
||||
ESP_LOGVV(TAG, "Service data:");
|
||||
for (const uint8_t byte : service_data.data) {
|
||||
ESP_LOGVV(TAG, "0x%02x", byte);
|
||||
}
|
||||
|
||||
const auto& data = service_data.data;
|
||||
|
||||
// Counter for deduplicating messages.
|
||||
uint8_t counter = data[1] & 0x0f;
|
||||
if (last_processed_counter_ == counter) {
|
||||
ESP_LOGVV(TAG, "Skipping already processed counter (%u)", counter);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Battery voltage in millivolts.
|
||||
uint16_t battery_millivolt = data[2] << 8 | data[3];
|
||||
float battery_voltage = battery_millivolt / 1000.0f;
|
||||
|
||||
// Temperature in 1000 * Celcius.
|
||||
uint16_t temp_millicelcius = data[4] << 8 | data[5];
|
||||
float temp_celcius = temp_millicelcius / 1000.0f;
|
||||
|
||||
// Relative air humidity in the range [0, 2^16).
|
||||
uint16_t humidity = data[6] << 8 | data[7];
|
||||
float humidity_percent = (100.0f * humidity) / (1 << 16);
|
||||
|
||||
// Relative soil moisture in [0 - 2^16).
|
||||
uint16_t soil_moisture = data[8] << 8 | data[9];
|
||||
float moisture_percent = (100.0f * soil_moisture) / (1 << 16);
|
||||
|
||||
if (battery_voltage_ != nullptr) {
|
||||
battery_voltage_->publish_state(battery_voltage);
|
||||
}
|
||||
if (temperature_ != nullptr) {
|
||||
temperature_->publish_state(temp_celcius);
|
||||
}
|
||||
if (humidity_ != nullptr) {
|
||||
humidity_->publish_state(humidity_percent);
|
||||
}
|
||||
if (soil_moisture_ != nullptr) {
|
||||
soil_moisture_->publish_state(moisture_percent);
|
||||
}
|
||||
|
||||
last_processed_counter_ = counter;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace b_parasite
|
||||
} // namespace esphome
|
||||
|
||||
#endif // ARDUINO_ARCH_ESP32
|
||||
40
esphome/components/b_parasite/b_parasite.h
Normal file
40
esphome/components/b_parasite/b_parasite.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace b_parasite {
|
||||
|
||||
class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
|
||||
public:
|
||||
void set_address(uint64_t address) { address_ = address; };
|
||||
void set_bindkey(const std::string &bindkey);
|
||||
|
||||
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; }
|
||||
void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
|
||||
void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
|
||||
void set_soil_moisture(sensor::Sensor *soil_moisture) { soil_moisture_ = soil_moisture; }
|
||||
|
||||
protected:
|
||||
// The received advertisement packet contains an unsigned 4 bits wrap-around counter
|
||||
// for deduplicating messages.
|
||||
int8_t last_processed_counter_ = -1;
|
||||
uint64_t address_;
|
||||
sensor::Sensor *battery_voltage_{nullptr};
|
||||
sensor::Sensor *temperature_{nullptr};
|
||||
sensor::Sensor *humidity_{nullptr};
|
||||
sensor::Sensor *soil_moisture_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace b_parasite
|
||||
} // namespace esphome
|
||||
|
||||
#endif // ARDUINO_ARCH_ESP32
|
||||
68
esphome/components/b_parasite/sensor.py
Normal file
68
esphome/components/b_parasite/sensor.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, esp32_ble_tracker
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_VOLTAGE,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_MOISTURE,
|
||||
CONF_MAC_ADDRESS,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ICON_EMPTY,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@rbaron"]
|
||||
|
||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||
|
||||
b_parasite_ns = cg.esphome_ns.namespace("b_parasite")
|
||||
BParasite = b_parasite_ns.class_(
|
||||
"BParasite", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BParasite),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
|
||||
UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE
|
||||
),
|
||||
cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
|
||||
UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
yield cg.register_component(var, config)
|
||||
yield esp32_ble_tracker.register_ble_device(var, config)
|
||||
|
||||
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||
|
||||
for (config_key, setter) in [
|
||||
(CONF_TEMPERATURE, var.set_temperature),
|
||||
(CONF_HUMIDITY, var.set_humidity),
|
||||
(CONF_BATTERY_VOLTAGE, var.set_battery_voltage),
|
||||
(CONF_MOISTURE, var.set_soil_moisture),
|
||||
]:
|
||||
if config_key in config:
|
||||
sens = yield sensor.new_sensor(config[config_key])
|
||||
cg.add(setter(sens))
|
||||
87
esphome/components/ble_client/__init__.py
Normal file
87
esphome/components/ble_client/__init__.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import esp32_ble_tracker
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MAC_ADDRESS,
|
||||
CONF_NAME,
|
||||
CONF_ON_CONNECT,
|
||||
CONF_ON_DISCONNECT,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
from esphome.core import coroutine
|
||||
from esphome import automation
|
||||
|
||||
CODEOWNERS = ["@buxtronix"]
|
||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||
|
||||
ble_client_ns = cg.esphome_ns.namespace("ble_client")
|
||||
BLEClient = ble_client_ns.class_(
|
||||
"BLEClient", cg.Component, esp32_ble_tracker.ESPBTClient
|
||||
)
|
||||
BLEClientNode = ble_client_ns.class_("BLEClientNode")
|
||||
BLEClientNodeConstRef = BLEClientNode.operator("ref").operator("const")
|
||||
# Triggers
|
||||
BLEClientConnectTrigger = ble_client_ns.class_(
|
||||
"BLEClientConnectTrigger", automation.Trigger.template(BLEClientNodeConstRef)
|
||||
)
|
||||
BLEClientDisconnectTrigger = ble_client_ns.class_(
|
||||
"BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef)
|
||||
)
|
||||
|
||||
# Espressif platformio framework is built with MAX_BLE_CONN to 3, so
|
||||
# enforce this in yaml checks.
|
||||
MULTI_CONF = 3
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BLEClient),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_NAME): cv.string,
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
BLEClientConnectTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
BLEClientDisconnectTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
CONF_BLE_CLIENT_ID = "ble_client_id"
|
||||
|
||||
BLE_CLIENT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_BLE_CLIENT_ID): cv.use_id(BLEClient),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@coroutine
|
||||
def register_ble_node(var, config):
|
||||
parent = yield cg.get_variable(config[CONF_BLE_CLIENT_ID])
|
||||
cg.add(parent.register_ble_node(var))
|
||||
|
||||
|
||||
def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
yield cg.register_component(var, config)
|
||||
yield esp32_ble_tracker.register_client(var, config)
|
||||
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||
for conf in config.get(CONF_ON_CONNECT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
yield automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_DISCONNECT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
yield automation.build_automation(trigger, [], conf)
|
||||
37
esphome/components/ble_client/automation.h
Normal file
37
esphome/components/ble_client/automation.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode {
|
||||
public:
|
||||
explicit BLEClientConnectTrigger(BLEClient *parent) { parent->register_ble_node(this); }
|
||||
void loop() override {}
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
|
||||
if (event == ESP_GATTC_OPEN_EVT && param->open.status == ESP_GATT_OK)
|
||||
this->trigger();
|
||||
if (event == ESP_GATTC_SEARCH_CMPL_EVT)
|
||||
this->node_state = espbt::ClientState::Established;
|
||||
}
|
||||
};
|
||||
|
||||
class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode {
|
||||
public:
|
||||
explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); }
|
||||
void loop() override {}
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
|
||||
if (event == ESP_GATTC_DISCONNECT_EVT && memcmp(param->disconnect.remote_bda, this->parent_->remote_bda, 6) == 0)
|
||||
this->trigger();
|
||||
if (event == ESP_GATTC_SEARCH_CMPL_EVT)
|
||||
this->node_state = espbt::ClientState::Established;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
392
esphome/components/ble_client/ble_client.cpp
Normal file
392
esphome/components/ble_client/ble_client.cpp
Normal file
@@ -0,0 +1,392 @@
|
||||
#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"
|
||||
#include "ble_client.h"
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
|
||||
static const char *TAG = "ble_client";
|
||||
|
||||
void BLEClient::setup() {
|
||||
auto ret = esp_ble_gattc_app_register(this->app_id);
|
||||
if (ret) {
|
||||
ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret);
|
||||
this->mark_failed();
|
||||
}
|
||||
this->set_states(espbt::ClientState::Idle);
|
||||
this->enabled = true;
|
||||
}
|
||||
|
||||
void BLEClient::loop() {
|
||||
if (this->state() == espbt::ClientState::Discovered) {
|
||||
this->connect();
|
||||
}
|
||||
for (auto *node : this->nodes_)
|
||||
node->loop();
|
||||
}
|
||||
|
||||
void BLEClient::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BLE Client:");
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str());
|
||||
}
|
||||
|
||||
bool BLEClient::parse_device(const espbt::ESPBTDevice &device) {
|
||||
if (!this->enabled)
|
||||
return false;
|
||||
if (device.address_uint64() != this->address)
|
||||
return false;
|
||||
if (this->state() != espbt::ClientState::Idle)
|
||||
return false;
|
||||
|
||||
ESP_LOGD(TAG, "Found device at MAC address [%s]", device.address_str().c_str());
|
||||
this->set_states(espbt::ClientState::Discovered);
|
||||
|
||||
auto addr = device.address_uint64();
|
||||
this->remote_bda[0] = (addr >> 40) & 0xFF;
|
||||
this->remote_bda[1] = (addr >> 32) & 0xFF;
|
||||
this->remote_bda[2] = (addr >> 24) & 0xFF;
|
||||
this->remote_bda[3] = (addr >> 16) & 0xFF;
|
||||
this->remote_bda[4] = (addr >> 8) & 0xFF;
|
||||
this->remote_bda[5] = (addr >> 0) & 0xFF;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string BLEClient::address_str() const {
|
||||
char buf[20];
|
||||
sprintf(buf, "%02x:%02x:%02x:%02x:%02x:%02x", (uint8_t)(this->address >> 40) & 0xff,
|
||||
(uint8_t)(this->address >> 32) & 0xff, (uint8_t)(this->address >> 24) & 0xff,
|
||||
(uint8_t)(this->address >> 16) & 0xff, (uint8_t)(this->address >> 8) & 0xff,
|
||||
(uint8_t)(this->address >> 0) & 0xff);
|
||||
std::string ret;
|
||||
ret = buf;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void BLEClient::set_enabled(bool enabled) {
|
||||
if (enabled == this->enabled)
|
||||
return;
|
||||
if (!enabled && this->state() != espbt::ClientState::Idle) {
|
||||
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str());
|
||||
auto ret = esp_ble_gattc_close(this->gattc_if, this->conn_id);
|
||||
if (ret) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s status=%d", this->address_str().c_str(), ret);
|
||||
}
|
||||
}
|
||||
this->enabled = enabled;
|
||||
}
|
||||
|
||||
void BLEClient::connect() {
|
||||
ESP_LOGI(TAG, "Attempting BLE connection to %s", this->address_str().c_str());
|
||||
auto ret = esp_ble_gattc_open(this->gattc_if, this->remote_bda, BLE_ADDR_TYPE_PUBLIC, true);
|
||||
if (ret) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_open error, address=%s status=%d", this->address_str().c_str(), ret);
|
||||
this->set_states(espbt::ClientState::Idle);
|
||||
} else {
|
||||
this->set_states(espbt::ClientState::Connecting);
|
||||
}
|
||||
}
|
||||
|
||||
void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
|
||||
return;
|
||||
if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && gattc_if != this->gattc_if)
|
||||
return;
|
||||
|
||||
bool all_established = this->all_nodes_established();
|
||||
|
||||
switch (event) {
|
||||
case ESP_GATTC_REG_EVT: {
|
||||
if (param->reg.status == ESP_GATT_OK) {
|
||||
ESP_LOGV(TAG, "gattc registered app id %d", this->app_id);
|
||||
this->gattc_if = esp_gattc_if;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "gattc app registration failed id=%d code=%d", param->reg.app_id, param->reg.status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str());
|
||||
if (param->open.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "connect to %s failed, status=%d", this->address_str().c_str(), param->open.status);
|
||||
this->set_states(espbt::ClientState::Idle);
|
||||
break;
|
||||
}
|
||||
this->conn_id = param->open.conn_id;
|
||||
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->open.conn_id);
|
||||
if (ret) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%d", ret);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CFG_MTU_EVT: {
|
||||
if (param->cfg_mtu.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "cfg_mtu to %s failed, status %d", this->address_str().c_str(), param->cfg_mtu.status);
|
||||
this->set_states(espbt::ClientState::Idle);
|
||||
break;
|
||||
}
|
||||
ESP_LOGV(TAG, "cfg_mtu status %d, mtu %d", param->cfg_mtu.status, param->cfg_mtu.mtu);
|
||||
esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, NULL);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
if (memcmp(param->disconnect.remote_bda, this->remote_bda, 6) != 0) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT", this->address_str().c_str());
|
||||
for (auto &svc : this->services_)
|
||||
delete svc;
|
||||
this->services_.clear();
|
||||
this->set_states(espbt::ClientState::Idle);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_SEARCH_RES_EVT: {
|
||||
BLEService *ble_service = new BLEService();
|
||||
ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid);
|
||||
ble_service->start_handle = param->search_res.start_handle;
|
||||
ble_service->end_handle = param->search_res.end_handle;
|
||||
ble_service->client = this;
|
||||
this->services_.push_back(ble_service);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_SEARCH_CMPL_EVT", this->address_str().c_str());
|
||||
for (auto &svc : this->services_) {
|
||||
ESP_LOGI(TAG, "Service UUID: %s", svc->uuid.to_string().c_str());
|
||||
ESP_LOGI(TAG, " start_handle: 0x%x end_handle: 0x%x", svc->start_handle, svc->end_handle);
|
||||
svc->parse_characteristics();
|
||||
}
|
||||
this->set_states(espbt::ClientState::Connected);
|
||||
this->set_state(espbt::ClientState::Established);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
auto descr = this->get_config_descriptor(param->reg_for_notify.handle);
|
||||
if (descr == nullptr) {
|
||||
ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", param->reg_for_notify.handle);
|
||||
break;
|
||||
}
|
||||
if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 ||
|
||||
descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) {
|
||||
ESP_LOGW(TAG, "Handle 0x%x (uuid %s) is not a client config char uuid", param->reg_for_notify.handle,
|
||||
descr->uuid.to_string().c_str());
|
||||
break;
|
||||
}
|
||||
uint8_t notify_en = 1;
|
||||
auto status = esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en),
|
||||
¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
for (auto *node : this->nodes_)
|
||||
node->gattc_event_handler(event, esp_gattc_if, param);
|
||||
|
||||
// Delete characteristics after clients have used them to save RAM.
|
||||
if (!all_established && this->all_nodes_established()) {
|
||||
for (auto &svc : this->services_)
|
||||
delete svc;
|
||||
this->services_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Parse GATT values into a float for a sensor.
|
||||
// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/
|
||||
float BLEClient::parse_char_value(uint8_t *value, uint16_t length) {
|
||||
// A length of one means a single octet value.
|
||||
if (length == 0)
|
||||
return 0;
|
||||
if (length == 1)
|
||||
return (float) ((uint8_t) value[0]);
|
||||
|
||||
switch (value[0]) {
|
||||
case 0x1: // boolean.
|
||||
case 0x2: // 2bit.
|
||||
case 0x3: // nibble.
|
||||
case 0x4: // uint8.
|
||||
return (float) ((uint8_t) value[1]);
|
||||
case 0x5: // uint12.
|
||||
case 0x6: // uint16.
|
||||
if (length > 2) {
|
||||
return (float) ((uint16_t)(value[1] << 8) + (uint16_t) value[2]);
|
||||
}
|
||||
case 0x7: // uint24.
|
||||
if (length > 3) {
|
||||
return (float) ((uint32_t)(value[1] << 16) + (uint32_t)(value[2] << 8) + (uint32_t)(value[3]));
|
||||
}
|
||||
case 0x8: // uint32.
|
||||
if (length > 4) {
|
||||
return (float) ((uint32_t)(value[1] << 24) + (uint32_t)(value[2] << 16) + (uint32_t)(value[3] << 8) +
|
||||
(uint32_t)(value[4]));
|
||||
}
|
||||
case 0xC: // int8.
|
||||
return (float) ((int8_t) value[1]);
|
||||
case 0xD: // int12.
|
||||
case 0xE: // int16.
|
||||
if (length > 2) {
|
||||
return (float) ((int16_t)(value[1] << 8) + (int16_t) value[2]);
|
||||
}
|
||||
case 0xF: // int24.
|
||||
if (length > 3) {
|
||||
return (float) ((int32_t)(value[1] << 16) + (int32_t)(value[2] << 8) + (int32_t)(value[3]));
|
||||
}
|
||||
case 0x10: // int32.
|
||||
if (length > 4) {
|
||||
return (float) ((int32_t)(value[1] << 24) + (int32_t)(value[2] << 16) + (int32_t)(value[3] << 8) +
|
||||
(int32_t)(value[4]));
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Cannot parse characteristic value of type 0x%x length %d", value[0], length);
|
||||
return NAN;
|
||||
}
|
||||
|
||||
BLEService *BLEClient::get_service(espbt::ESPBTUUID uuid) {
|
||||
for (auto svc : this->services_)
|
||||
if (svc->uuid == uuid)
|
||||
return svc;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BLEService *BLEClient::get_service(uint16_t uuid) { return this->get_service(espbt::ESPBTUUID::from_uint16(uuid)); }
|
||||
|
||||
BLECharacteristic *BLEClient::get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr) {
|
||||
auto svc = this->get_service(service);
|
||||
if (svc == nullptr)
|
||||
return nullptr;
|
||||
return svc->get_characteristic(chr);
|
||||
}
|
||||
|
||||
BLECharacteristic *BLEClient::get_characteristic(uint16_t service, uint16_t chr) {
|
||||
return this->get_characteristic(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr));
|
||||
}
|
||||
|
||||
BLEDescriptor *BLEClient::get_config_descriptor(uint16_t handle) {
|
||||
for (auto &svc : this->services_)
|
||||
for (auto &chr : svc->characteristics)
|
||||
if (chr->handle == handle)
|
||||
for (auto &desc : chr->descriptors)
|
||||
if (desc->uuid == espbt::ESPBTUUID::from_uint16(0x2902))
|
||||
return desc;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BLECharacteristic *BLEService::get_characteristic(espbt::ESPBTUUID uuid) {
|
||||
for (auto &chr : this->characteristics)
|
||||
if (chr->uuid == uuid)
|
||||
return chr;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BLECharacteristic *BLEService::get_characteristic(uint16_t uuid) {
|
||||
return this->get_characteristic(espbt::ESPBTUUID::from_uint16(uuid));
|
||||
}
|
||||
|
||||
BLEDescriptor *BLEClient::get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr) {
|
||||
auto svc = this->get_service(service);
|
||||
if (svc == nullptr)
|
||||
return nullptr;
|
||||
auto ch = svc->get_characteristic(chr);
|
||||
if (ch == nullptr)
|
||||
return nullptr;
|
||||
return ch->get_descriptor(descr);
|
||||
}
|
||||
|
||||
BLEDescriptor *BLEClient::get_descriptor(uint16_t service, uint16_t chr, uint16_t descr) {
|
||||
return this->get_descriptor(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr),
|
||||
espbt::ESPBTUUID::from_uint16(descr));
|
||||
}
|
||||
|
||||
BLEService::~BLEService() {
|
||||
for (auto &chr : this->characteristics)
|
||||
delete chr;
|
||||
}
|
||||
|
||||
void BLEService::parse_characteristics() {
|
||||
uint16_t offset = 0;
|
||||
esp_gattc_char_elem_t result;
|
||||
|
||||
while (true) {
|
||||
uint16_t count = 1;
|
||||
esp_gatt_status_t status = esp_ble_gattc_get_all_char(
|
||||
this->client->gattc_if, this->client->conn_id, this->start_handle, this->end_handle, &result, &count, offset);
|
||||
if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_get_all_char error, status=%d", status);
|
||||
break;
|
||||
}
|
||||
if (count == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
BLECharacteristic *characteristic = new BLECharacteristic();
|
||||
characteristic->uuid = espbt::ESPBTUUID::from_uuid(result.uuid);
|
||||
characteristic->properties = result.properties;
|
||||
characteristic->handle = result.char_handle;
|
||||
characteristic->service = this;
|
||||
this->characteristics.push_back(characteristic);
|
||||
ESP_LOGI(TAG, " characteristic %s, handle 0x%x, properties 0x%x", characteristic->uuid.to_string().c_str(),
|
||||
characteristic->handle, characteristic->properties);
|
||||
characteristic->parse_descriptors();
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
|
||||
BLECharacteristic::~BLECharacteristic() {
|
||||
for (auto &desc : this->descriptors)
|
||||
delete desc;
|
||||
}
|
||||
|
||||
void BLECharacteristic::parse_descriptors() {
|
||||
uint16_t offset = 0;
|
||||
esp_gattc_descr_elem_t result;
|
||||
|
||||
while (true) {
|
||||
uint16_t count = 1;
|
||||
esp_gatt_status_t status = esp_ble_gattc_get_all_descr(
|
||||
this->service->client->gattc_if, this->service->client->conn_id, this->handle, &result, &count, offset);
|
||||
if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_get_all_descr error, status=%d", status);
|
||||
break;
|
||||
}
|
||||
if (count == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
BLEDescriptor *desc = new BLEDescriptor();
|
||||
desc->uuid = espbt::ESPBTUUID::from_uuid(result.uuid);
|
||||
desc->handle = result.handle;
|
||||
desc->characteristic = this;
|
||||
this->descriptors.push_back(desc);
|
||||
ESP_LOGV(TAG, " descriptor %s, handle 0x%x", desc->uuid.to_string().c_str(), desc->handle);
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
|
||||
BLEDescriptor *BLECharacteristic::get_descriptor(espbt::ESPBTUUID uuid) {
|
||||
for (auto &desc : this->descriptors)
|
||||
if (desc->uuid == uuid)
|
||||
return desc;
|
||||
return nullptr;
|
||||
}
|
||||
BLEDescriptor *BLECharacteristic::get_descriptor(uint16_t uuid) {
|
||||
return this->get_descriptor(espbt::ESPBTUUID::from_uint16(uuid));
|
||||
}
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
140
esphome/components/ble_client/ble_client.h
Normal file
140
esphome/components/ble_client/ble_client.h
Normal file
@@ -0,0 +1,140 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gattc_api.h>
|
||||
#include <esp_bt_defs.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
class BLEClient;
|
||||
class BLEService;
|
||||
class BLECharacteristic;
|
||||
|
||||
class BLEClientNode {
|
||||
public:
|
||||
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;
|
||||
virtual void loop() = 0;
|
||||
void set_address(uint64_t address) { address_ = address; }
|
||||
espbt::ESPBTClient *client;
|
||||
// This should be transitioned to Established once the node no longer needs
|
||||
// the services/descriptors/characteristics of the parent client. This will
|
||||
// allow some memory to be freed.
|
||||
espbt::ClientState node_state;
|
||||
|
||||
BLEClient *parent() { return this->parent_; }
|
||||
void set_ble_client_parent(BLEClient *parent) { this->parent_ = parent; }
|
||||
|
||||
protected:
|
||||
BLEClient *parent_;
|
||||
uint64_t address_;
|
||||
};
|
||||
|
||||
class BLEDescriptor {
|
||||
public:
|
||||
espbt::ESPBTUUID uuid;
|
||||
uint16_t handle;
|
||||
|
||||
BLECharacteristic *characteristic;
|
||||
};
|
||||
|
||||
class BLECharacteristic {
|
||||
public:
|
||||
~BLECharacteristic();
|
||||
espbt::ESPBTUUID uuid;
|
||||
uint16_t handle;
|
||||
esp_gatt_char_prop_t properties;
|
||||
std::vector<BLEDescriptor *> descriptors;
|
||||
void parse_descriptors();
|
||||
BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid);
|
||||
BLEDescriptor *get_descriptor(uint16_t uuid);
|
||||
|
||||
BLEService *service;
|
||||
};
|
||||
|
||||
class BLEService {
|
||||
public:
|
||||
~BLEService();
|
||||
espbt::ESPBTUUID uuid;
|
||||
uint16_t start_handle;
|
||||
uint16_t end_handle;
|
||||
std::vector<BLECharacteristic *> characteristics;
|
||||
BLEClient *client;
|
||||
void parse_characteristics();
|
||||
BLECharacteristic *get_characteristic(espbt::ESPBTUUID uuid);
|
||||
BLECharacteristic *get_characteristic(uint16_t uuid);
|
||||
};
|
||||
|
||||
class BLEClient : public espbt::ESPBTClient, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
|
||||
bool parse_device(const espbt::ESPBTDevice &device) override;
|
||||
void on_scan_end() override {}
|
||||
void connect();
|
||||
|
||||
void set_address(uint64_t address) { this->address = address; }
|
||||
|
||||
void set_enabled(bool enabled);
|
||||
|
||||
void register_ble_node(BLEClientNode *node) {
|
||||
node->client = this;
|
||||
node->set_ble_client_parent(this);
|
||||
this->nodes_.push_back(node);
|
||||
}
|
||||
|
||||
BLEService *get_service(espbt::ESPBTUUID uuid);
|
||||
BLEService *get_service(uint16_t uuid);
|
||||
BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr);
|
||||
BLECharacteristic *get_characteristic(uint16_t service, uint16_t chr);
|
||||
BLEDescriptor *get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr);
|
||||
BLEDescriptor *get_descriptor(uint16_t service, uint16_t chr, uint16_t descr);
|
||||
// Get the configuration descriptor for the given characteristic handle.
|
||||
BLEDescriptor *get_config_descriptor(uint16_t handle);
|
||||
|
||||
float parse_char_value(uint8_t *value, uint16_t length);
|
||||
|
||||
int gattc_if;
|
||||
esp_bd_addr_t remote_bda;
|
||||
uint16_t conn_id;
|
||||
uint64_t address;
|
||||
bool enabled;
|
||||
std::string address_str() const;
|
||||
|
||||
protected:
|
||||
void set_states(espbt::ClientState st) {
|
||||
this->set_state(st);
|
||||
for (auto &node : nodes_)
|
||||
node->node_state = st;
|
||||
}
|
||||
bool all_nodes_established() {
|
||||
if (this->state() != espbt::ClientState::Established)
|
||||
return false;
|
||||
for (auto &node : nodes_)
|
||||
if (node->node_state != espbt::ClientState::Established)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<BLEClientNode *> nodes_;
|
||||
std::vector<BLEService *> services_;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
115
esphome/components/ble_client/sensor/__init__.py
Normal file
115
esphome/components/ble_client/sensor/__init__.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, ble_client, esp32_ble_tracker
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_EMPTY,
|
||||
CONF_ID,
|
||||
UNIT_EMPTY,
|
||||
ICON_EMPTY,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_SERVICE_UUID,
|
||||
)
|
||||
from esphome import automation
|
||||
from .. import ble_client_ns
|
||||
|
||||
DEPENDENCIES = ["ble_client"]
|
||||
|
||||
CONF_CHARACTERISTIC_UUID = "characteristic_uuid"
|
||||
CONF_DESCRIPTOR_UUID = "descriptor_uuid"
|
||||
|
||||
CONF_NOTIFY = "notify"
|
||||
CONF_ON_NOTIFY = "on_notify"
|
||||
|
||||
BLESensor = ble_client_ns.class_(
|
||||
"BLESensor", sensor.Sensor, cg.PollingComponent, ble_client.BLEClientNode
|
||||
)
|
||||
BLESensorNotifyTrigger = ble_client_ns.class_(
|
||||
"BLESensorNotifyTrigger", automation.Trigger.template(cg.float_)
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BLESensor),
|
||||
cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
|
||||
cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid,
|
||||
cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid,
|
||||
cv.Optional(CONF_NOTIFY, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ON_NOTIFY): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
BLESensorNotifyTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(ble_client.BLE_CLIENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
|
||||
cg.add(
|
||||
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_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_hex_array(config[CONF_CHARACTERISTIC_UUID])
|
||||
cg.add(var.set_char_uuid128(uuid128))
|
||||
|
||||
if CONF_DESCRIPTOR_UUID in config:
|
||||
if len(config[CONF_DESCRIPTOR_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
|
||||
cg.add(
|
||||
var.set_descr_uuid16(
|
||||
esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID])
|
||||
)
|
||||
)
|
||||
elif len(config[CONF_DESCRIPTOR_UUID]) == len(
|
||||
esp32_ble_tracker.bt_uuid32_format
|
||||
):
|
||||
cg.add(
|
||||
var.set_descr_uuid32(
|
||||
esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID])
|
||||
)
|
||||
)
|
||||
elif len(config[CONF_DESCRIPTOR_UUID]) == len(
|
||||
esp32_ble_tracker.bt_uuid128_format
|
||||
):
|
||||
uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_DESCRIPTOR_UUID])
|
||||
cg.add(var.set_descr_uuid128(uuid128))
|
||||
|
||||
yield cg.register_component(var, config)
|
||||
yield ble_client.register_ble_node(var, config)
|
||||
cg.add(var.set_enable_notify(config[CONF_NOTIFY]))
|
||||
yield sensor.register_sensor(var, config)
|
||||
for conf in config.get(CONF_ON_NOTIFY, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
yield ble_client.register_ble_node(trigger, config)
|
||||
yield automation.build_automation(trigger, [(float, "x")], conf)
|
||||
37
esphome/components/ble_client/sensor/automation.h
Normal file
37
esphome/components/ble_client/sensor/automation.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/ble_client/sensor/ble_sensor.h"
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
|
||||
class BLESensorNotifyTrigger : public Trigger<float>, public BLESensor {
|
||||
public:
|
||||
explicit BLESensorNotifyTrigger(BLESensor *sensor) { sensor_ = sensor; }
|
||||
void 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_SEARCH_CMPL_EVT: {
|
||||
this->sensor_->node_state = espbt::ClientState::Established;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.conn_id != this->sensor_->parent()->conn_id || param->notify.handle != this->sensor_->handle)
|
||||
break;
|
||||
this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len));
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
BLESensor *sensor_;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
129
esphome/components/ble_client/sensor/ble_sensor.cpp
Normal file
129
esphome/components/ble_client/sensor/ble_sensor.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "ble_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 ARDUINO_ARCH_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
|
||||
static const char *TAG = "ble_sensor";
|
||||
|
||||
uint32_t BLESensor::hash_base() { return 343459825UL; }
|
||||
|
||||
void BLESensor::loop() {}
|
||||
|
||||
void BLESensor::dump_config() {
|
||||
LOG_SENSOR("", "BLE Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Characteristic UUID: %s", this->char_uuid_.to_string().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Descriptor UUID : %s", this->descr_uuid_.to_string().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Notifications : %s", YESNO(this->notify_));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
void BLESensor::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->handle = 0;
|
||||
auto chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
|
||||
if (chr == nullptr) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(),
|
||||
this->char_uuid_.to_string().c_str());
|
||||
break;
|
||||
}
|
||||
this->handle = chr->handle;
|
||||
if (this->descr_uuid_.get_uuid().len > 0) {
|
||||
auto descr = chr->get_descriptor(this->descr_uuid_);
|
||||
if (descr == nullptr) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
ESP_LOGW(TAG, "No sensor descriptor found at service %s char %s descr %s",
|
||||
this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(),
|
||||
this->descr_uuid_.to_string().c_str());
|
||||
break;
|
||||
}
|
||||
this->handle = descr->handle;
|
||||
}
|
||||
if (this->notify_) {
|
||||
auto status =
|
||||
esp_ble_gattc_register_for_notify(this->parent()->gattc_if, this->parent()->remote_bda, chr->handle);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status);
|
||||
}
|
||||
} else {
|
||||
this->node_state = espbt::ClientState::Established;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_READ_CHAR_EVT: {
|
||||
if (param->read.conn_id != this->parent()->conn_id)
|
||||
break;
|
||||
if (param->read.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
|
||||
break;
|
||||
}
|
||||
if (param->read.handle == this->handle) {
|
||||
this->status_clear_warning();
|
||||
this->publish_state((float) param->read.value[0]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.conn_id != this->parent()->conn_id || param->notify.handle != this->handle)
|
||||
break;
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
|
||||
param->notify.handle, param->notify.value[0]);
|
||||
this->publish_state((float) param->notify.value[0]);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
this->node_state = espbt::ClientState::Established;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void BLESensor::update() {
|
||||
if (this->node_state != espbt::ClientState::Established) {
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
if (this->handle == 0) {
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, no service or characteristic found", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
auto status =
|
||||
esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
ESP_LOGW(TAG, "[%s] Error sending read request for sensor, status=%d", this->get_name().c_str(), status);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
#endif
|
||||
46
esphome/components/ble_client/sensor/ble_sensor.h
Normal file
46
esphome/components/ble_client/sensor/ble_sensor.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#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 ARDUINO_ARCH_ESP32
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode {
|
||||
public:
|
||||
void loop() override;
|
||||
void update() 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;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
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_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 set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
||||
void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
void set_enable_notify(bool notify) { this->notify_ = notify; }
|
||||
uint16_t handle;
|
||||
|
||||
protected:
|
||||
uint32_t hash_base() override;
|
||||
bool notify_;
|
||||
espbt::ESPBTUUID service_uuid_;
|
||||
espbt::ESPBTUUID char_uuid_;
|
||||
espbt::ESPBTUUID descr_uuid_;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
#endif
|
||||
30
esphome/components/ble_client/switch/__init__.py
Normal file
30
esphome/components/ble_client/switch/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import switch, ble_client
|
||||
from esphome.const import CONF_ICON, CONF_ID, CONF_INVERTED, ICON_BLUETOOTH
|
||||
from .. import ble_client_ns
|
||||
|
||||
BLEClientSwitch = ble_client_ns.class_(
|
||||
"BLEClientSwitch", switch.Switch, cg.Component, ble_client.BLEClientNode
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
switch.SWITCH_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BLEClientSwitch),
|
||||
cv.Optional(CONF_INVERTED): cv.invalid(
|
||||
"BLE client switches do not support inverted mode!"
|
||||
),
|
||||
cv.Optional(CONF_ICON, default=ICON_BLUETOOTH): switch.icon,
|
||||
}
|
||||
)
|
||||
.extend(ble_client.BLE_CLIENT_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
yield cg.register_component(var, config)
|
||||
yield switch.register_switch(var, config)
|
||||
yield ble_client.register_ble_node(var, config)
|
||||
39
esphome/components/ble_client/switch/ble_switch.cpp
Normal file
39
esphome/components/ble_client/switch/ble_switch.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include "ble_switch.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
|
||||
static const char *TAG = "ble_switch";
|
||||
|
||||
void BLEClientSwitch::write_state(bool state) {
|
||||
this->parent_->set_enabled(state);
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
void BLEClientSwitch::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_REG_EVT:
|
||||
this->publish_state(this->parent_->enabled);
|
||||
break;
|
||||
case ESP_GATTC_OPEN_EVT:
|
||||
this->node_state = espbt::ClientState::Established;
|
||||
break;
|
||||
case ESP_GATTC_DISCONNECT_EVT:
|
||||
this->node_state = espbt::ClientState::Idle;
|
||||
this->publish_state(this->parent_->enabled);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void BLEClientSwitch::dump_config() { LOG_SWITCH("", "BLE Client Switch", this); }
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
#endif
|
||||
30
esphome/components/ble_client/switch/ble_switch.h
Normal file
30
esphome/components/ble_client/switch/ble_switch.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#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/switch/switch.h"
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
class BLEClientSwitch : public switch_::Switch, public Component, public BLEClientNode {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void loop() 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;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
#endif
|
||||
64
esphome/components/bme680_bsec/__init__.py
Normal file
64
esphome/components/bme680_bsec/__init__.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
CODEOWNERS = ["@trvrnrth"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensor", "text_sensor"]
|
||||
|
||||
CONF_BME680_BSEC_ID = "bme680_bsec_id"
|
||||
CONF_TEMPERATURE_OFFSET = "temperature_offset"
|
||||
CONF_IAQ_MODE = "iaq_mode"
|
||||
CONF_SAMPLE_RATE = "sample_rate"
|
||||
CONF_STATE_SAVE_INTERVAL = "state_save_interval"
|
||||
|
||||
bme680_bsec_ns = cg.esphome_ns.namespace("bme680_bsec")
|
||||
|
||||
IAQMode = bme680_bsec_ns.enum("IAQMode")
|
||||
IAQ_MODE_OPTIONS = {
|
||||
"STATIC": IAQMode.IAQ_MODE_STATIC,
|
||||
"MOBILE": IAQMode.IAQ_MODE_MOBILE,
|
||||
}
|
||||
|
||||
SampleRate = bme680_bsec_ns.enum("SampleRate")
|
||||
SAMPLE_RATE_OPTIONS = {
|
||||
"LP": SampleRate.SAMPLE_RATE_LP,
|
||||
"ULP": SampleRate.SAMPLE_RATE_ULP,
|
||||
}
|
||||
|
||||
BME680BSECComponent = bme680_bsec_ns.class_(
|
||||
"BME680BSECComponent", cg.Component, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BME680BSECComponent),
|
||||
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
|
||||
cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum(
|
||||
IAQ_MODE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum(
|
||||
SAMPLE_RATE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_STATE_SAVE_INTERVAL, default="6hours"
|
||||
): cv.positive_time_period_minutes,
|
||||
}
|
||||
).extend(i2c.i2c_device_schema(0x76))
|
||||
|
||||
|
||||
def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
yield cg.register_component(var, config)
|
||||
yield i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET]))
|
||||
cg.add(var.set_iaq_mode(config[CONF_IAQ_MODE]))
|
||||
cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))
|
||||
cg.add(
|
||||
var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds)
|
||||
)
|
||||
|
||||
cg.add_define("USING_BSEC")
|
||||
cg.add_library("BSEC Software Library", "1.6.1480")
|
||||
396
esphome/components/bme680_bsec/bme680_bsec.cpp
Normal file
396
esphome/components/bme680_bsec/bme680_bsec.cpp
Normal file
@@ -0,0 +1,396 @@
|
||||
|
||||
|
||||
#include "bme680_bsec.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <string>
|
||||
|
||||
namespace esphome {
|
||||
namespace bme680_bsec {
|
||||
#ifdef USING_BSEC
|
||||
static const char *TAG = "bme680_bsec.sensor";
|
||||
|
||||
static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
|
||||
|
||||
BME680BSECComponent *BME680BSECComponent::instance;
|
||||
|
||||
void BME680BSECComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC...");
|
||||
BME680BSECComponent::instance = this;
|
||||
|
||||
this->bsec_status_ = bsec_init();
|
||||
if (this->bsec_status_ != BSEC_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->bme680_.dev_id = this->address_;
|
||||
this->bme680_.intf = BME680_I2C_INTF;
|
||||
this->bme680_.read = BME680BSECComponent::read_bytes_wrapper;
|
||||
this->bme680_.write = BME680BSECComponent::write_bytes_wrapper;
|
||||
this->bme680_.delay_ms = BME680BSECComponent::delay_ms;
|
||||
this->bme680_.amb_temp = 25;
|
||||
this->bme680_.power_mode = BME680_FORCED_MODE;
|
||||
|
||||
this->bme680_status_ = bme680_init(&this->bme680_);
|
||||
if (this->bme680_status_ != BME680_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->sample_rate_ == SAMPLE_RATE_ULP) {
|
||||
const uint8_t bsec_config[] = {
|
||||
#include "config/generic_33v_300s_28d/bsec_iaq.txt"
|
||||
};
|
||||
this->set_config_(bsec_config);
|
||||
this->update_subscription_(BSEC_SAMPLE_RATE_ULP);
|
||||
} else {
|
||||
const uint8_t bsec_config[] = {
|
||||
#include "config/generic_33v_3s_28d/bsec_iaq.txt"
|
||||
};
|
||||
this->set_config_(bsec_config);
|
||||
this->update_subscription_(BSEC_SAMPLE_RATE_LP);
|
||||
}
|
||||
if (this->bsec_status_ != BSEC_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->load_state_();
|
||||
}
|
||||
|
||||
void BME680BSECComponent::set_config_(const uint8_t *config) {
|
||||
uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE];
|
||||
this->bsec_status_ = bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, work_buffer, sizeof(work_buffer));
|
||||
}
|
||||
|
||||
void BME680BSECComponent::update_subscription_(float sample_rate) {
|
||||
bsec_sensor_configuration_t virtual_sensors[BSEC_NUMBER_OUTPUTS];
|
||||
int num_virtual_sensors = 0;
|
||||
|
||||
if (this->iaq_sensor_) {
|
||||
virtual_sensors[num_virtual_sensors].sensor_id =
|
||||
this->iaq_mode_ == IAQ_MODE_STATIC ? BSEC_OUTPUT_STATIC_IAQ : BSEC_OUTPUT_IAQ;
|
||||
virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
|
||||
num_virtual_sensors++;
|
||||
}
|
||||
|
||||
if (this->co2_equivalent_sensor_) {
|
||||
virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_CO2_EQUIVALENT;
|
||||
virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
|
||||
num_virtual_sensors++;
|
||||
}
|
||||
|
||||
if (this->breath_voc_equivalent_sensor_) {
|
||||
virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_BREATH_VOC_EQUIVALENT;
|
||||
virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
|
||||
num_virtual_sensors++;
|
||||
}
|
||||
|
||||
if (this->pressure_sensor_) {
|
||||
virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_PRESSURE;
|
||||
virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
|
||||
num_virtual_sensors++;
|
||||
}
|
||||
|
||||
if (this->gas_resistance_sensor_) {
|
||||
virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_GAS;
|
||||
virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
|
||||
num_virtual_sensors++;
|
||||
}
|
||||
|
||||
if (this->temperature_sensor_) {
|
||||
virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE;
|
||||
virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
|
||||
num_virtual_sensors++;
|
||||
}
|
||||
|
||||
if (this->humidity_sensor_) {
|
||||
virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY;
|
||||
virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
|
||||
num_virtual_sensors++;
|
||||
}
|
||||
|
||||
bsec_sensor_configuration_t sensor_settings[BSEC_MAX_PHYSICAL_SENSOR];
|
||||
uint8_t num_sensor_settings = BSEC_MAX_PHYSICAL_SENSOR;
|
||||
this->bsec_status_ =
|
||||
bsec_update_subscription(virtual_sensors, num_virtual_sensors, sensor_settings, &num_sensor_settings);
|
||||
}
|
||||
|
||||
void BME680BSECComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BME680 via BSEC:");
|
||||
|
||||
bsec_version_t version;
|
||||
bsec_get_version(&version);
|
||||
ESP_LOGCONFIG(TAG, " BSEC Version: %d.%d.%d.%d", version.major, version.minor, version.major_bugfix,
|
||||
version.minor_bugfix);
|
||||
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication failed (BSEC Status: %d, BME680 Status: %d)", this->bsec_status_,
|
||||
this->bme680_status_);
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Temperature Offset: %.2f", this->temperature_offset_);
|
||||
ESP_LOGCONFIG(TAG, " IAQ Mode: %s", this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile");
|
||||
ESP_LOGCONFIG(TAG, " Sample Rate: %s", this->sample_rate_ == SAMPLE_RATE_ULP ? "ULP" : "LP");
|
||||
ESP_LOGCONFIG(TAG, " State Save Interval: %ims", this->state_save_interval_ms_);
|
||||
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
LOG_SENSOR(" ", "Gas Resistance", this->gas_resistance_sensor_);
|
||||
LOG_SENSOR(" ", "IAQ", this->iaq_sensor_);
|
||||
LOG_SENSOR(" ", "Numeric IAQ Accuracy", this->iaq_accuracy_sensor_);
|
||||
LOG_TEXT_SENSOR(" ", "IAQ Accuracy", this->iaq_accuracy_text_sensor_);
|
||||
LOG_SENSOR(" ", "CO2 Equivalent", this->co2_equivalent_sensor_);
|
||||
LOG_SENSOR(" ", "Breath VOC Equivalent", this->breath_voc_equivalent_sensor_);
|
||||
}
|
||||
|
||||
float BME680BSECComponent::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void BME680BSECComponent::loop() {
|
||||
this->run_();
|
||||
|
||||
if (this->bsec_status_ < BSEC_OK || this->bme680_status_ < BME680_OK) {
|
||||
this->status_set_error();
|
||||
} else {
|
||||
this->status_clear_error();
|
||||
}
|
||||
if (this->bsec_status_ > BSEC_OK || this->bme680_status_ > BME680_OK) {
|
||||
this->status_set_warning();
|
||||
} else {
|
||||
this->status_clear_warning();
|
||||
}
|
||||
}
|
||||
|
||||
void BME680BSECComponent::run_() {
|
||||
int64_t curr_time_ns = this->get_time_ns_();
|
||||
if (curr_time_ns < this->next_call_ns_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Performing sensor run");
|
||||
|
||||
bsec_bme_settings_t bme680_settings;
|
||||
this->bsec_status_ = bsec_sensor_control(curr_time_ns, &bme680_settings);
|
||||
if (this->bsec_status_ < BSEC_OK) {
|
||||
ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC Error Code %d)", this->bsec_status_);
|
||||
return;
|
||||
}
|
||||
this->next_call_ns_ = bme680_settings.next_call;
|
||||
|
||||
this->bme680_.gas_sett.run_gas = bme680_settings.run_gas;
|
||||
this->bme680_.tph_sett.os_hum = bme680_settings.humidity_oversampling;
|
||||
this->bme680_.tph_sett.os_temp = bme680_settings.temperature_oversampling;
|
||||
this->bme680_.tph_sett.os_pres = bme680_settings.pressure_oversampling;
|
||||
this->bme680_.gas_sett.heatr_temp = bme680_settings.heater_temperature;
|
||||
this->bme680_.gas_sett.heatr_dur = bme680_settings.heating_duration;
|
||||
uint16_t desired_settings =
|
||||
BME680_OST_SEL | BME680_OSP_SEL | BME680_OSH_SEL | BME680_FILTER_SEL | BME680_GAS_SENSOR_SEL;
|
||||
this->bme680_status_ = bme680_set_sensor_settings(desired_settings, &this->bme680_);
|
||||
if (this->bme680_status_ != BME680_OK) {
|
||||
ESP_LOGW(TAG, "Failed to set sensor settings (BME680 Error Code %d)", this->bme680_status_);
|
||||
return;
|
||||
}
|
||||
|
||||
this->bme680_status_ = bme680_set_sensor_mode(&this->bme680_);
|
||||
if (this->bme680_status_ != BME680_OK) {
|
||||
ESP_LOGW(TAG, "Failed to set sensor mode (BME680 Error Code %d)", this->bme680_status_);
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t meas_dur = 0;
|
||||
bme680_get_profile_dur(&meas_dur, &this->bme680_);
|
||||
ESP_LOGV(TAG, "Queueing read in %ums", meas_dur);
|
||||
this->set_timeout("read", meas_dur, [this, bme680_settings]() { this->read_(bme680_settings); });
|
||||
}
|
||||
|
||||
void BME680BSECComponent::read_(bsec_bme_settings_t bme680_settings) {
|
||||
ESP_LOGV(TAG, "Reading data");
|
||||
struct bme680_field_data data;
|
||||
this->bme680_status_ = bme680_get_sensor_data(&data, &this->bme680_);
|
||||
|
||||
if (this->bme680_status_ != BME680_OK) {
|
||||
ESP_LOGW(TAG, "Failed to get sensor data (BME680 Error Code %d)", this->bme680_status_);
|
||||
return;
|
||||
}
|
||||
if (!(data.status & BME680_NEW_DATA_MSK)) {
|
||||
ESP_LOGD(TAG, "BME680 did not report new data");
|
||||
return;
|
||||
}
|
||||
|
||||
bsec_input_t inputs[BSEC_MAX_PHYSICAL_SENSOR]; // Temperature, Pressure, Humidity & Gas Resistance
|
||||
uint8_t num_inputs = 0;
|
||||
int64_t curr_time_ns = this->get_time_ns_();
|
||||
|
||||
if (bme680_settings.process_data & BSEC_PROCESS_TEMPERATURE) {
|
||||
inputs[num_inputs].sensor_id = BSEC_INPUT_TEMPERATURE;
|
||||
inputs[num_inputs].signal = data.temperature / 100.0f;
|
||||
inputs[num_inputs].time_stamp = curr_time_ns;
|
||||
num_inputs++;
|
||||
|
||||
// Temperature offset from the real temperature due to external heat sources
|
||||
inputs[num_inputs].sensor_id = BSEC_INPUT_HEATSOURCE;
|
||||
inputs[num_inputs].signal = this->temperature_offset_;
|
||||
inputs[num_inputs].time_stamp = curr_time_ns;
|
||||
num_inputs++;
|
||||
}
|
||||
if (bme680_settings.process_data & BSEC_PROCESS_HUMIDITY) {
|
||||
inputs[num_inputs].sensor_id = BSEC_INPUT_HUMIDITY;
|
||||
inputs[num_inputs].signal = data.humidity / 1000.0f;
|
||||
inputs[num_inputs].time_stamp = curr_time_ns;
|
||||
num_inputs++;
|
||||
}
|
||||
if (bme680_settings.process_data & BSEC_PROCESS_PRESSURE) {
|
||||
inputs[num_inputs].sensor_id = BSEC_INPUT_PRESSURE;
|
||||
inputs[num_inputs].signal = data.pressure;
|
||||
inputs[num_inputs].time_stamp = curr_time_ns;
|
||||
num_inputs++;
|
||||
}
|
||||
if (bme680_settings.process_data & BSEC_PROCESS_GAS) {
|
||||
inputs[num_inputs].sensor_id = BSEC_INPUT_GASRESISTOR;
|
||||
inputs[num_inputs].signal = data.gas_resistance;
|
||||
inputs[num_inputs].time_stamp = curr_time_ns;
|
||||
num_inputs++;
|
||||
}
|
||||
if (num_inputs < 1) {
|
||||
ESP_LOGD(TAG, "No signal inputs available for BSEC");
|
||||
return;
|
||||
}
|
||||
|
||||
bsec_output_t outputs[BSEC_NUMBER_OUTPUTS];
|
||||
uint8_t num_outputs = BSEC_NUMBER_OUTPUTS;
|
||||
this->bsec_status_ = bsec_do_steps(inputs, num_inputs, outputs, &num_outputs);
|
||||
if (this->bsec_status_ != BSEC_OK) {
|
||||
ESP_LOGW(TAG, "BSEC failed to process signals (BSEC Error Code %d)", this->bsec_status_);
|
||||
return;
|
||||
}
|
||||
if (num_outputs < 1) {
|
||||
ESP_LOGD(TAG, "No signal outputs provided by BSEC");
|
||||
return;
|
||||
}
|
||||
|
||||
this->publish_(outputs, num_outputs);
|
||||
}
|
||||
|
||||
void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) {
|
||||
ESP_LOGV(TAG, "Publishing sensor states");
|
||||
for (uint8_t i = 0; i < num_outputs; i++) {
|
||||
switch (outputs[i].sensor_id) {
|
||||
case BSEC_OUTPUT_IAQ:
|
||||
case BSEC_OUTPUT_STATIC_IAQ:
|
||||
uint8_t accuracy;
|
||||
accuracy = outputs[i].accuracy;
|
||||
this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal);
|
||||
this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
|
||||
this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true);
|
||||
|
||||
// Queue up an opportunity to save state
|
||||
this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); });
|
||||
break;
|
||||
case BSEC_OUTPUT_CO2_EQUIVALENT:
|
||||
this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal);
|
||||
break;
|
||||
case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
|
||||
this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal);
|
||||
break;
|
||||
case BSEC_OUTPUT_RAW_PRESSURE:
|
||||
this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f);
|
||||
break;
|
||||
case BSEC_OUTPUT_RAW_GAS:
|
||||
this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal);
|
||||
break;
|
||||
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE:
|
||||
this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal);
|
||||
break;
|
||||
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY:
|
||||
this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int64_t BME680BSECComponent::get_time_ns_() {
|
||||
int64_t time_ms = millis();
|
||||
if (this->last_time_ms_ > time_ms) {
|
||||
this->millis_overflow_counter_++;
|
||||
}
|
||||
this->last_time_ms_ = time_ms;
|
||||
|
||||
return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000);
|
||||
}
|
||||
|
||||
void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) {
|
||||
if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) {
|
||||
return;
|
||||
}
|
||||
sensor->publish_state(value);
|
||||
}
|
||||
|
||||
void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value) {
|
||||
if (!sensor || (sensor->has_state() && sensor->state == value)) {
|
||||
return;
|
||||
}
|
||||
sensor->publish_state(value);
|
||||
}
|
||||
|
||||
int8_t BME680BSECComponent::read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) {
|
||||
return BME680BSECComponent::instance->read_bytes(a_register, data, len) ? 0 : -1;
|
||||
}
|
||||
|
||||
int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) {
|
||||
return BME680BSECComponent::instance->write_bytes(a_register, data, len) ? 0 : -1;
|
||||
}
|
||||
|
||||
void BME680BSECComponent::delay_ms(uint32_t period) {
|
||||
ESP_LOGV(TAG, "Delaying for %ums", period);
|
||||
delay(period);
|
||||
}
|
||||
|
||||
void BME680BSECComponent::load_state_() {
|
||||
uint32_t hash = fnv1_hash("bme680_bsec_state_" + to_string(this->address_));
|
||||
this->bsec_state_ = global_preferences.make_preference<uint8_t[BSEC_MAX_STATE_BLOB_SIZE]>(hash, true);
|
||||
|
||||
uint8_t state[BSEC_MAX_STATE_BLOB_SIZE];
|
||||
if (this->bsec_state_.load(&state)) {
|
||||
ESP_LOGV(TAG, "Loading state");
|
||||
uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE];
|
||||
this->bsec_status_ = bsec_set_state(state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, sizeof(work_buffer));
|
||||
if (this->bsec_status_ != BSEC_OK) {
|
||||
ESP_LOGW(TAG, "Failed to load state (BSEC Error Code %d)", this->bsec_status_);
|
||||
}
|
||||
ESP_LOGI(TAG, "Loaded state");
|
||||
}
|
||||
}
|
||||
|
||||
void BME680BSECComponent::save_state_(uint8_t accuracy) {
|
||||
if (accuracy < 3 || (millis() - this->last_state_save_ms_ < this->state_save_interval_ms_)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Saving state");
|
||||
|
||||
uint8_t state[BSEC_MAX_STATE_BLOB_SIZE];
|
||||
uint8_t work_buffer[BSEC_MAX_STATE_BLOB_SIZE];
|
||||
uint32_t num_serialized_state = BSEC_MAX_STATE_BLOB_SIZE;
|
||||
|
||||
this->bsec_status_ =
|
||||
bsec_get_state(0, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, BSEC_MAX_STATE_BLOB_SIZE, &num_serialized_state);
|
||||
if (this->bsec_status_ != BSEC_OK) {
|
||||
ESP_LOGW(TAG, "Failed fetch state for save (BSEC Error Code %d)", this->bsec_status_);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->bsec_state_.save(&state)) {
|
||||
ESP_LOGW(TAG, "Failed to save state");
|
||||
return;
|
||||
}
|
||||
this->last_state_save_ms_ = millis();
|
||||
|
||||
ESP_LOGI(TAG, "Saved state");
|
||||
}
|
||||
#endif
|
||||
} // namespace bme680_bsec
|
||||
} // namespace esphome
|
||||
108
esphome/components/bme680_bsec/bme680_bsec.h
Normal file
108
esphome/components/bme680_bsec/bme680_bsec.h
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include <map>
|
||||
|
||||
#ifdef USING_BSEC
|
||||
#include <bsec.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace bme680_bsec {
|
||||
#ifdef USING_BSEC
|
||||
|
||||
enum IAQMode {
|
||||
IAQ_MODE_STATIC = 0,
|
||||
IAQ_MODE_MOBILE = 1,
|
||||
};
|
||||
|
||||
enum SampleRate {
|
||||
SAMPLE_RATE_LP = 0,
|
||||
SAMPLE_RATE_ULP = 1,
|
||||
};
|
||||
|
||||
class BME680BSECComponent : public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
void set_temperature_offset(float offset) { this->temperature_offset_ = offset; }
|
||||
void set_iaq_mode(IAQMode iaq_mode) { this->iaq_mode_ = iaq_mode; }
|
||||
void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; }
|
||||
void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; }
|
||||
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
|
||||
void set_gas_resistance_sensor(sensor::Sensor *gas_resistance_sensor) {
|
||||
gas_resistance_sensor_ = gas_resistance_sensor;
|
||||
}
|
||||
void set_iaq_sensor(sensor::Sensor *iaq_sensor) { iaq_sensor_ = iaq_sensor; }
|
||||
void set_iaq_accuracy_text_sensor(text_sensor::TextSensor *iaq_accuracy_text_sensor) {
|
||||
iaq_accuracy_text_sensor_ = iaq_accuracy_text_sensor;
|
||||
}
|
||||
void set_iaq_accuracy_sensor(sensor::Sensor *iaq_accuracy_sensor) { iaq_accuracy_sensor_ = iaq_accuracy_sensor; }
|
||||
void set_co2_equivalent_sensor(sensor::Sensor *co2_equivalent_sensor) {
|
||||
co2_equivalent_sensor_ = co2_equivalent_sensor;
|
||||
}
|
||||
void set_breath_voc_equivalent_sensor(sensor::Sensor *breath_voc_equivalent_sensor) {
|
||||
breath_voc_equivalent_sensor_ = breath_voc_equivalent_sensor;
|
||||
}
|
||||
|
||||
static BME680BSECComponent *instance;
|
||||
static int8_t read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len);
|
||||
static int8_t write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len);
|
||||
static void delay_ms(uint32_t period);
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
void set_config_(const uint8_t *config);
|
||||
void update_subscription_(float sample_rate);
|
||||
|
||||
void run_();
|
||||
void read_(bsec_bme_settings_t bme680_settings);
|
||||
void publish_(const bsec_output_t *outputs, uint8_t num_outputs);
|
||||
int64_t get_time_ns_();
|
||||
|
||||
void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false);
|
||||
void publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value);
|
||||
|
||||
void load_state_();
|
||||
void save_state_(uint8_t accuracy);
|
||||
|
||||
struct bme680_dev bme680_;
|
||||
bsec_library_return_t bsec_status_{BSEC_OK};
|
||||
int8_t bme680_status_{BME680_OK};
|
||||
|
||||
int64_t last_time_ms_{0};
|
||||
uint32_t millis_overflow_counter_{0};
|
||||
int64_t next_call_ns_{0};
|
||||
|
||||
ESPPreferenceObject bsec_state_;
|
||||
uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day
|
||||
uint32_t last_state_save_ms_ = 0;
|
||||
|
||||
float temperature_offset_{0};
|
||||
IAQMode iaq_mode_{IAQ_MODE_STATIC};
|
||||
SampleRate sample_rate_{SAMPLE_RATE_LP};
|
||||
|
||||
sensor::Sensor *temperature_sensor_;
|
||||
sensor::Sensor *pressure_sensor_;
|
||||
sensor::Sensor *humidity_sensor_;
|
||||
sensor::Sensor *gas_resistance_sensor_;
|
||||
sensor::Sensor *iaq_sensor_;
|
||||
text_sensor::TextSensor *iaq_accuracy_text_sensor_;
|
||||
sensor::Sensor *iaq_accuracy_sensor_;
|
||||
sensor::Sensor *co2_equivalent_sensor_;
|
||||
sensor::Sensor *breath_voc_equivalent_sensor_;
|
||||
};
|
||||
#endif
|
||||
} // namespace bme680_bsec
|
||||
} // namespace esphome
|
||||
91
esphome/components/bme680_bsec/sensor.py
Normal file
91
esphome/components/bme680_bsec/sensor.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor
|
||||
from esphome.const import (
|
||||
CONF_GAS_RESISTANCE,
|
||||
CONF_HUMIDITY,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_EMPTY,
|
||||
UNIT_HECTOPASCAL,
|
||||
UNIT_OHM,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PERCENT,
|
||||
ICON_GAS_CYLINDER,
|
||||
ICON_GAUGE,
|
||||
ICON_THERMOMETER,
|
||||
ICON_WATER_PERCENT,
|
||||
)
|
||||
from esphome.core import coroutine
|
||||
from . import BME680BSECComponent, CONF_BME680_BSEC_ID
|
||||
|
||||
DEPENDENCIES = ["bme680_bsec"]
|
||||
|
||||
CONF_IAQ = "iaq"
|
||||
CONF_IAQ_ACCURACY = "iaq_accuracy"
|
||||
CONF_CO2_EQUIVALENT = "co2_equivalent"
|
||||
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
|
||||
UNIT_IAQ = "IAQ"
|
||||
ICON_ACCURACY = "mdi:checkbox-marked-circle-outline"
|
||||
ICON_TEST_TUBE = "mdi:test-tube"
|
||||
|
||||
TYPES = {
|
||||
CONF_TEMPERATURE: "set_temperature_sensor",
|
||||
CONF_PRESSURE: "set_pressure_sensor",
|
||||
CONF_HUMIDITY: "set_humidity_sensor",
|
||||
CONF_GAS_RESISTANCE: "set_gas_resistance_sensor",
|
||||
CONF_IAQ: "set_iaq_sensor",
|
||||
CONF_IAQ_ACCURACY: "set_iaq_accuracy_sensor",
|
||||
CONF_CO2_EQUIVALENT: "set_co2_equivalent_sensor",
|
||||
CONF_BREATH_VOC_EQUIVALENT: "set_breath_voc_equivalent_sensor",
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
UNIT_CELSIUS, ICON_THERMOMETER, 1, DEVICE_CLASS_TEMPERATURE
|
||||
),
|
||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||
UNIT_HECTOPASCAL, ICON_GAUGE, 1, DEVICE_CLASS_PRESSURE
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
UNIT_PERCENT, ICON_WATER_PERCENT, 1, DEVICE_CLASS_HUMIDITY
|
||||
),
|
||||
cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema(
|
||||
UNIT_OHM, ICON_GAS_CYLINDER, 0, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_IAQ): sensor.sensor_schema(
|
||||
UNIT_IAQ, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema(
|
||||
UNIT_EMPTY, ICON_ACCURACY, 0, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema(
|
||||
UNIT_PARTS_PER_MILLION, ICON_TEST_TUBE, 1, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema(
|
||||
UNIT_PARTS_PER_MILLION, ICON_TEST_TUBE, 1, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@coroutine
|
||||
def setup_conf(config, key, hub, funcName):
|
||||
if key in config:
|
||||
conf = config[key]
|
||||
var = yield sensor.new_sensor(conf)
|
||||
func = getattr(hub, funcName)
|
||||
cg.add(func(var))
|
||||
|
||||
|
||||
def to_code(config):
|
||||
hub = yield cg.get_variable(config[CONF_BME680_BSEC_ID])
|
||||
for key, funcName in TYPES.items():
|
||||
yield setup_conf(config, key, hub, funcName)
|
||||
41
esphome/components/bme680_bsec/text_sensor.py
Normal file
41
esphome/components/bme680_bsec/text_sensor.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import text_sensor
|
||||
from esphome.const import CONF_ID, CONF_ICON
|
||||
from esphome.core import coroutine
|
||||
from . import BME680BSECComponent, CONF_BME680_BSEC_ID
|
||||
|
||||
DEPENDENCIES = ["bme680_bsec"]
|
||||
|
||||
CONF_IAQ_ACCURACY = "iaq_accuracy"
|
||||
ICON_ACCURACY = "mdi:checkbox-marked-circle-outline"
|
||||
|
||||
TYPES = {CONF_IAQ_ACCURACY: "set_iaq_accuracy_text_sensor"}
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent),
|
||||
cv.Optional(CONF_IAQ_ACCURACY): text_sensor.TEXT_SENSOR_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
|
||||
cv.Optional(CONF_ICON, default=ICON_ACCURACY): cv.icon,
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@coroutine
|
||||
def setup_conf(config, key, hub, funcName):
|
||||
if key in config:
|
||||
conf = config[key]
|
||||
var = cg.new_Pvariable(conf[CONF_ID])
|
||||
yield text_sensor.register_text_sensor(var, conf)
|
||||
func = getattr(hub, funcName)
|
||||
cg.add(func(var))
|
||||
|
||||
|
||||
def to_code(config):
|
||||
hub = yield cg.get_variable(config[CONF_BME680_BSEC_ID])
|
||||
for key, funcName in TYPES.items():
|
||||
yield setup_conf(config, key, hub, funcName)
|
||||
@@ -16,7 +16,7 @@ class CustomComponentConstructor {
|
||||
}
|
||||
}
|
||||
|
||||
Component *get_component(int i) { return this->components_[i]; }
|
||||
Component *get_component(int i) const { return this->components_[i]; }
|
||||
|
||||
protected:
|
||||
std::vector<Component *> components_;
|
||||
|
||||
@@ -2,7 +2,7 @@ import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import core, automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_ROTATION
|
||||
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_PAGE_ID, CONF_ROTATION
|
||||
from esphome.core import coroutine, coroutine_with_priority
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -19,6 +19,9 @@ DisplayPageShowNextAction = display_ns.class_(
|
||||
DisplayPageShowPrevAction = display_ns.class_(
|
||||
"DisplayPageShowPrevAction", automation.Action
|
||||
)
|
||||
DisplayIsDisplayingPageCondition = display_ns.class_(
|
||||
"DisplayIsDisplayingPageCondition", automation.Condition
|
||||
)
|
||||
|
||||
DISPLAY_ROTATIONS = {
|
||||
0: display_ns.DISPLAY_ROTATION_0_DEGREES,
|
||||
@@ -125,6 +128,26 @@ def display_page_show_previous_to_code(config, action_id, template_arg, args):
|
||||
yield cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"display.is_displaying_page",
|
||||
DisplayIsDisplayingPageCondition,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(DisplayBuffer),
|
||||
cv.Required(CONF_PAGE_ID): cv.use_id(DisplayPage),
|
||||
},
|
||||
key=CONF_PAGE_ID,
|
||||
),
|
||||
)
|
||||
def display_is_displaying_page_to_code(config, condition_id, template_arg, args):
|
||||
paren = yield cg.get_variable(config[CONF_ID])
|
||||
page = yield cg.get_variable(config[CONF_PAGE_ID])
|
||||
var = cg.new_Pvariable(condition_id, template_arg, paren)
|
||||
cg.add(var.set_page(page))
|
||||
|
||||
yield var
|
||||
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
def to_code(config):
|
||||
cg.add_global(display_ns.using)
|
||||
|
||||
@@ -296,6 +296,8 @@ class DisplayBuffer {
|
||||
|
||||
void set_pages(std::vector<DisplayPage *> pages);
|
||||
|
||||
const DisplayPage *get_active_page() const { return this->page_; }
|
||||
|
||||
/// Internal method to set the display rotation with.
|
||||
void set_rotation(DisplayRotation rotation);
|
||||
|
||||
@@ -448,5 +450,17 @@ template<typename... Ts> class DisplayPageShowPrevAction : public Action<Ts...>
|
||||
DisplayBuffer *buffer_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisplayIsDisplayingPageCondition : public Condition<Ts...> {
|
||||
public:
|
||||
DisplayIsDisplayingPageCondition(DisplayBuffer *parent) : parent_(parent) {}
|
||||
|
||||
void set_page(DisplayPage *page) { this->page_ = page; }
|
||||
bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; }
|
||||
|
||||
protected:
|
||||
DisplayBuffer *parent_;
|
||||
DisplayPage *page_;
|
||||
};
|
||||
|
||||
} // namespace display
|
||||
} // namespace esphome
|
||||
|
||||
@@ -26,6 +26,7 @@ CONF_WINDOW = "window"
|
||||
CONF_ACTIVE = "active"
|
||||
esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker")
|
||||
ESP32BLETracker = esp32_ble_tracker_ns.class_("ESP32BLETracker", cg.Component)
|
||||
ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient")
|
||||
ESPBTDeviceListener = esp32_ble_tracker_ns.class_("ESPBTDeviceListener")
|
||||
ESPBTDevice = esp32_ble_tracker_ns.class_("ESPBTDevice")
|
||||
ESPBTDeviceConstRef = ESPBTDevice.operator("ref").operator("const")
|
||||
@@ -220,3 +221,10 @@ def register_ble_device(var, config):
|
||||
paren = yield cg.get_variable(config[CONF_ESP32_BLE_ID])
|
||||
cg.add(paren.register_listener(var))
|
||||
yield var
|
||||
|
||||
|
||||
@coroutine
|
||||
def register_client(var, config):
|
||||
paren = yield cg.get_variable(config[CONF_ESP32_BLE_ID])
|
||||
cg.add(paren.register_client(var))
|
||||
yield var
|
||||
|
||||
@@ -48,7 +48,23 @@ void ESP32BLETracker::setup() {
|
||||
}
|
||||
|
||||
void ESP32BLETracker::loop() {
|
||||
if (xSemaphoreTake(this->scan_end_lock_, 0L)) {
|
||||
BLEEvent *ble_event = this->ble_events_.pop();
|
||||
while (ble_event != nullptr) {
|
||||
if (ble_event->type_)
|
||||
this->real_gattc_event_handler(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if,
|
||||
&ble_event->event_.gattc.gattc_param);
|
||||
else
|
||||
this->real_gap_event_handler(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param);
|
||||
delete ble_event;
|
||||
ble_event = this->ble_events_.pop();
|
||||
}
|
||||
|
||||
bool connecting = false;
|
||||
for (auto *client : this->clients_) {
|
||||
if (client->state() == ClientState::Connecting || client->state() == ClientState::Discovered)
|
||||
connecting = true;
|
||||
}
|
||||
if (!connecting && xSemaphoreTake(this->scan_end_lock_, 0L)) {
|
||||
xSemaphoreGive(this->scan_end_lock_);
|
||||
global_esp32_ble_tracker->start_scan(false);
|
||||
}
|
||||
@@ -69,6 +85,17 @@ void ESP32BLETracker::loop() {
|
||||
if (listener->parse_device(device))
|
||||
found = true;
|
||||
|
||||
for (auto *client : this->clients_)
|
||||
if (client->parse_device(device)) {
|
||||
found = true;
|
||||
if (client->state() == ClientState::Discovered) {
|
||||
esp_ble_gap_stop_scanning();
|
||||
if (xSemaphoreTake(this->scan_end_lock_, 10L / portTICK_PERIOD_MS)) {
|
||||
xSemaphoreGive(this->scan_end_lock_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
this->print_bt_device_info(device);
|
||||
}
|
||||
@@ -122,6 +149,11 @@ bool ESP32BLETracker::ble_setup() {
|
||||
ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
err = esp_ble_gattc_register_callback(ESP32BLETracker::gattc_event_handler);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Empty name
|
||||
esp_ble_gap_set_device_name("");
|
||||
@@ -166,7 +198,17 @@ void ESP32BLETracker::start_scan(bool first) {
|
||||
});
|
||||
}
|
||||
|
||||
void ESP32BLETracker::register_client(ESPBTClient *client) {
|
||||
client->app_id = ++this->app_id_;
|
||||
this->clients_.push_back(client);
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
BLEEvent *gap_event = new BLEEvent(event, param);
|
||||
global_esp32_ble_tracker->ble_events_.push(gap_event);
|
||||
}
|
||||
|
||||
void ESP32BLETracker::real_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
switch (event) {
|
||||
case ESP_GAP_BLE_SCAN_RESULT_EVT:
|
||||
global_esp32_ble_tracker->gap_scan_result(param->scan_rst);
|
||||
@@ -177,6 +219,9 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
|
||||
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
|
||||
global_esp32_ble_tracker->gap_scan_start_complete(param->scan_start_cmpl);
|
||||
break;
|
||||
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
|
||||
global_esp32_ble_tracker->gap_scan_stop_complete(param->scan_stop_cmpl);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -190,6 +235,10 @@ void ESP32BLETracker::gap_scan_start_complete(const esp_ble_gap_cb_param_t::ble_
|
||||
this->scan_start_failed_ = param.status;
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_stop_complete(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) {
|
||||
xSemaphoreGive(this->scan_end_lock_);
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) {
|
||||
if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
|
||||
if (xSemaphoreTake(this->scan_result_lock_, 0L)) {
|
||||
@@ -203,6 +252,19 @@ void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_res
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
BLEEvent *gattc_event = new BLEEvent(event, gattc_if, param);
|
||||
global_esp32_ble_tracker->ble_events_.push(gattc_event);
|
||||
}
|
||||
|
||||
void ESP32BLETracker::real_gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
for (auto *client : global_esp32_ble_tracker->clients_) {
|
||||
client->gattc_event_handler(event, gattc_if, param);
|
||||
}
|
||||
}
|
||||
|
||||
ESPBTUUID::ESPBTUUID() : uuid_() {}
|
||||
ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) {
|
||||
ESPBTUUID ret;
|
||||
@@ -223,6 +285,15 @@ ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) {
|
||||
ret.uuid_.uuid.uuid128[i] = data[i];
|
||||
return ret;
|
||||
}
|
||||
ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) {
|
||||
ESPBTUUID ret;
|
||||
ret.uuid_.len = uuid.len;
|
||||
ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16;
|
||||
ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32;
|
||||
for (size_t i = 0; i < ESP_UUID_LEN_128; i++)
|
||||
ret.uuid_.uuid.uuid128[i] = uuid.uuid.uuid128[i];
|
||||
return ret;
|
||||
}
|
||||
ESPBTUUID ESPBTUUID::as_128bit() const {
|
||||
if (this->uuid_.len == ESP_UUID_LEN_128) {
|
||||
return *this;
|
||||
@@ -241,7 +312,7 @@ ESPBTUUID ESPBTUUID::as_128bit() const {
|
||||
}
|
||||
bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const {
|
||||
if (this->uuid_.len == ESP_UUID_LEN_16) {
|
||||
return (this->uuid_.uuid.uuid16 >> 8) == data2 || (this->uuid_.uuid.uuid16 & 0xFF) == data1;
|
||||
return (this->uuid_.uuid.uuid16 >> 8) == data2 && (this->uuid_.uuid.uuid16 & 0xFF) == data1;
|
||||
} else if (this->uuid_.len == ESP_UUID_LEN_32) {
|
||||
for (uint8_t i = 0; i < 3; i++) {
|
||||
bool a = ((this->uuid_.uuid.uuid32 >> i * 8) & 0xFF) == data1;
|
||||
@@ -289,16 +360,21 @@ std::string ESPBTUUID::to_string() {
|
||||
char sbuf[64];
|
||||
switch (this->uuid_.len) {
|
||||
case ESP_UUID_LEN_16:
|
||||
sprintf(sbuf, "%02X:%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff);
|
||||
sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff);
|
||||
break;
|
||||
case ESP_UUID_LEN_32:
|
||||
sprintf(sbuf, "%02X:%02X:%02X:%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff),
|
||||
sprintf(sbuf, "0x%02X%02X%02X%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff),
|
||||
(this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff);
|
||||
break;
|
||||
default:
|
||||
case ESP_UUID_LEN_128:
|
||||
for (uint8_t i = 0; i < 16; i++)
|
||||
sprintf(sbuf + i * 3, "%02X:", this->uuid_.uuid.uuid128[i]);
|
||||
char *bpos = sbuf;
|
||||
for (int8_t i = 15; i >= 0; i--) {
|
||||
sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]);
|
||||
bpos += 2;
|
||||
if (i == 3 || i == 5 || i == 7 || i == 9)
|
||||
sprintf(bpos++, "-");
|
||||
}
|
||||
sbuf[47] = '\0';
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "queue.h"
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gattc_api.h>
|
||||
#include <esp_bt_defs.h>
|
||||
|
||||
namespace esphome {
|
||||
@@ -23,6 +25,8 @@ class ESPBTUUID {
|
||||
|
||||
static ESPBTUUID from_raw(const uint8_t *data);
|
||||
|
||||
static ESPBTUUID from_uuid(esp_bt_uuid_t uuid);
|
||||
|
||||
ESPBTUUID as_128bit() const;
|
||||
|
||||
bool contains(uint8_t data1, uint8_t data2) const;
|
||||
@@ -135,6 +139,32 @@ class ESPBTDeviceListener {
|
||||
ESP32BLETracker *parent_{nullptr};
|
||||
};
|
||||
|
||||
enum class ClientState {
|
||||
// Connection is idle, no device detected.
|
||||
Idle,
|
||||
// Device advertisement found.
|
||||
Discovered,
|
||||
// Connection in progress.
|
||||
Connecting,
|
||||
// Initial connection established.
|
||||
Connected,
|
||||
// The client and sub-clients have completed setup.
|
||||
Established,
|
||||
};
|
||||
|
||||
class ESPBTClient : public ESPBTDeviceListener {
|
||||
public:
|
||||
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;
|
||||
virtual void connect() = 0;
|
||||
void set_state(ClientState st) { this->state_ = st; }
|
||||
ClientState state() const { return state_; }
|
||||
int app_id;
|
||||
|
||||
protected:
|
||||
ClientState state_;
|
||||
};
|
||||
|
||||
class ESP32BLETracker : public Component {
|
||||
public:
|
||||
void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; }
|
||||
@@ -153,6 +183,8 @@ class ESP32BLETracker : public Component {
|
||||
this->listeners_.push_back(listener);
|
||||
}
|
||||
|
||||
void register_client(ESPBTClient *client);
|
||||
|
||||
void print_bt_device_info(const ESPBTDevice &device);
|
||||
|
||||
protected:
|
||||
@@ -162,16 +194,26 @@ class ESP32BLETracker : public Component {
|
||||
void start_scan(bool first);
|
||||
/// Callback that will handle all GAP events and redistribute them to other callbacks.
|
||||
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
|
||||
void real_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
|
||||
/// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received.
|
||||
void gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m);
|
||||
/// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received.
|
||||
void gap_scan_set_param_complete(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m);
|
||||
/// Called when a `ESP_GAP_BLE_SCAN_START_COMPLETE_EVT` event is received.
|
||||
void gap_scan_start_complete(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m);
|
||||
/// Called when a `ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT` event is received.
|
||||
void gap_scan_stop_complete(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m);
|
||||
|
||||
int app_id_;
|
||||
/// Callback that will handle all GATTC events and redistribute them to other callbacks.
|
||||
static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
|
||||
void real_gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
|
||||
|
||||
/// Vector of addresses that have already been printed in print_bt_device_info
|
||||
std::vector<uint64_t> already_discovered_;
|
||||
std::vector<ESPBTDeviceListener *> listeners_;
|
||||
/// Client parameters.
|
||||
std::vector<ESPBTClient *> clients_;
|
||||
/// A structure holding the ESP BLE scan parameters.
|
||||
esp_ble_scan_params_t scan_params_;
|
||||
/// The interval in seconds to perform scans.
|
||||
@@ -185,6 +227,8 @@ class ESP32BLETracker : public Component {
|
||||
esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_buffer_[16];
|
||||
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
|
||||
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
|
||||
|
||||
Queue<BLEEvent> ble_events_;
|
||||
};
|
||||
|
||||
extern ESP32BLETracker *global_esp32_ble_tracker;
|
||||
|
||||
96
esphome/components/esp32_ble_tracker/queue.h
Normal file
96
esphome/components/esp32_ble_tracker/queue.h
Normal file
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
/*
|
||||
* BLE events come in from a separate Task (thread) in the ESP32 stack. Rather
|
||||
* than trying to deal wth various locking strategies, all incoming GAP and GATT
|
||||
* events will simply be placed on a semaphore guarded queue. The next time the
|
||||
* component runs loop(), these events are popped off the queue and handed at
|
||||
* this safer time.
|
||||
*/
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_ble_tracker {
|
||||
|
||||
template<class T> class Queue {
|
||||
public:
|
||||
Queue() { m = xSemaphoreCreateMutex(); }
|
||||
|
||||
void push(T *element) {
|
||||
if (element == nullptr)
|
||||
return;
|
||||
if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) {
|
||||
q.push(element);
|
||||
xSemaphoreGive(m);
|
||||
}
|
||||
}
|
||||
|
||||
T *pop() {
|
||||
T *element = nullptr;
|
||||
|
||||
if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) {
|
||||
if (!q.empty()) {
|
||||
element = q.front();
|
||||
q.pop();
|
||||
}
|
||||
xSemaphoreGive(m);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::queue<T *> q;
|
||||
SemaphoreHandle_t m;
|
||||
};
|
||||
|
||||
// Received GAP and GATTC events are only queued, and get processed in the main loop().
|
||||
// This class stores each event in a single type.
|
||||
class BLEEvent {
|
||||
public:
|
||||
BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
|
||||
this->event_.gap.gap_event = e;
|
||||
memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t));
|
||||
this->type_ = 0;
|
||||
};
|
||||
|
||||
BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
|
||||
this->event_.gattc.gattc_event = e;
|
||||
this->event_.gattc.gattc_if = i;
|
||||
memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t));
|
||||
// Need to also make a copy of notify event data.
|
||||
if (e == ESP_GATTC_NOTIFY_EVT) {
|
||||
memcpy(this->event_.gattc.notify_data, p->notify.value, p->notify.value_len);
|
||||
this->event_.gattc.gattc_param.notify.value = this->event_.gattc.notify_data;
|
||||
}
|
||||
this->type_ = 1;
|
||||
};
|
||||
|
||||
union {
|
||||
struct gap_event {
|
||||
esp_gap_ble_cb_event_t gap_event;
|
||||
esp_ble_gap_cb_param_t gap_param;
|
||||
} gap;
|
||||
|
||||
struct gattc_event {
|
||||
esp_gattc_cb_event_t gattc_event;
|
||||
esp_gatt_if_t gattc_if;
|
||||
esp_ble_gattc_cb_param_t gattc_param;
|
||||
uint8_t notify_data[64];
|
||||
} gattc;
|
||||
} event_;
|
||||
uint8_t type_; // 0=gap 1=gattc
|
||||
};
|
||||
|
||||
} // namespace esp32_ble_tracker
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
@@ -37,6 +37,11 @@ void HOT ESP8266PWM::write_state(float state) {
|
||||
uint32_t duty_off = total_time_us - duty_on;
|
||||
|
||||
if (duty_on == 0) {
|
||||
// This is a hacky fix for servos: Servo PWM high time is maximum 2.4ms by default
|
||||
// The frequency check is to affect this fix for servos mostly as the frequency is usually 50-300 hz
|
||||
if (this->pin_->digital_read() && 50 <= this->frequency_ && this->frequency_ <= 300) {
|
||||
delay(3);
|
||||
}
|
||||
stopWaveform(this->pin_->get_pin());
|
||||
this->pin_->digital_write(this->pin_->is_inverted());
|
||||
} else if (duty_off == 0) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from esphome import pins
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.network import add_mdns_library
|
||||
from esphome.const import (
|
||||
CONF_DOMAIN,
|
||||
CONF_ID,
|
||||
@@ -9,6 +10,7 @@ from esphome.const import (
|
||||
CONF_TYPE,
|
||||
CONF_USE_ADDRESS,
|
||||
ESP_PLATFORM_ESP32,
|
||||
CONF_ENABLE_MDNS,
|
||||
CONF_GATEWAY,
|
||||
CONF_SUBNET,
|
||||
CONF_DNS1,
|
||||
@@ -80,6 +82,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31),
|
||||
cv.Optional(CONF_POWER_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA,
|
||||
cv.Optional(CONF_ENABLE_MDNS, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
|
||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||
cv.Optional("hostname"): cv.invalid(
|
||||
@@ -122,3 +125,6 @@ def to_code(config):
|
||||
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
|
||||
|
||||
cg.add_define("USE_ETHERNET")
|
||||
|
||||
if config[CONF_ENABLE_MDNS]:
|
||||
add_mdns_library()
|
||||
|
||||
@@ -33,7 +33,9 @@ void EthernetComponent::setup() {
|
||||
|
||||
this->start_connect_();
|
||||
|
||||
#ifdef USE_MDNS
|
||||
network_setup_mdns();
|
||||
#endif
|
||||
}
|
||||
void EthernetComponent::loop() {
|
||||
const uint32_t now = millis();
|
||||
|
||||
197
esphome/components/external_components/__init__.py
Normal file
197
esphome/components/external_components/__init__.py
Normal file
@@ -0,0 +1,197 @@
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import hashlib
|
||||
import datetime
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_COMPONENTS,
|
||||
CONF_SOURCE,
|
||||
CONF_URL,
|
||||
CONF_TYPE,
|
||||
CONF_EXTERNAL_COMPONENTS,
|
||||
CONF_PATH,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome import loader
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = CONF_EXTERNAL_COMPONENTS
|
||||
|
||||
TYPE_GIT = "git"
|
||||
TYPE_LOCAL = "local"
|
||||
CONF_REFRESH = "refresh"
|
||||
CONF_REF = "ref"
|
||||
|
||||
|
||||
def validate_git_ref(value):
|
||||
if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None:
|
||||
raise cv.Invalid("Not a valid git ref")
|
||||
return value
|
||||
|
||||
|
||||
GIT_SCHEMA = {
|
||||
cv.Required(CONF_URL): cv.url,
|
||||
cv.Optional(CONF_REF): validate_git_ref,
|
||||
}
|
||||
LOCAL_SCHEMA = {
|
||||
cv.Required(CONF_PATH): cv.directory,
|
||||
}
|
||||
|
||||
|
||||
def validate_source_shorthand(value):
|
||||
if not isinstance(value, str):
|
||||
raise cv.Invalid("Shorthand only for strings")
|
||||
try:
|
||||
return SOURCE_SCHEMA({CONF_TYPE: TYPE_LOCAL, CONF_PATH: value})
|
||||
except cv.Invalid:
|
||||
pass
|
||||
# Regex for GitHub repo name with optional branch/tag
|
||||
# Note: git allows other branch/tag names as well, but never seen them used before
|
||||
m = re.match(
|
||||
r"github://([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@([a-zA-Z0-9\-_.\./]+))?",
|
||||
value,
|
||||
)
|
||||
if m is None:
|
||||
raise cv.Invalid(
|
||||
"Source is not a file system path or in expected github://username/name[@branch-or-tag] format!"
|
||||
)
|
||||
conf = {
|
||||
CONF_TYPE: TYPE_GIT,
|
||||
CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git",
|
||||
}
|
||||
if m.group(3):
|
||||
conf[CONF_REF] = m.group(3)
|
||||
return SOURCE_SCHEMA(conf)
|
||||
|
||||
|
||||
def validate_refresh(value: str):
|
||||
if value.lower() == "always":
|
||||
return validate_refresh("0s")
|
||||
if value.lower() == "never":
|
||||
return validate_refresh("1000y")
|
||||
return cv.positive_time_period_seconds(value)
|
||||
|
||||
|
||||
SOURCE_SCHEMA = cv.Any(
|
||||
validate_source_shorthand,
|
||||
cv.typed_schema(
|
||||
{
|
||||
TYPE_GIT: cv.Schema(GIT_SCHEMA),
|
||||
TYPE_LOCAL: cv.Schema(LOCAL_SCHEMA),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.ensure_list(
|
||||
{
|
||||
cv.Required(CONF_SOURCE): SOURCE_SCHEMA,
|
||||
cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, validate_refresh),
|
||||
cv.Optional(CONF_COMPONENTS, default="all"): cv.Any(
|
||||
"all", cv.ensure_list(cv.string)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
pass
|
||||
|
||||
|
||||
def _compute_destination_path(key: str) -> Path:
|
||||
base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN
|
||||
h = hashlib.new("sha256")
|
||||
h.update(key.encode())
|
||||
return base_dir / h.hexdigest()[:8]
|
||||
|
||||
|
||||
def _handle_git_response(ret):
|
||||
if ret.returncode != 0 and ret.stderr:
|
||||
err_str = ret.stderr.decode("utf-8")
|
||||
lines = [x.strip() for x in err_str.splitlines()]
|
||||
if lines[-1].startswith("fatal:"):
|
||||
raise cv.Invalid(lines[-1][len("fatal: ") :])
|
||||
raise cv.Invalid(err_str)
|
||||
|
||||
|
||||
def _process_single_config(config: dict):
|
||||
conf = config[CONF_SOURCE]
|
||||
if conf[CONF_TYPE] == TYPE_GIT:
|
||||
key = f"{conf[CONF_URL]}@{conf.get(CONF_REF)}"
|
||||
repo_dir = _compute_destination_path(key)
|
||||
if not repo_dir.is_dir():
|
||||
cmd = ["git", "clone", "--depth=1"]
|
||||
if CONF_REF in conf:
|
||||
cmd += ["--branch", conf[CONF_REF]]
|
||||
cmd += [conf[CONF_URL], str(repo_dir)]
|
||||
ret = subprocess.run(cmd, capture_output=True, check=False)
|
||||
_handle_git_response(ret)
|
||||
|
||||
else:
|
||||
# Check refresh needed
|
||||
file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD")
|
||||
# On first clone, FETCH_HEAD does not exists
|
||||
if not file_timestamp.exists():
|
||||
file_timestamp = Path(repo_dir / ".git" / "HEAD")
|
||||
age = datetime.datetime.now() - datetime.datetime.fromtimestamp(
|
||||
file_timestamp.stat().st_mtime
|
||||
)
|
||||
if age.seconds > config[CONF_REFRESH].total_seconds:
|
||||
_LOGGER.info("Executing git pull %s", key)
|
||||
cmd = ["git", "pull"]
|
||||
ret = subprocess.run(
|
||||
cmd, cwd=repo_dir, capture_output=True, check=False
|
||||
)
|
||||
_handle_git_response(ret)
|
||||
|
||||
if (repo_dir / "esphome" / "components").is_dir():
|
||||
components_dir = repo_dir / "esphome" / "components"
|
||||
elif (repo_dir / "components").is_dir():
|
||||
components_dir = repo_dir / "components"
|
||||
else:
|
||||
raise cv.Invalid(
|
||||
"Could not find components folder for source. Please check the source contains a 'components' or 'esphome/components' folder",
|
||||
[CONF_SOURCE],
|
||||
)
|
||||
|
||||
elif conf[CONF_TYPE] == TYPE_LOCAL:
|
||||
components_dir = Path(CORE.relative_config_path(conf[CONF_PATH]))
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
if config[CONF_COMPONENTS] == "all":
|
||||
num_components = len(list(components_dir.glob("*/__init__.py")))
|
||||
if num_components > 100:
|
||||
# Prevent accidentally including all components from an esphome fork/branch
|
||||
# In this case force the user to manually specify which components they want to include
|
||||
raise cv.Invalid(
|
||||
"This source is an ESPHome fork or branch. Please manually specify the components you want to import using the 'components' key",
|
||||
[CONF_COMPONENTS],
|
||||
)
|
||||
allowed_components = None
|
||||
else:
|
||||
for i, name in enumerate(config[CONF_COMPONENTS]):
|
||||
expected = components_dir / name / "__init__.py"
|
||||
if not expected.is_file():
|
||||
raise cv.Invalid(
|
||||
f"Could not find __init__.py file for component {name}. Please check the component is defined by this source (search path: {expected})",
|
||||
[CONF_COMPONENTS, i],
|
||||
)
|
||||
allowed_components = config[CONF_COMPONENTS]
|
||||
|
||||
loader.install_meta_finder(components_dir, allowed_components=allowed_components)
|
||||
|
||||
|
||||
def do_external_components_pass(config: dict) -> None:
|
||||
conf = config.get(DOMAIN)
|
||||
if conf is None:
|
||||
return
|
||||
with cv.prepend_path(DOMAIN):
|
||||
conf = CONFIG_SCHEMA(conf)
|
||||
for i, c in enumerate(conf):
|
||||
with cv.prepend_path(i):
|
||||
_process_single_config(c)
|
||||
293
esphome/components/fingerprint_grow/__init__.py
Normal file
293
esphome/components/fingerprint_grow/__init__.py
Normal file
@@ -0,0 +1,293 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome import pins
|
||||
from esphome.components import uart
|
||||
from esphome.const import (
|
||||
CONF_COLOR,
|
||||
CONF_COUNT,
|
||||
CONF_FINGER_ID,
|
||||
CONF_ID,
|
||||
CONF_NEW_PASSWORD,
|
||||
CONF_NUM_SCANS,
|
||||
CONF_ON_ENROLLMENT_DONE,
|
||||
CONF_ON_ENROLLMENT_FAILED,
|
||||
CONF_ON_ENROLLMENT_SCAN,
|
||||
CONF_ON_FINGER_SCAN_MATCHED,
|
||||
CONF_ON_FINGER_SCAN_UNMATCHED,
|
||||
CONF_PASSWORD,
|
||||
CONF_SENSING_PIN,
|
||||
CONF_SPEED,
|
||||
CONF_STATE,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@OnFreund", "@loongyh"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
AUTO_LOAD = ["binary_sensor", "sensor"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_FINGERPRINT_GROW_ID = "fingerprint_grow_id"
|
||||
|
||||
fingerprint_grow_ns = cg.esphome_ns.namespace("fingerprint_grow")
|
||||
FingerprintGrowComponent = fingerprint_grow_ns.class_(
|
||||
"FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice
|
||||
)
|
||||
|
||||
FingerScanMatchedTrigger = fingerprint_grow_ns.class_(
|
||||
"FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16)
|
||||
)
|
||||
|
||||
FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_(
|
||||
"FingerScanUnmatchedTrigger", automation.Trigger.template()
|
||||
)
|
||||
|
||||
EnrollmentScanTrigger = fingerprint_grow_ns.class_(
|
||||
"EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16)
|
||||
)
|
||||
|
||||
EnrollmentDoneTrigger = fingerprint_grow_ns.class_(
|
||||
"EnrollmentDoneTrigger", automation.Trigger.template(cg.uint16)
|
||||
)
|
||||
|
||||
EnrollmentFailedTrigger = fingerprint_grow_ns.class_(
|
||||
"EnrollmentFailedTrigger", automation.Trigger.template(cg.uint16)
|
||||
)
|
||||
|
||||
EnrollmentAction = fingerprint_grow_ns.class_("EnrollmentAction", automation.Action)
|
||||
CancelEnrollmentAction = fingerprint_grow_ns.class_(
|
||||
"CancelEnrollmentAction", automation.Action
|
||||
)
|
||||
DeleteAction = fingerprint_grow_ns.class_("DeleteAction", automation.Action)
|
||||
DeleteAllAction = fingerprint_grow_ns.class_("DeleteAllAction", automation.Action)
|
||||
LEDControlAction = fingerprint_grow_ns.class_("LEDControlAction", automation.Action)
|
||||
AuraLEDControlAction = fingerprint_grow_ns.class_(
|
||||
"AuraLEDControlAction", automation.Action
|
||||
)
|
||||
|
||||
AuraLEDState = fingerprint_grow_ns.enum("GrowAuraLEDState", True)
|
||||
AURA_LED_STATES = {
|
||||
"BREATHING": AuraLEDState.BREATHING,
|
||||
"FLASHING": AuraLEDState.FLASHING,
|
||||
"ALWAYS_ON": AuraLEDState.ALWAYS_ON,
|
||||
"ALWAYS_OFF": AuraLEDState.ALWAYS_OFF,
|
||||
"GRADUAL_ON": AuraLEDState.GRADUAL_ON,
|
||||
"GRADUAL_OFF": AuraLEDState.GRADUAL_OFF,
|
||||
}
|
||||
validate_aura_led_states = cv.enum(AURA_LED_STATES, upper=True)
|
||||
AuraLEDColor = fingerprint_grow_ns.enum("GrowAuraLEDColor", True)
|
||||
AURA_LED_COLORS = {
|
||||
"RED": AuraLEDColor.RED,
|
||||
"BLUE": AuraLEDColor.BLUE,
|
||||
"PURPLE": AuraLEDColor.PURPLE,
|
||||
}
|
||||
validate_aura_led_colors = cv.enum(AURA_LED_COLORS, upper=True)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(FingerprintGrowComponent),
|
||||
cv.Optional(CONF_SENSING_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Optional(CONF_PASSWORD): cv.uint32_t,
|
||||
cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t,
|
||||
cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
FingerScanMatchedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_FINGER_SCAN_UNMATCHED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
FingerScanUnmatchedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
EnrollmentScanTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
EnrollmentDoneTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
EnrollmentFailedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("500ms"))
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
yield cg.register_component(var, config)
|
||||
if CONF_PASSWORD in config:
|
||||
password = config[CONF_PASSWORD]
|
||||
cg.add(var.set_password(password))
|
||||
yield uart.register_uart_device(var, config)
|
||||
|
||||
if CONF_NEW_PASSWORD in config:
|
||||
new_password = config[CONF_NEW_PASSWORD]
|
||||
cg.add(var.set_new_password(new_password))
|
||||
|
||||
if CONF_SENSING_PIN in config:
|
||||
sensing_pin = yield cg.gpio_pin_expression(config[CONF_SENSING_PIN])
|
||||
cg.add(var.set_sensing_pin(sensing_pin))
|
||||
|
||||
for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
yield automation.build_automation(
|
||||
trigger, [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_FINGER_SCAN_UNMATCHED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
yield automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
yield automation.build_automation(
|
||||
trigger, [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_ENROLLMENT_DONE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
yield automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
yield automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"fingerprint_grow.enroll",
|
||||
EnrollmentAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(FingerprintGrowComponent),
|
||||
cv.Required(CONF_FINGER_ID): cv.templatable(cv.uint16_t),
|
||||
cv.Optional(CONF_NUM_SCANS): cv.templatable(cv.uint8_t),
|
||||
},
|
||||
key=CONF_FINGER_ID,
|
||||
),
|
||||
)
|
||||
def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
yield cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
template_ = yield cg.templatable(config[CONF_FINGER_ID], args, cg.uint16)
|
||||
cg.add(var.set_finger_id(template_))
|
||||
if CONF_NUM_SCANS in config:
|
||||
template_ = yield cg.templatable(config[CONF_NUM_SCANS], args, cg.uint8)
|
||||
cg.add(var.set_num_scans(template_))
|
||||
yield var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"fingerprint_grow.cancel_enroll",
|
||||
CancelEnrollmentAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(FingerprintGrowComponent),
|
||||
}
|
||||
),
|
||||
)
|
||||
def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
yield cg.register_parented(var, config[CONF_ID])
|
||||
yield var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"fingerprint_grow.delete",
|
||||
DeleteAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(FingerprintGrowComponent),
|
||||
cv.Required(CONF_FINGER_ID): cv.templatable(cv.uint16_t),
|
||||
},
|
||||
key=CONF_FINGER_ID,
|
||||
),
|
||||
)
|
||||
def fingerprint_grow_delete_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
yield cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
template_ = yield cg.templatable(config[CONF_FINGER_ID], args, cg.uint16)
|
||||
cg.add(var.set_finger_id(template_))
|
||||
yield var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"fingerprint_grow.delete_all",
|
||||
DeleteAllAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(FingerprintGrowComponent),
|
||||
}
|
||||
),
|
||||
)
|
||||
def fingerprint_grow_delete_all_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
yield cg.register_parented(var, config[CONF_ID])
|
||||
yield var
|
||||
|
||||
|
||||
FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(FingerprintGrowComponent),
|
||||
cv.Required(CONF_STATE): cv.templatable(cv.boolean),
|
||||
},
|
||||
key=CONF_STATE,
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"fingerprint_grow.led_control",
|
||||
LEDControlAction,
|
||||
FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA,
|
||||
)
|
||||
def fingerprint_grow_led_control_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
yield cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
template_ = yield cg.templatable(config[CONF_STATE], args, cg.bool_)
|
||||
cg.add(var.set_state(template_))
|
||||
yield var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"fingerprint_grow.aura_led_control",
|
||||
AuraLEDControlAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(FingerprintGrowComponent),
|
||||
cv.Required(CONF_STATE): cv.templatable(validate_aura_led_states),
|
||||
cv.Required(CONF_SPEED): cv.templatable(cv.uint8_t),
|
||||
cv.Required(CONF_COLOR): cv.templatable(validate_aura_led_colors),
|
||||
cv.Required(CONF_COUNT): cv.templatable(cv.uint8_t),
|
||||
}
|
||||
),
|
||||
)
|
||||
def fingerprint_grow_aura_led_control_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
yield cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
for key in [CONF_STATE, CONF_SPEED, CONF_COLOR, CONF_COUNT]:
|
||||
template_ = yield cg.templatable(config[key], args, cg.uint8)
|
||||
cg.add(getattr(var, f"set_{key}")(template_))
|
||||
yield var
|
||||
20
esphome/components/fingerprint_grow/binary_sensor.py
Normal file
20
esphome/components/fingerprint_grow/binary_sensor.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import binary_sensor
|
||||
from esphome.const import CONF_ICON, ICON_KEY_PLUS
|
||||
from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent
|
||||
|
||||
DEPENDENCIES = ["fingerprint_grow"]
|
||||
|
||||
CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent),
|
||||
cv.Optional(CONF_ICON, default=ICON_KEY_PLUS): cv.icon,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
hub = yield cg.get_variable(config[CONF_FINGERPRINT_GROW_ID])
|
||||
var = yield binary_sensor.new_binary_sensor(config)
|
||||
cg.add(hub.set_enrolling_binary_sensor(var))
|
||||
434
esphome/components/fingerprint_grow/fingerprint_grow.cpp
Normal file
434
esphome/components/fingerprint_grow/fingerprint_grow.cpp
Normal file
@@ -0,0 +1,434 @@
|
||||
#include "fingerprint_grow.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace fingerprint_grow {
|
||||
|
||||
static const char* TAG = "fingerprint_grow";
|
||||
|
||||
// Based on Adafruit's library: https://github.com/adafruit/Adafruit-Fingerprint-Sensor-Library
|
||||
|
||||
void FingerprintGrowComponent::update() {
|
||||
if (this->enrollment_image_ > this->enrollment_buffers_) {
|
||||
this->finish_enrollment(this->save_fingerprint_());
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->sensing_pin_ != nullptr) {
|
||||
if (this->sensing_pin_->digital_read() == HIGH) {
|
||||
ESP_LOGV(TAG, "No touch sensing");
|
||||
this->waiting_removal_ = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->waiting_removal_) {
|
||||
if (this->scan_image_(1) == NO_FINGER) {
|
||||
ESP_LOGD(TAG, "Finger removed");
|
||||
this->waiting_removal_ = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->enrollment_image_ == 0) {
|
||||
this->scan_and_match_();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t result = this->scan_image_(this->enrollment_image_);
|
||||
if (result == NO_FINGER) {
|
||||
return;
|
||||
}
|
||||
this->waiting_removal_ = true;
|
||||
if (result != OK) {
|
||||
this->finish_enrollment(result);
|
||||
return;
|
||||
}
|
||||
this->enrollment_scan_callback_.call(this->enrollment_image_, this->enrollment_slot_);
|
||||
++this->enrollment_image_;
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader...");
|
||||
if (this->check_password_()) {
|
||||
if (this->new_password_ != nullptr) {
|
||||
if (this->set_password_())
|
||||
return;
|
||||
} else {
|
||||
if (this->get_parameters_())
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->mark_failed();
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::enroll_fingerprint(uint16_t finger_id, uint8_t num_buffers) {
|
||||
ESP_LOGI(TAG, "Starting enrollment in slot %d", finger_id);
|
||||
if (this->enrolling_binary_sensor_ != nullptr) {
|
||||
this->enrolling_binary_sensor_->publish_state(true);
|
||||
}
|
||||
this->enrollment_slot_ = finger_id;
|
||||
this->enrollment_buffers_ = num_buffers;
|
||||
this->enrollment_image_ = 1;
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::finish_enrollment(uint8_t result) {
|
||||
if (result == OK) {
|
||||
this->enrollment_done_callback_.call(this->enrollment_slot_);
|
||||
} else {
|
||||
this->enrollment_failed_callback_.call(this->enrollment_slot_);
|
||||
}
|
||||
this->enrollment_image_ = 0;
|
||||
this->enrollment_slot_ = 0;
|
||||
if (this->enrolling_binary_sensor_ != nullptr) {
|
||||
this->enrolling_binary_sensor_->publish_state(false);
|
||||
}
|
||||
ESP_LOGI(TAG, "Finished enrollment");
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::scan_and_match_() {
|
||||
if (this->sensing_pin_ != nullptr) {
|
||||
ESP_LOGD(TAG, "Scan and match");
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Scan and match");
|
||||
}
|
||||
if (this->scan_image_(1) == OK) {
|
||||
this->waiting_removal_ = true;
|
||||
this->data_ = {SEARCH, 0x01, 0x00, 0x00, (uint8_t)(this->capacity_ >> 8), (uint8_t)(this->capacity_ & 0xFF)};
|
||||
switch (this->send_command_()) {
|
||||
case OK: {
|
||||
ESP_LOGD(TAG, "Fingerprint matched");
|
||||
uint16_t finger_id = ((uint16_t) this->data_[1] << 8) | this->data_[2];
|
||||
uint16_t confidence = ((uint16_t) this->data_[3] << 8) | this->data_[4];
|
||||
if (this->last_finger_id_sensor_ != nullptr) {
|
||||
this->last_finger_id_sensor_->publish_state(finger_id);
|
||||
}
|
||||
if (this->last_confidence_sensor_ != nullptr) {
|
||||
this->last_confidence_sensor_->publish_state(confidence);
|
||||
}
|
||||
this->finger_scan_matched_callback_.call(finger_id, confidence);
|
||||
break;
|
||||
}
|
||||
case NOT_FOUND:
|
||||
ESP_LOGD(TAG, "Fingerprint not matched to any saved slots");
|
||||
this->finger_scan_unmatched_callback_.call();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) {
|
||||
if (this->sensing_pin_ != nullptr) {
|
||||
ESP_LOGD(TAG, "Getting image %d", buffer);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Getting image %d", buffer);
|
||||
}
|
||||
this->data_ = {GET_IMAGE};
|
||||
switch (this->send_command_()) {
|
||||
case OK:
|
||||
break;
|
||||
case NO_FINGER:
|
||||
if (this->sensing_pin_ != nullptr) {
|
||||
ESP_LOGD(TAG, "No finger");
|
||||
} else {
|
||||
ESP_LOGV(TAG, "No finger");
|
||||
}
|
||||
return this->data_[0];
|
||||
case IMAGE_FAIL:
|
||||
ESP_LOGE(TAG, "Imaging error");
|
||||
default:
|
||||
return this->data_[0];
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Processing image %d", buffer);
|
||||
this->data_ = {IMAGE_2_TZ, buffer};
|
||||
switch (this->send_command_()) {
|
||||
case OK:
|
||||
ESP_LOGI(TAG, "Processed image %d", buffer);
|
||||
break;
|
||||
case IMAGE_MESS:
|
||||
ESP_LOGE(TAG, "Image too messy");
|
||||
break;
|
||||
case FEATURE_FAIL:
|
||||
case INVALID_IMAGE:
|
||||
ESP_LOGE(TAG, "Could not find fingerprint features");
|
||||
break;
|
||||
}
|
||||
return this->data_[0];
|
||||
}
|
||||
|
||||
uint8_t FingerprintGrowComponent::save_fingerprint_() {
|
||||
ESP_LOGI(TAG, "Creating model");
|
||||
this->data_ = {REG_MODEL};
|
||||
switch (this->send_command_()) {
|
||||
case OK:
|
||||
break;
|
||||
case ENROLL_MISMATCH:
|
||||
ESP_LOGE(TAG, "Scans do not match");
|
||||
default:
|
||||
return this->data_[0];
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Storing model");
|
||||
this->data_ = {STORE, 0x01, (uint8_t)(this->enrollment_slot_ >> 8), (uint8_t)(this->enrollment_slot_ & 0xFF)};
|
||||
switch (this->send_command_()) {
|
||||
case OK:
|
||||
ESP_LOGI(TAG, "Stored model");
|
||||
break;
|
||||
case BAD_LOCATION:
|
||||
ESP_LOGE(TAG, "Invalid slot");
|
||||
break;
|
||||
case FLASH_ERR:
|
||||
ESP_LOGE(TAG, "Error writing to flash");
|
||||
break;
|
||||
}
|
||||
return this->data_[0];
|
||||
}
|
||||
|
||||
bool FingerprintGrowComponent::check_password_() {
|
||||
ESP_LOGD(TAG, "Checking password");
|
||||
this->data_ = {VERIFY_PASSWORD, (uint8_t)(this->password_ >> 24), (uint8_t)(this->password_ >> 16),
|
||||
(uint8_t)(this->password_ >> 8), (uint8_t)(this->password_ & 0xFF)};
|
||||
switch (this->send_command_()) {
|
||||
case OK:
|
||||
ESP_LOGD(TAG, "Password verified");
|
||||
return true;
|
||||
case PASSWORD_FAIL:
|
||||
ESP_LOGE(TAG, "Wrong password");
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FingerprintGrowComponent::set_password_() {
|
||||
ESP_LOGI(TAG, "Setting new password: %d", *this->new_password_);
|
||||
this->data_ = {SET_PASSWORD, (uint8_t)(*this->new_password_ >> 24), (uint8_t)(*this->new_password_ >> 16),
|
||||
(uint8_t)(*this->new_password_ >> 8), (uint8_t)(*this->new_password_ & 0xFF)};
|
||||
if (this->send_command_() == OK) {
|
||||
ESP_LOGI(TAG, "New password successfully set");
|
||||
ESP_LOGI(TAG, "Define the new password in your configuration and reflash now");
|
||||
ESP_LOGW(TAG, "!!!Forgetting the password will render your device unusable!!!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FingerprintGrowComponent::get_parameters_() {
|
||||
ESP_LOGD(TAG, "Getting parameters");
|
||||
this->data_ = {READ_SYS_PARAM};
|
||||
if (this->send_command_() == OK) {
|
||||
ESP_LOGD(TAG, "Got parameters");
|
||||
if (this->status_sensor_ != nullptr) {
|
||||
this->status_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]);
|
||||
}
|
||||
this->capacity_ = ((uint16_t) this->data_[5] << 8) | this->data_[6];
|
||||
if (this->capacity_sensor_ != nullptr) {
|
||||
this->capacity_sensor_->publish_state(this->capacity_);
|
||||
}
|
||||
if (this->security_level_sensor_ != nullptr) {
|
||||
this->security_level_sensor_->publish_state(((uint16_t) this->data_[7] << 8) | this->data_[8]);
|
||||
}
|
||||
if (this->enrolling_binary_sensor_ != nullptr) {
|
||||
this->enrolling_binary_sensor_->publish_state(false);
|
||||
}
|
||||
this->get_fingerprint_count_();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::get_fingerprint_count_() {
|
||||
ESP_LOGD(TAG, "Getting fingerprint count");
|
||||
this->data_ = {TEMPLATE_COUNT};
|
||||
if (this->send_command_() == OK) {
|
||||
ESP_LOGD(TAG, "Got fingerprint count");
|
||||
if (this->fingerprint_count_sensor_ != nullptr)
|
||||
this->fingerprint_count_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]);
|
||||
}
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::delete_fingerprint(uint16_t finger_id) {
|
||||
ESP_LOGI(TAG, "Deleting fingerprint in slot %d", finger_id);
|
||||
this->data_ = {DELETE, (uint8_t)(finger_id >> 8), (uint8_t)(finger_id & 0xFF), 0x00, 0x01};
|
||||
switch (this->send_command_()) {
|
||||
case OK:
|
||||
ESP_LOGI(TAG, "Deleted fingerprint");
|
||||
this->get_fingerprint_count_();
|
||||
break;
|
||||
case DELETE_FAIL:
|
||||
ESP_LOGE(TAG, "Reader failed to delete fingerprint");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::delete_all_fingerprints() {
|
||||
ESP_LOGI(TAG, "Deleting all stored fingerprints");
|
||||
this->data_ = {EMPTY};
|
||||
switch (this->send_command_()) {
|
||||
case OK:
|
||||
ESP_LOGI(TAG, "Deleted all fingerprints");
|
||||
this->get_fingerprint_count_();
|
||||
break;
|
||||
case DB_CLEAR_FAIL:
|
||||
ESP_LOGE(TAG, "Reader failed to clear fingerprint library");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::led_control(bool state) {
|
||||
ESP_LOGD(TAG, "Setting LED");
|
||||
if (state)
|
||||
this->data_ = {LED_ON};
|
||||
else
|
||||
this->data_ = {LED_OFF};
|
||||
switch (this->send_command_()) {
|
||||
case OK:
|
||||
ESP_LOGD(TAG, "LED set");
|
||||
break;
|
||||
case PACKET_RCV_ERR:
|
||||
case TIMEOUT:
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Try aura_led_control instead");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, uint8_t color, uint8_t count) {
|
||||
const uint32_t now = millis();
|
||||
const uint32_t elapsed = now - this->last_aura_led_control_;
|
||||
if (elapsed < this->last_aura_led_duration_) {
|
||||
delay(this->last_aura_led_duration_ - elapsed);
|
||||
}
|
||||
ESP_LOGD(TAG, "Setting Aura LED");
|
||||
this->data_ = {AURA_CONFIG, state, speed, color, count};
|
||||
switch (this->send_command_()) {
|
||||
case OK:
|
||||
ESP_LOGD(TAG, "Aura LED set");
|
||||
this->last_aura_led_control_ = millis();
|
||||
this->last_aura_led_duration_ = 10 * speed * count;
|
||||
break;
|
||||
case PACKET_RCV_ERR:
|
||||
case TIMEOUT:
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Try led_control instead");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t FingerprintGrowComponent::send_command_() {
|
||||
this->write((uint8_t)(START_CODE >> 8));
|
||||
this->write((uint8_t)(START_CODE & 0xFF));
|
||||
this->write(this->address_[0]);
|
||||
this->write(this->address_[1]);
|
||||
this->write(this->address_[2]);
|
||||
this->write(this->address_[3]);
|
||||
this->write(COMMAND);
|
||||
|
||||
uint16_t wire_length = this->data_.size() + 2;
|
||||
this->write((uint8_t)(wire_length >> 8));
|
||||
this->write((uint8_t)(wire_length & 0xFF));
|
||||
|
||||
uint16_t sum = ((wire_length) >> 8) + ((wire_length) &0xFF) + COMMAND;
|
||||
for (auto data : this->data_) {
|
||||
this->write(data);
|
||||
sum += data;
|
||||
}
|
||||
|
||||
this->write((uint8_t)(sum >> 8));
|
||||
this->write((uint8_t)(sum & 0xFF));
|
||||
|
||||
this->data_.clear();
|
||||
|
||||
uint8_t byte;
|
||||
uint16_t idx = 0, length = 0;
|
||||
|
||||
for (uint16_t timer = 0; timer < 1000; timer++) {
|
||||
if (this->available() == 0) {
|
||||
delay(1);
|
||||
continue;
|
||||
}
|
||||
byte = this->read();
|
||||
switch (idx) {
|
||||
case 0:
|
||||
if (byte != (uint8_t)(START_CODE >> 8))
|
||||
continue;
|
||||
break;
|
||||
case 1:
|
||||
if (byte != (uint8_t)(START_CODE & 0xFF)) {
|
||||
idx = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
if (byte != this->address_[idx - 2]) {
|
||||
idx = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 6:
|
||||
if (byte != ACK) {
|
||||
idx = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 7:
|
||||
length = (uint16_t) byte << 8;
|
||||
break;
|
||||
case 8:
|
||||
length |= byte;
|
||||
break;
|
||||
default:
|
||||
this->data_.push_back(byte);
|
||||
if ((idx - 8) == length) {
|
||||
switch (this->data_[0]) {
|
||||
case OK:
|
||||
case NO_FINGER:
|
||||
case IMAGE_FAIL:
|
||||
case IMAGE_MESS:
|
||||
case FEATURE_FAIL:
|
||||
case NO_MATCH:
|
||||
case NOT_FOUND:
|
||||
case ENROLL_MISMATCH:
|
||||
case BAD_LOCATION:
|
||||
case DELETE_FAIL:
|
||||
case DB_CLEAR_FAIL:
|
||||
case PASSWORD_FAIL:
|
||||
case INVALID_IMAGE:
|
||||
case FLASH_ERR:
|
||||
break;
|
||||
case PACKET_RCV_ERR:
|
||||
ESP_LOGE(TAG, "Reader failed to process request");
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Unknown response received from reader: %d", this->data_[0]);
|
||||
break;
|
||||
}
|
||||
return this->data_[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
ESP_LOGE(TAG, "No response received from reader");
|
||||
this->data_[0] = TIMEOUT;
|
||||
return TIMEOUT;
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "GROW_FINGERPRINT_READER:");
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Fingerprint Count", this->fingerprint_count_sensor_);
|
||||
LOG_SENSOR(" ", "Status", this->status_sensor_);
|
||||
LOG_SENSOR(" ", "Capacity", this->capacity_sensor_);
|
||||
LOG_SENSOR(" ", "Security Level", this->security_level_sensor_);
|
||||
LOG_SENSOR(" ", "Last Finger ID", this->last_finger_id_sensor_);
|
||||
LOG_SENSOR(" ", "Last Confidence", this->last_confidence_sensor_);
|
||||
}
|
||||
|
||||
} // namespace fingerprint_grow
|
||||
} // namespace esphome
|
||||
276
esphome/components/fingerprint_grow/fingerprint_grow.h
Normal file
276
esphome/components/fingerprint_grow/fingerprint_grow.h
Normal file
@@ -0,0 +1,276 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace fingerprint_grow {
|
||||
|
||||
static const uint16_t START_CODE = 0xEF01;
|
||||
|
||||
enum GrowPacketType {
|
||||
COMMAND = 0x01,
|
||||
DATA = 0x02,
|
||||
ACK = 0x07,
|
||||
END_DATA = 0x08,
|
||||
};
|
||||
|
||||
enum GrowCommand {
|
||||
GET_IMAGE = 0x01,
|
||||
IMAGE_2_TZ = 0x02,
|
||||
SEARCH = 0x04,
|
||||
REG_MODEL = 0x05,
|
||||
STORE = 0x06,
|
||||
LOAD = 0x07,
|
||||
UPLOAD = 0x08,
|
||||
DELETE = 0x0C,
|
||||
EMPTY = 0x0D,
|
||||
READ_SYS_PARAM = 0x0F,
|
||||
SET_PASSWORD = 0x12,
|
||||
VERIFY_PASSWORD = 0x13,
|
||||
HI_SPEED_SEARCH = 0x1B,
|
||||
TEMPLATE_COUNT = 0x1D,
|
||||
AURA_CONFIG = 0x35,
|
||||
LED_ON = 0x50,
|
||||
LED_OFF = 0x51,
|
||||
};
|
||||
|
||||
enum GrowResponse {
|
||||
OK = 0x00,
|
||||
PACKET_RCV_ERR = 0x01,
|
||||
NO_FINGER = 0x02,
|
||||
IMAGE_FAIL = 0x03,
|
||||
IMAGE_MESS = 0x06,
|
||||
FEATURE_FAIL = 0x07,
|
||||
NO_MATCH = 0x08,
|
||||
NOT_FOUND = 0x09,
|
||||
ENROLL_MISMATCH = 0x0A,
|
||||
BAD_LOCATION = 0x0B,
|
||||
DB_RANGE_FAIL = 0x0C,
|
||||
UPLOAD_FEATURE_FAIL = 0x0D,
|
||||
PACKET_RESPONSE_FAIL = 0x0E,
|
||||
UPLOAD_FAIL = 0x0F,
|
||||
DELETE_FAIL = 0x10,
|
||||
DB_CLEAR_FAIL = 0x11,
|
||||
PASSWORD_FAIL = 0x13,
|
||||
INVALID_IMAGE = 0x15,
|
||||
FLASH_ERR = 0x18,
|
||||
INVALID_REG = 0x1A,
|
||||
BAD_PACKET = 0xFE,
|
||||
TIMEOUT = 0xFF,
|
||||
};
|
||||
|
||||
enum GrowAuraLEDState {
|
||||
BREATHING = 0x01,
|
||||
FLASHING = 0x02,
|
||||
ALWAYS_ON = 0x03,
|
||||
ALWAYS_OFF = 0x04,
|
||||
GRADUAL_ON = 0x05,
|
||||
GRADUAL_OFF = 0x06,
|
||||
};
|
||||
|
||||
enum GrowAuraLEDColor {
|
||||
RED = 0x01,
|
||||
BLUE = 0x02,
|
||||
PURPLE = 0x03,
|
||||
};
|
||||
|
||||
class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevice {
|
||||
public:
|
||||
void update() override;
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_address(uint32_t address) {
|
||||
this->address_[0] = (uint8_t)(address >> 24);
|
||||
this->address_[1] = (uint8_t)(address >> 16);
|
||||
this->address_[2] = (uint8_t)(address >> 8);
|
||||
this->address_[3] = (uint8_t)(address & 0xFF);
|
||||
}
|
||||
void set_sensing_pin(GPIOPin *sensing_pin) { this->sensing_pin_ = sensing_pin; }
|
||||
void set_password(uint32_t password) { this->password_ = password; }
|
||||
void set_new_password(uint32_t new_password) { this->new_password_ = &new_password; }
|
||||
void set_fingerprint_count_sensor(sensor::Sensor *fingerprint_count_sensor) {
|
||||
this->fingerprint_count_sensor_ = fingerprint_count_sensor;
|
||||
}
|
||||
void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; }
|
||||
void set_capacity_sensor(sensor::Sensor *capacity_sensor) { this->capacity_sensor_ = capacity_sensor; }
|
||||
void set_security_level_sensor(sensor::Sensor *security_level_sensor) {
|
||||
this->security_level_sensor_ = security_level_sensor;
|
||||
}
|
||||
void set_last_finger_id_sensor(sensor::Sensor *last_finger_id_sensor) {
|
||||
this->last_finger_id_sensor_ = last_finger_id_sensor;
|
||||
}
|
||||
void set_last_confidence_sensor(sensor::Sensor *last_confidence_sensor) {
|
||||
this->last_confidence_sensor_ = last_confidence_sensor;
|
||||
}
|
||||
void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) {
|
||||
this->enrolling_binary_sensor_ = enrolling_binary_sensor;
|
||||
}
|
||||
void add_on_finger_scan_matched_callback(std::function<void(uint16_t, uint16_t)> callback) {
|
||||
this->finger_scan_matched_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_finger_scan_unmatched_callback(std::function<void()> callback) {
|
||||
this->finger_scan_unmatched_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_enrollment_scan_callback(std::function<void(uint8_t, uint16_t)> callback) {
|
||||
this->enrollment_scan_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_enrollment_done_callback(std::function<void(uint16_t)> callback) {
|
||||
this->enrollment_done_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void add_on_enrollment_failed_callback(std::function<void(uint16_t)> callback) {
|
||||
this->enrollment_failed_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void enroll_fingerprint(uint16_t finger_id, uint8_t num_buffers);
|
||||
void finish_enrollment(uint8_t result);
|
||||
void delete_fingerprint(uint16_t finger_id);
|
||||
void delete_all_fingerprints();
|
||||
|
||||
void led_control(bool state);
|
||||
void aura_led_control(uint8_t state, uint8_t speed, uint8_t color, uint8_t count);
|
||||
|
||||
protected:
|
||||
void scan_and_match_();
|
||||
uint8_t scan_image_(uint8_t buffer);
|
||||
uint8_t save_fingerprint_();
|
||||
bool check_password_();
|
||||
bool set_password_();
|
||||
bool get_parameters_();
|
||||
void get_fingerprint_count_();
|
||||
uint8_t send_command_();
|
||||
|
||||
std::vector<uint8_t> data_ = {};
|
||||
uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF};
|
||||
uint16_t capacity_ = 64;
|
||||
uint32_t password_ = 0x0;
|
||||
uint32_t *new_password_{nullptr};
|
||||
GPIOPin *sensing_pin_{nullptr};
|
||||
uint8_t enrollment_image_ = 0;
|
||||
uint16_t enrollment_slot_ = 0;
|
||||
uint8_t enrollment_buffers_ = 5;
|
||||
bool waiting_removal_ = false;
|
||||
uint32_t last_aura_led_control_ = 0;
|
||||
uint16_t last_aura_led_duration_ = 0;
|
||||
sensor::Sensor *fingerprint_count_sensor_{nullptr};
|
||||
sensor::Sensor *status_sensor_{nullptr};
|
||||
sensor::Sensor *capacity_sensor_{nullptr};
|
||||
sensor::Sensor *security_level_sensor_{nullptr};
|
||||
sensor::Sensor *last_finger_id_sensor_{nullptr};
|
||||
sensor::Sensor *last_confidence_sensor_{nullptr};
|
||||
binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr};
|
||||
CallbackManager<void(uint16_t, uint16_t)> finger_scan_matched_callback_;
|
||||
CallbackManager<void()> finger_scan_unmatched_callback_;
|
||||
CallbackManager<void(uint8_t, uint16_t)> enrollment_scan_callback_;
|
||||
CallbackManager<void(uint16_t)> enrollment_done_callback_;
|
||||
CallbackManager<void(uint16_t)> enrollment_failed_callback_;
|
||||
};
|
||||
|
||||
class FingerScanMatchedTrigger : public Trigger<uint16_t, uint16_t> {
|
||||
public:
|
||||
explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) {
|
||||
parent->add_on_finger_scan_matched_callback(
|
||||
[this](uint16_t finger_id, uint16_t confidence) { this->trigger(finger_id, confidence); });
|
||||
}
|
||||
};
|
||||
|
||||
class FingerScanUnmatchedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit FingerScanUnmatchedTrigger(FingerprintGrowComponent *parent) {
|
||||
parent->add_on_finger_scan_unmatched_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class EnrollmentScanTrigger : public Trigger<uint8_t, uint16_t> {
|
||||
public:
|
||||
explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) {
|
||||
parent->add_on_enrollment_scan_callback(
|
||||
[this](uint8_t scan_num, uint16_t finger_id) { this->trigger(scan_num, finger_id); });
|
||||
}
|
||||
};
|
||||
|
||||
class EnrollmentDoneTrigger : public Trigger<uint16_t> {
|
||||
public:
|
||||
explicit EnrollmentDoneTrigger(FingerprintGrowComponent *parent) {
|
||||
parent->add_on_enrollment_done_callback([this](uint16_t finger_id) { this->trigger(finger_id); });
|
||||
}
|
||||
};
|
||||
|
||||
class EnrollmentFailedTrigger : public Trigger<uint16_t> {
|
||||
public:
|
||||
explicit EnrollmentFailedTrigger(FingerprintGrowComponent *parent) {
|
||||
parent->add_on_enrollment_failed_callback([this](uint16_t finger_id) { this->trigger(finger_id); });
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class EnrollmentAction : public Action<Ts...>, public Parented<FingerprintGrowComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, finger_id)
|
||||
TEMPLATABLE_VALUE(uint8_t, num_scans)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto finger_id = this->finger_id_.value(x...);
|
||||
auto num_scans = this->num_scans_.value(x...);
|
||||
if (num_scans) {
|
||||
this->parent_->enroll_fingerprint(finger_id, num_scans);
|
||||
} else {
|
||||
this->parent_->enroll_fingerprint(finger_id, 2);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
class CancelEnrollmentAction : public Action<Ts...>, public Parented<FingerprintGrowComponent> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->finish_enrollment(1); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class DeleteAction : public Action<Ts...>, public Parented<FingerprintGrowComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, finger_id)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto finger_id = this->finger_id_.value(x...);
|
||||
this->parent_->delete_fingerprint(finger_id);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class DeleteAllAction : public Action<Ts...>, public Parented<FingerprintGrowComponent> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->delete_all_fingerprints(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class LEDControlAction : public Action<Ts...>, public Parented<FingerprintGrowComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(bool, state)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto state = this->state_.value(x...);
|
||||
this->parent_->led_control(state);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class AuraLEDControlAction : public Action<Ts...>, public Parented<FingerprintGrowComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint8_t, state)
|
||||
TEMPLATABLE_VALUE(uint8_t, speed)
|
||||
TEMPLATABLE_VALUE(uint8_t, color)
|
||||
TEMPLATABLE_VALUE(uint8_t, count)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto state = this->state_.value(x...);
|
||||
auto speed = this->speed_.value(x...);
|
||||
auto color = this->color_.value(x...);
|
||||
auto count = this->count_.value(x...);
|
||||
|
||||
this->parent_->aura_led_control(state, speed, color, count);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace fingerprint_grow
|
||||
} // namespace esphome
|
||||
64
esphome/components/fingerprint_grow/sensor.py
Normal file
64
esphome/components/fingerprint_grow/sensor.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor
|
||||
from esphome.const import (
|
||||
CONF_CAPACITY,
|
||||
CONF_FINGERPRINT_COUNT,
|
||||
CONF_LAST_CONFIDENCE,
|
||||
CONF_LAST_FINGER_ID,
|
||||
CONF_SECURITY_LEVEL,
|
||||
CONF_STATUS,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
ICON_ACCOUNT,
|
||||
ICON_ACCOUNT_CHECK,
|
||||
ICON_DATABASE,
|
||||
ICON_EMPTY,
|
||||
ICON_FINGERPRINT,
|
||||
ICON_SECURITY,
|
||||
UNIT_EMPTY,
|
||||
)
|
||||
from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent
|
||||
|
||||
DEPENDENCIES = ["fingerprint_grow"]
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent),
|
||||
cv.Optional(CONF_FINGERPRINT_COUNT): sensor.sensor_schema(
|
||||
UNIT_EMPTY, ICON_FINGERPRINT, 0, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_STATUS): sensor.sensor_schema(
|
||||
UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_CAPACITY): sensor.sensor_schema(
|
||||
UNIT_EMPTY, ICON_DATABASE, 0, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_SECURITY_LEVEL): sensor.sensor_schema(
|
||||
UNIT_EMPTY, ICON_SECURITY, 0, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_LAST_FINGER_ID): sensor.sensor_schema(
|
||||
UNIT_EMPTY, ICON_ACCOUNT, 0, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_LAST_CONFIDENCE): sensor.sensor_schema(
|
||||
UNIT_EMPTY, ICON_ACCOUNT_CHECK, 0, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
hub = yield cg.get_variable(config[CONF_FINGERPRINT_GROW_ID])
|
||||
|
||||
for key in [
|
||||
CONF_FINGERPRINT_COUNT,
|
||||
CONF_STATUS,
|
||||
CONF_CAPACITY,
|
||||
CONF_SECURITY_LEVEL,
|
||||
CONF_LAST_FINGER_ID,
|
||||
CONF_LAST_CONFIDENCE,
|
||||
]:
|
||||
if key not in config:
|
||||
continue
|
||||
conf = config[key]
|
||||
sens = yield sensor.new_sensor(conf)
|
||||
cg.add(getattr(hub, f"set_{key}_sensor")(sens))
|
||||
@@ -72,7 +72,7 @@ def validate_truetype_file(value):
|
||||
|
||||
|
||||
DEFAULT_GLYPHS = (
|
||||
' !"%()+,-.:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
||||
' !"%()+,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
||||
)
|
||||
CONF_RAW_DATA_ID = "raw_data_id"
|
||||
|
||||
|
||||
@@ -41,33 +41,33 @@ const uint8_t FUJITSU_GENERAL_TEMPERATURE_NIBBLE = 16;
|
||||
|
||||
// Power on
|
||||
const uint8_t FUJITSU_GENERAL_POWER_ON_NIBBLE = 17;
|
||||
const uint8_t FUJITSU_GENERAL_POWER_ON = 0x01;
|
||||
const uint8_t FUJITSU_GENERAL_POWER_OFF = 0x00;
|
||||
const uint8_t FUJITSU_GENERAL_POWER_ON = 0x01;
|
||||
|
||||
// Mode
|
||||
const uint8_t FUJITSU_GENERAL_MODE_NIBBLE = 19;
|
||||
const uint8_t FUJITSU_GENERAL_MODE_AUTO = 0x00;
|
||||
const uint8_t FUJITSU_GENERAL_MODE_HEAT = 0x04;
|
||||
const uint8_t FUJITSU_GENERAL_MODE_COOL = 0x01;
|
||||
const uint8_t FUJITSU_GENERAL_MODE_DRY = 0x02;
|
||||
const uint8_t FUJITSU_GENERAL_MODE_FAN = 0x03;
|
||||
const uint8_t FUJITSU_GENERAL_MODE_HEAT = 0x04;
|
||||
// const uint8_t FUJITSU_GENERAL_MODE_10C = 0x0B;
|
||||
|
||||
// Swing
|
||||
const uint8_t FUJITSU_GENERAL_FAN_NIBBLE = 20;
|
||||
const uint8_t FUJITSU_GENERAL_SWING_NIBBLE = 20;
|
||||
const uint8_t FUJITSU_GENERAL_SWING_NONE = 0x00;
|
||||
const uint8_t FUJITSU_GENERAL_SWING_VERTICAL = 0x01;
|
||||
const uint8_t FUJITSU_GENERAL_SWING_HORIZONTAL = 0x02;
|
||||
const uint8_t FUJITSU_GENERAL_SWING_BOTH = 0x03;
|
||||
|
||||
// Fan
|
||||
const uint8_t FUJITSU_GENERAL_FAN_NIBBLE = 21;
|
||||
const uint8_t FUJITSU_GENERAL_FAN_AUTO = 0x00;
|
||||
const uint8_t FUJITSU_GENERAL_FAN_HIGH = 0x01;
|
||||
const uint8_t FUJITSU_GENERAL_FAN_MEDIUM = 0x02;
|
||||
const uint8_t FUJITSU_GENERAL_FAN_LOW = 0x03;
|
||||
const uint8_t FUJITSU_GENERAL_FAN_SILENT = 0x04;
|
||||
|
||||
// Fan speed
|
||||
const uint8_t FUJITSU_GENERAL_SWING_NIBBLE = 21;
|
||||
const uint8_t FUJITSU_GENERAL_SWING_NONE = 0x00;
|
||||
const uint8_t FUJITSU_GENERAL_SWING_VERTICAL = 0x01;
|
||||
const uint8_t FUJITSU_GENERAL_SWING_HORIZONTAL = 0x02;
|
||||
const uint8_t FUJITSU_GENERAL_SWING_BOTH = 0x03;
|
||||
|
||||
// TODO Outdoor Unit Low Noise
|
||||
// const uint8_t FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14 = 0xA0;
|
||||
// const uint8_t FUJITSU_GENERAL_STATE_BYTE14 = 0x20;
|
||||
|
||||
@@ -11,6 +11,42 @@ namespace fujitsu_general {
|
||||
const uint8_t FUJITSU_GENERAL_TEMP_MIN = 16; // Celsius // TODO 16 for heating, 18 for cooling, unsupported in ESPH
|
||||
const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius
|
||||
|
||||
// clang-format off
|
||||
/**
|
||||
* ```
|
||||
* turn
|
||||
* on temp mode fan swing
|
||||
* * | | | | | | *
|
||||
*
|
||||
* temperatures 1 1248 124 124 1
|
||||
* auto auto 18 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000100 00000000 00000000 00000000 00000000 00000000 00000100 11110001
|
||||
* auto auto 19 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10001100 00000000 00000000 00000000 00000000 00000000 00000100 11111110
|
||||
* auto auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00000000 00000000 00000000 00000000 00000000 00000100 11110011
|
||||
*
|
||||
* on flag:
|
||||
* on at 16 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000000 00100000 00000000 00000000 00000000 00000000 00000100 11010101
|
||||
* down to 16 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000000 00100000 00000000 00000000 00000000 00000000 00000100 00110101
|
||||
*
|
||||
* mode options:
|
||||
* auto auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00000000 00000000 00000000 00000000 00000000 00000100 11110011
|
||||
* cool auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 10000000 00000000 00000000 00000000 00000000 00000100 01110011
|
||||
* dry auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 01000000 00000000 00000000 00000000 00000000 00000100 10110011
|
||||
* fan (auto) (30) 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 11000000 00000000 00000000 00000000 00000000 00000100 00110011
|
||||
* heat auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00100000 00000000 00000000 00000000 00000000 00000100 11010011
|
||||
*
|
||||
* fan options:
|
||||
* heat 30 high 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00100000 10000000 00000000 00000000 00000000 00000100 01010011
|
||||
* heat 30 med 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 01000000 00000000 00000000 00000000 00000100 01010011
|
||||
* heat 30 low 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 11000000 00000000 00000000 00000000 00000100 10010011
|
||||
* heat 30 quiet 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011
|
||||
*
|
||||
* swing options:
|
||||
* heat 30 swing vert 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00101000 00000000 00000000 00000000 00000100 00011101
|
||||
* heat 30 noswing 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011
|
||||
* ```
|
||||
*/
|
||||
// clang-format on
|
||||
|
||||
class FujitsuGeneralClimate : public climate_ir::ClimateIR {
|
||||
public:
|
||||
FujitsuGeneralClimate()
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import uart
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.components import sensor
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_SPEED,
|
||||
CONF_COURSE,
|
||||
CONF_ALTITUDE,
|
||||
CONF_SATELLITES,
|
||||
UNIT_DEGREES,
|
||||
UNIT_KILOMETER_PER_HOUR,
|
||||
UNIT_METER,
|
||||
UNIT_EMPTY,
|
||||
ICON_EMPTY,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
AUTO_LOAD = ["sensor"]
|
||||
|
||||
CODEOWNERS = ["@coogle"]
|
||||
|
||||
gps_ns = cg.esphome_ns.namespace("gps")
|
||||
GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice)
|
||||
@@ -15,9 +33,27 @@ CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(GPS),
|
||||
cv.Optional(CONF_LATITUDE): sensor.sensor_schema(
|
||||
UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_LONGITUDE): sensor.sensor_schema(
|
||||
UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_SPEED): sensor.sensor_schema(
|
||||
UNIT_KILOMETER_PER_HOUR, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_COURSE): sensor.sensor_schema(
|
||||
UNIT_DEGREES, ICON_EMPTY, 2, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_ALTITUDE): sensor.sensor_schema(
|
||||
UNIT_METER, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
cv.Optional(CONF_SATELLITES): sensor.sensor_schema(
|
||||
UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(cv.polling_component_schema("20s"))
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
@@ -27,5 +63,29 @@ def to_code(config):
|
||||
yield cg.register_component(var, config)
|
||||
yield uart.register_uart_device(var, config)
|
||||
|
||||
if CONF_LATITUDE in config:
|
||||
sens = yield sensor.new_sensor(config[CONF_LATITUDE])
|
||||
cg.add(var.set_latitude_sensor(sens))
|
||||
|
||||
if CONF_LONGITUDE in config:
|
||||
sens = yield sensor.new_sensor(config[CONF_LONGITUDE])
|
||||
cg.add(var.set_longitude_sensor(sens))
|
||||
|
||||
if CONF_SPEED in config:
|
||||
sens = yield sensor.new_sensor(config[CONF_SPEED])
|
||||
cg.add(var.set_speed_sensor(sens))
|
||||
|
||||
if CONF_COURSE in config:
|
||||
sens = yield sensor.new_sensor(config[CONF_COURSE])
|
||||
cg.add(var.set_course_sensor(sens))
|
||||
|
||||
if CONF_ALTITUDE in config:
|
||||
sens = yield sensor.new_sensor(config[CONF_ALTITUDE])
|
||||
cg.add(var.set_altitude_sensor(sens))
|
||||
|
||||
if CONF_SATELLITES in config:
|
||||
sens = yield sensor.new_sensor(config[CONF_SATELLITES])
|
||||
cg.add(var.set_satellites_sensor(sens))
|
||||
|
||||
# https://platformio.org/lib/show/1655/TinyGPSPlus
|
||||
cg.add_library("1655", "1.0.2") # TinyGPSPlus, has name conflict
|
||||
|
||||
@@ -8,34 +8,57 @@ static const char *TAG = "gps";
|
||||
|
||||
TinyGPSPlus &GPSListener::get_tiny_gps() { return this->parent_->get_tiny_gps(); }
|
||||
|
||||
void GPS::update() {
|
||||
if (this->latitude_sensor_ != nullptr)
|
||||
this->latitude_sensor_->publish_state(this->latitude_);
|
||||
|
||||
if (this->longitude_sensor_ != nullptr)
|
||||
this->longitude_sensor_->publish_state(this->longitude_);
|
||||
|
||||
if (this->speed_sensor_ != nullptr)
|
||||
this->speed_sensor_->publish_state(this->speed_);
|
||||
|
||||
if (this->course_sensor_ != nullptr)
|
||||
this->course_sensor_->publish_state(this->course_);
|
||||
|
||||
if (this->altitude_sensor_ != nullptr)
|
||||
this->altitude_sensor_->publish_state(this->altitude_);
|
||||
|
||||
if (this->satellites_sensor_ != nullptr)
|
||||
this->satellites_sensor_->publish_state(this->satellites_);
|
||||
}
|
||||
|
||||
void GPS::loop() {
|
||||
while (this->available() && !this->has_time_) {
|
||||
if (this->tiny_gps_.encode(this->read())) {
|
||||
if (tiny_gps_.location.isUpdated()) {
|
||||
this->latitude_ = tiny_gps_.location.lat();
|
||||
this->longitude_ = tiny_gps_.location.lng();
|
||||
|
||||
ESP_LOGD(TAG, "Location:");
|
||||
ESP_LOGD(TAG, " Lat: %f", tiny_gps_.location.lat());
|
||||
ESP_LOGD(TAG, " Lon: %f", tiny_gps_.location.lng());
|
||||
ESP_LOGD(TAG, " Lat: %f", this->latitude_);
|
||||
ESP_LOGD(TAG, " Lon: %f", this->longitude_);
|
||||
}
|
||||
|
||||
if (tiny_gps_.speed.isUpdated()) {
|
||||
this->speed_ = tiny_gps_.speed.kmph();
|
||||
ESP_LOGD(TAG, "Speed:");
|
||||
ESP_LOGD(TAG, " %f km/h", tiny_gps_.speed.kmph());
|
||||
ESP_LOGD(TAG, " %f km/h", this->speed_);
|
||||
}
|
||||
if (tiny_gps_.course.isUpdated()) {
|
||||
this->course_ = tiny_gps_.course.deg();
|
||||
ESP_LOGD(TAG, "Course:");
|
||||
ESP_LOGD(TAG, " %f °", tiny_gps_.course.deg());
|
||||
ESP_LOGD(TAG, " %f °", this->course_);
|
||||
}
|
||||
if (tiny_gps_.altitude.isUpdated()) {
|
||||
this->altitude_ = tiny_gps_.altitude.meters();
|
||||
ESP_LOGD(TAG, "Altitude:");
|
||||
ESP_LOGD(TAG, " %f m", tiny_gps_.altitude.meters());
|
||||
ESP_LOGD(TAG, " %f m", this->altitude_);
|
||||
}
|
||||
if (tiny_gps_.satellites.isUpdated()) {
|
||||
this->satellites_ = tiny_gps_.satellites.value();
|
||||
ESP_LOGD(TAG, "Satellites:");
|
||||
ESP_LOGD(TAG, " %d", tiny_gps_.satellites.value());
|
||||
}
|
||||
if (tiny_gps_.satellites.isUpdated()) {
|
||||
ESP_LOGD(TAG, "HDOP:");
|
||||
ESP_LOGD(TAG, " %.2f", tiny_gps_.hdop.hdop());
|
||||
ESP_LOGD(TAG, " %d", this->satellites_);
|
||||
}
|
||||
|
||||
for (auto *listener : this->listeners_)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include <TinyGPS++.h>
|
||||
|
||||
namespace esphome {
|
||||
@@ -20,17 +21,41 @@ class GPSListener {
|
||||
GPS *parent_;
|
||||
};
|
||||
|
||||
class GPS : public Component, public uart::UARTDevice {
|
||||
class GPS : public PollingComponent, public uart::UARTDevice {
|
||||
public:
|
||||
void set_latitude_sensor(sensor::Sensor *latitude_sensor) { latitude_sensor_ = latitude_sensor; }
|
||||
void set_longitude_sensor(sensor::Sensor *longitude_sensor) { longitude_sensor_ = longitude_sensor; }
|
||||
void set_speed_sensor(sensor::Sensor *speed_sensor) { speed_sensor_ = speed_sensor; }
|
||||
void set_course_sensor(sensor::Sensor *course_sensor) { course_sensor_ = course_sensor; }
|
||||
void set_altitude_sensor(sensor::Sensor *altitude_sensor) { altitude_sensor_ = altitude_sensor; }
|
||||
void set_satellites_sensor(sensor::Sensor *satellites_sensor) { satellites_sensor_ = satellites_sensor; }
|
||||
|
||||
void register_listener(GPSListener *listener) {
|
||||
listener->parent_ = this;
|
||||
this->listeners_.push_back(listener);
|
||||
}
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
void loop() override;
|
||||
void update() override;
|
||||
|
||||
TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; }
|
||||
|
||||
protected:
|
||||
float latitude_ = -1;
|
||||
float longitude_ = -1;
|
||||
float speed_ = -1;
|
||||
float course_ = -1;
|
||||
float altitude_ = -1;
|
||||
int satellites_ = -1;
|
||||
|
||||
sensor::Sensor *latitude_sensor_{nullptr};
|
||||
sensor::Sensor *longitude_sensor_{nullptr};
|
||||
sensor::Sensor *speed_sensor_{nullptr};
|
||||
sensor::Sensor *course_sensor_{nullptr};
|
||||
sensor::Sensor *altitude_sensor_{nullptr};
|
||||
sensor::Sensor *satellites_sensor_{nullptr};
|
||||
|
||||
bool has_time_{false};
|
||||
TinyGPSPlus tiny_gps_;
|
||||
std::vector<GPSListener *> listeners_{};
|
||||
|
||||
@@ -14,7 +14,7 @@ from esphome.const import (
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE, Lambda
|
||||
from esphome.core_config import PLATFORMIO_ESP8266_LUT
|
||||
from esphome.core.config import PLATFORMIO_ESP8266_LUT
|
||||
|
||||
DEPENDENCIES = ["network"]
|
||||
AUTO_LOAD = ["json"]
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import pins
|
||||
from esphome.const import (
|
||||
CONF_CHANNEL,
|
||||
CONF_FREQUENCY,
|
||||
CONF_ID,
|
||||
CONF_SCAN,
|
||||
@@ -9,6 +10,7 @@ from esphome.const import (
|
||||
CONF_SDA,
|
||||
CONF_ADDRESS,
|
||||
CONF_I2C_ID,
|
||||
CONF_MULTIPLEXER,
|
||||
)
|
||||
from esphome.core import coroutine, coroutine_with_priority
|
||||
|
||||
@@ -16,6 +18,7 @@ CODEOWNERS = ["@esphome/core"]
|
||||
i2c_ns = cg.esphome_ns.namespace("i2c")
|
||||
I2CComponent = i2c_ns.class_("I2CComponent", cg.Component)
|
||||
I2CDevice = i2c_ns.class_("I2CDevice")
|
||||
I2CMultiplexer = i2c_ns.class_("I2CMultiplexer", I2CDevice)
|
||||
|
||||
MULTI_CONF = True
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
@@ -30,6 +33,13 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
I2CMULTIPLEXER_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(I2CMultiplexer),
|
||||
cv.Required(CONF_CHANNEL): cv.uint8_t,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(1.0)
|
||||
def to_code(config):
|
||||
@@ -53,6 +63,7 @@ def i2c_device_schema(default_address):
|
||||
"""
|
||||
schema = {
|
||||
cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CComponent),
|
||||
cv.Optional(CONF_MULTIPLEXER): I2CMULTIPLEXER_SCHEMA,
|
||||
}
|
||||
if default_address is None:
|
||||
schema[cv.Required(CONF_ADDRESS)] = cv.i2c_address
|
||||
@@ -72,3 +83,8 @@ def register_i2c_device(var, config):
|
||||
parent = yield cg.get_variable(config[CONF_I2C_ID])
|
||||
cg.add(var.set_i2c_parent(parent))
|
||||
cg.add(var.set_i2c_address(config[CONF_ADDRESS]))
|
||||
if CONF_MULTIPLEXER in config:
|
||||
multiplexer = yield cg.get_variable(config[CONF_MULTIPLEXER][CONF_ID])
|
||||
cg.add(
|
||||
var.set_i2c_multiplexer(multiplexer, config[CONF_MULTIPLEXER][CONF_CHANNEL])
|
||||
)
|
||||
|
||||
@@ -178,28 +178,67 @@ bool I2CComponent::write_byte_16(uint8_t address, uint8_t a_register, uint16_t d
|
||||
}
|
||||
|
||||
void I2CDevice::set_i2c_address(uint8_t address) { this->address_ = address; }
|
||||
#ifdef USE_I2C_MULTIPLEXER
|
||||
void I2CDevice::set_i2c_multiplexer(I2CMultiplexer *multiplexer, uint8_t channel) {
|
||||
ESP_LOGVV(TAG, " Setting Multiplexer %p for channel %d", multiplexer, channel);
|
||||
this->multiplexer_ = multiplexer;
|
||||
this->channel_ = channel;
|
||||
}
|
||||
|
||||
void I2CDevice::check_multiplexer_() {
|
||||
if (this->multiplexer_ != nullptr) {
|
||||
ESP_LOGVV(TAG, "Multiplexer setting channel to %d", this->channel_);
|
||||
this->multiplexer_->set_channel(this->channel_);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool I2CDevice::read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) { // NOLINT
|
||||
#ifdef USE_I2C_MULTIPLEXER
|
||||
this->check_multiplexer_();
|
||||
#endif
|
||||
return this->parent_->read_bytes(this->address_, a_register, data, len, conversion);
|
||||
}
|
||||
bool I2CDevice::read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion) { // NOLINT
|
||||
#ifdef USE_I2C_MULTIPLEXER
|
||||
this->check_multiplexer_();
|
||||
#endif
|
||||
return this->parent_->read_byte(this->address_, a_register, data, conversion);
|
||||
}
|
||||
bool I2CDevice::write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) { // NOLINT
|
||||
#ifdef USE_I2C_MULTIPLEXER
|
||||
this->check_multiplexer_();
|
||||
#endif
|
||||
return this->parent_->write_bytes(this->address_, a_register, data, len);
|
||||
}
|
||||
bool I2CDevice::write_byte(uint8_t a_register, uint8_t data) { // NOLINT
|
||||
#ifdef USE_I2C_MULTIPLEXER
|
||||
this->check_multiplexer_();
|
||||
#endif
|
||||
return this->parent_->write_byte(this->address_, a_register, data);
|
||||
}
|
||||
bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion) { // NOLINT
|
||||
#ifdef USE_I2C_MULTIPLEXER
|
||||
this->check_multiplexer_();
|
||||
#endif
|
||||
return this->parent_->read_bytes_16(this->address_, a_register, data, len, conversion);
|
||||
}
|
||||
bool I2CDevice::read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion) { // NOLINT
|
||||
#ifdef USE_I2C_MULTIPLEXER
|
||||
this->check_multiplexer_();
|
||||
#endif
|
||||
return this->parent_->read_byte_16(this->address_, a_register, data, conversion);
|
||||
}
|
||||
bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) { // NOLINT
|
||||
#ifdef USE_I2C_MULTIPLEXER
|
||||
this->check_multiplexer_();
|
||||
#endif
|
||||
return this->parent_->write_bytes_16(this->address_, a_register, data, len);
|
||||
}
|
||||
bool I2CDevice::write_byte_16(uint8_t a_register, uint16_t data) { // NOLINT
|
||||
#ifdef USE_I2C_MULTIPLEXER
|
||||
this->check_multiplexer_();
|
||||
#endif
|
||||
return this->parent_->write_byte_16(this->address_, a_register, data);
|
||||
}
|
||||
void I2CDevice::set_i2c_parent(I2CComponent *parent) { this->parent_ = parent; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <Wire.h>
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
@@ -135,7 +136,7 @@ extern uint8_t next_i2c_bus_num_;
|
||||
#endif
|
||||
|
||||
class I2CDevice;
|
||||
|
||||
class I2CMultiplexer;
|
||||
class I2CRegister {
|
||||
public:
|
||||
I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {}
|
||||
@@ -167,7 +168,10 @@ class I2CDevice {
|
||||
|
||||
/// Manually set the i2c address of this device.
|
||||
void set_i2c_address(uint8_t address);
|
||||
|
||||
#ifdef USE_I2C_MULTIPLEXER
|
||||
/// Manually set the i2c multiplexer of this device.
|
||||
void set_i2c_multiplexer(I2CMultiplexer *multiplexer, uint8_t channel);
|
||||
#endif
|
||||
/// Manually set the parent i2c bus for this device.
|
||||
void set_i2c_parent(I2CComponent *parent);
|
||||
|
||||
@@ -280,9 +284,19 @@ class I2CDevice {
|
||||
bool write_byte_16(uint8_t a_register, uint16_t data);
|
||||
|
||||
protected:
|
||||
// Checks for multiplexer set and set channel
|
||||
void check_multiplexer_();
|
||||
uint8_t address_{0x00};
|
||||
I2CComponent *parent_{nullptr};
|
||||
#ifdef USE_I2C_MULTIPLEXER
|
||||
I2CMultiplexer *multiplexer_{nullptr};
|
||||
uint8_t channel_;
|
||||
#endif
|
||||
};
|
||||
class I2CMultiplexer : public I2CDevice {
|
||||
public:
|
||||
I2CMultiplexer() = default;
|
||||
virtual void set_channel(uint8_t channelno);
|
||||
};
|
||||
|
||||
} // namespace i2c
|
||||
} // namespace esphome
|
||||
|
||||
@@ -11,6 +11,40 @@ inline static float random_cubic_float() {
|
||||
return r * r * r;
|
||||
}
|
||||
|
||||
/// Pulse effect.
|
||||
class PulseLightEffect : public LightEffect {
|
||||
public:
|
||||
explicit PulseLightEffect(const std::string &name) : LightEffect(name) {}
|
||||
|
||||
void apply() override {
|
||||
const uint32_t now = millis();
|
||||
if (now - this->last_color_change_ < this->update_interval_) {
|
||||
return;
|
||||
}
|
||||
auto call = this->state_->turn_on();
|
||||
float out = this->on_ ? 1.0 : 0.0;
|
||||
call.set_brightness_if_supported(out);
|
||||
this->on_ = !this->on_;
|
||||
call.set_transition_length_if_supported(this->transition_length_);
|
||||
// don't tell HA every change
|
||||
call.set_publish(false);
|
||||
call.set_save(false);
|
||||
call.perform();
|
||||
|
||||
this->last_color_change_ = now;
|
||||
}
|
||||
|
||||
void set_transition_length(uint32_t transition_length) { this->transition_length_ = transition_length; }
|
||||
|
||||
void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
|
||||
|
||||
protected:
|
||||
bool on_ = false;
|
||||
uint32_t last_color_change_{0};
|
||||
uint32_t transition_length_{};
|
||||
uint32_t update_interval_{};
|
||||
};
|
||||
|
||||
/// Random effect. Sets random colors every 10 seconds and slowly transitions between them.
|
||||
class RandomLightEffect : public LightEffect {
|
||||
public:
|
||||
@@ -22,10 +56,14 @@ class RandomLightEffect : public LightEffect {
|
||||
return;
|
||||
}
|
||||
auto call = this->state_->turn_on();
|
||||
call.set_red_if_supported(random_float());
|
||||
call.set_green_if_supported(random_float());
|
||||
call.set_blue_if_supported(random_float());
|
||||
call.set_white_if_supported(random_float());
|
||||
if (this->state_->get_traits().get_supports_rgb()) {
|
||||
call.set_red_if_supported(random_float());
|
||||
call.set_green_if_supported(random_float());
|
||||
call.set_blue_if_supported(random_float());
|
||||
call.set_white_if_supported(random_float());
|
||||
} else {
|
||||
call.set_brightness_if_supported(random_float());
|
||||
}
|
||||
call.set_color_temperature_if_supported(random_float());
|
||||
call.set_transition_length_if_supported(this->transition_length_);
|
||||
call.set_publish(true);
|
||||
|
||||
@@ -26,6 +26,7 @@ from esphome.const import (
|
||||
from esphome.util import Registry
|
||||
from .types import (
|
||||
LambdaLightEffect,
|
||||
PulseLightEffect,
|
||||
RandomLightEffect,
|
||||
StrobeLightEffect,
|
||||
StrobeLightEffectColor,
|
||||
@@ -152,7 +153,27 @@ def automation_effect_to_code(config, effect_id):
|
||||
yield var
|
||||
|
||||
|
||||
@register_rgb_effect(
|
||||
@register_monochromatic_effect(
|
||||
"pulse",
|
||||
PulseLightEffect,
|
||||
"Pulse",
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_TRANSITION_LENGTH, default="1s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_UPDATE_INTERVAL, default="1s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
},
|
||||
)
|
||||
def pulse_effect_to_code(config, effect_id):
|
||||
effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
|
||||
cg.add(effect.set_transition_length(config[CONF_TRANSITION_LENGTH]))
|
||||
cg.add(effect.set_update_interval(config[CONF_UPDATE_INTERVAL]))
|
||||
yield effect
|
||||
|
||||
|
||||
@register_monochromatic_effect(
|
||||
"random",
|
||||
RandomLightEffect,
|
||||
"Random",
|
||||
|
||||
@@ -31,6 +31,7 @@ LightTurnOffTrigger = light_ns.class_(
|
||||
|
||||
# Effects
|
||||
LightEffect = light_ns.class_("LightEffect")
|
||||
PulseLightEffect = light_ns.class_("PulseLightEffect", LightEffect)
|
||||
RandomLightEffect = light_ns.class_("RandomLightEffect", LightEffect)
|
||||
LambdaLightEffect = light_ns.class_("LambdaLightEffect", LightEffect)
|
||||
AutomationLightEffect = light_ns.class_("AutomationLightEffect", LightEffect)
|
||||
|
||||
@@ -25,9 +25,17 @@ void MQTTClientComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up MQTT...");
|
||||
this->mqtt_client_.onMessage([this](char *topic, char *payload, AsyncMqttClientMessageProperties properties,
|
||||
size_t len, size_t index, size_t total) {
|
||||
std::string payload_s(payload, len);
|
||||
std::string topic_s(topic);
|
||||
this->on_message(topic_s, payload_s);
|
||||
if (index == 0)
|
||||
this->payload_buffer_.reserve(total);
|
||||
|
||||
// append new payload, may contain incomplete MQTT message
|
||||
this->payload_buffer_.append(payload, len);
|
||||
|
||||
// MQTT fully received
|
||||
if (len + index == total) {
|
||||
this->on_message(topic, this->payload_buffer_);
|
||||
this->payload_buffer_.clear();
|
||||
}
|
||||
});
|
||||
this->mqtt_client_.onDisconnect([this](AsyncMqttClientDisconnectReason reason) {
|
||||
this->state_ = MQTT_CLIENT_DISCONNECTED;
|
||||
@@ -347,6 +355,26 @@ void MQTTClientComponent::subscribe_json(const std::string &topic, mqtt_json_cal
|
||||
this->subscriptions_.push_back(subscription);
|
||||
}
|
||||
|
||||
void MQTTClientComponent::unsubscribe(const std::string &topic) {
|
||||
uint16_t ret = this->mqtt_client_.unsubscribe(topic.c_str());
|
||||
yield();
|
||||
if (ret != 0) {
|
||||
ESP_LOGV(TAG, "unsubscribe(topic='%s')", topic.c_str());
|
||||
} else {
|
||||
delay(5);
|
||||
ESP_LOGV(TAG, "Unsubscribe failed for topic='%s'.", topic.c_str());
|
||||
this->status_momentary_warning("unsubscribe", 1000);
|
||||
}
|
||||
|
||||
auto it = subscriptions_.begin();
|
||||
while (it != subscriptions_.end()) {
|
||||
if (it->topic == topic)
|
||||
it = subscriptions_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// Publish
|
||||
bool MQTTClientComponent::publish(const std::string &topic, const std::string &payload, uint8_t qos, bool retain) {
|
||||
return this->publish(topic, payload.data(), payload.size(), qos, retain);
|
||||
|
||||
@@ -159,6 +159,15 @@ class MQTTClientComponent : public Component {
|
||||
*/
|
||||
void subscribe_json(const std::string &topic, mqtt_json_callback_t callback, uint8_t qos = 0);
|
||||
|
||||
/** Unsubscribe from an MQTT topic.
|
||||
*
|
||||
* If multiple existing subscriptions to the same topic exist, all of them will be removed.
|
||||
*
|
||||
* @param topic The topic to unsubscribe from.
|
||||
* Must match the topic in the original subscribe or subscribe_json call exactly.
|
||||
*/
|
||||
void unsubscribe(const std::string &topic);
|
||||
|
||||
/** Publish a MQTTMessage
|
||||
*
|
||||
* @param message The message.
|
||||
@@ -250,6 +259,7 @@ class MQTTClientComponent : public Component {
|
||||
};
|
||||
std::string topic_prefix_{};
|
||||
MQTTMessage log_message_;
|
||||
std::string payload_buffer_;
|
||||
int log_level_{ESPHOME_LOG_LEVEL};
|
||||
|
||||
std::vector<MQTTSubscription> subscriptions_;
|
||||
|
||||
@@ -68,7 +68,8 @@ class NeoPixelBusLightOutputBase : public light::AddressableLight {
|
||||
void add_leds(uint16_t count_pixels) { this->add_leds(new NeoPixelBus<T_COLOR_FEATURE, T_METHOD>(count_pixels)); }
|
||||
void add_leds(NeoPixelBus<T_COLOR_FEATURE, T_METHOD> *controller) {
|
||||
this->controller_ = controller;
|
||||
this->controller_->Begin();
|
||||
// controller gets initialised in setup() - avoid calling twice (crashes with RMT)
|
||||
// this->controller_->Begin();
|
||||
}
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
|
||||
@@ -1,2 +1,13 @@
|
||||
# Dummy package to allow components to depend on network
|
||||
import esphome.codegen as cg
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
|
||||
def add_mdns_library():
|
||||
cg.add_define("USE_MDNS")
|
||||
if CORE.is_esp32:
|
||||
cg.add_library("ESPmDNS", None)
|
||||
elif CORE.is_esp8266:
|
||||
cg.add_library("ESP8266mDNS", None)
|
||||
|
||||
@@ -7,22 +7,38 @@
|
||||
namespace esphome {
|
||||
namespace rc522 {
|
||||
|
||||
static const uint8_t WAIT_I_RQ = 0x30; // RxIRq and IdleIRq
|
||||
|
||||
static const char *TAG = "rc522";
|
||||
|
||||
static const uint8_t RESET_COUNT = 5;
|
||||
|
||||
void format_uid(char *buf, const uint8_t *uid, uint8_t uid_length) {
|
||||
std::string format_buffer(uint8_t *b, uint8_t len) {
|
||||
char buf[32];
|
||||
int offset = 0;
|
||||
for (uint8_t i = 0; i < uid_length; i++) {
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
const char *format = "%02X";
|
||||
if (i + 1 < uid_length)
|
||||
if (i + 1 < len)
|
||||
format = "%02X-";
|
||||
offset += sprintf(buf + offset, format, b[i]);
|
||||
}
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::string format_uid(std::vector<uint8_t> &uid) {
|
||||
char buf[32];
|
||||
int offset = 0;
|
||||
for (uint8_t i = 0; i < uid.size(); i++) {
|
||||
const char *format = "%02X";
|
||||
if (i + 1 < uid.size())
|
||||
format = "%02X-";
|
||||
offset += sprintf(buf + offset, format, uid[i]);
|
||||
}
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
void RC522::setup() {
|
||||
initialize_pending_ = true;
|
||||
state_ = STATE_SETUP;
|
||||
// Pull device out of power down / reset state.
|
||||
|
||||
// First set the resetPowerDownPin as digital input, to check the MFRC522 power down mode.
|
||||
@@ -48,7 +64,7 @@ void RC522::setup() {
|
||||
}
|
||||
|
||||
void RC522::initialize_() {
|
||||
// Per originall code, wait 50 ms
|
||||
// Per original code, wait 50 ms
|
||||
if (millis() - reset_timeout_ < 50)
|
||||
return;
|
||||
|
||||
@@ -75,9 +91,8 @@ void RC522::initialize_() {
|
||||
pcd_write_register(TX_ASK_REG, 0x40);
|
||||
pcd_write_register(MODE_REG, 0x3D); // Default 0x3F. Set the preset value for the CRC coprocessor for the CalcCRC
|
||||
// command to 0x6363 (ISO 14443-3 part 6.2.4)
|
||||
pcd_antenna_on_(); // Enable the antenna driver pins TX1 and TX2 (they were disabled by the reset)
|
||||
|
||||
initialize_pending_ = false;
|
||||
state_ = STATE_INIT;
|
||||
}
|
||||
|
||||
void RC522::dump_config() {
|
||||
@@ -99,76 +114,163 @@ void RC522::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
void RC522::update() {
|
||||
if (state_ == STATE_INIT) {
|
||||
pcd_antenna_on_();
|
||||
pcd_clear_register_bit_mask_(COLL_REG, 0x80); // ValuesAfterColl=1 => Bits received after collision are cleared.
|
||||
buffer_[0] = PICC_CMD_REQA;
|
||||
pcd_transceive_data_(1);
|
||||
state_ = STATE_PICC_REQUEST_A;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Communication takes longer than update interval: %d", state_);
|
||||
}
|
||||
}
|
||||
|
||||
void RC522::loop() {
|
||||
// First check reset is needed
|
||||
if (reset_count_ > 0) {
|
||||
pcd_reset_();
|
||||
return;
|
||||
}
|
||||
if (initialize_pending_) {
|
||||
if (state_ == STATE_SETUP) {
|
||||
initialize_();
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() - update_wait_ < this->update_interval_)
|
||||
return;
|
||||
StatusCode status = STATUS_ERROR; // For lint passing. TODO: refactor this
|
||||
if (awaiting_comm_) {
|
||||
if (state_ == STATE_SELECT_SERIAL_DONE)
|
||||
status = await_crc_();
|
||||
else
|
||||
status = await_transceive_();
|
||||
|
||||
auto status = picc_is_new_card_present_();
|
||||
|
||||
static StatusCode LAST_STATUS = StatusCode::STATUS_OK;
|
||||
|
||||
if (status != LAST_STATUS) {
|
||||
ESP_LOGD(TAG, "Status is now: %d", status);
|
||||
LAST_STATUS = status;
|
||||
}
|
||||
|
||||
if (status == STATUS_ERROR) // No card
|
||||
{
|
||||
// ESP_LOGE(TAG, "Error");
|
||||
// mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (status != STATUS_OK) // We can receive STATUS_TIMEOUT when no card, or unexpected status.
|
||||
return;
|
||||
|
||||
// Try process card
|
||||
if (!picc_read_card_serial_()) {
|
||||
ESP_LOGW(TAG, "Requesting tag read failed!");
|
||||
return;
|
||||
};
|
||||
|
||||
if (uid_.size < 4) {
|
||||
return;
|
||||
ESP_LOGW(TAG, "Read serial size: %d", uid_.size);
|
||||
}
|
||||
|
||||
update_wait_ = millis();
|
||||
|
||||
bool report = true;
|
||||
// 1. Go through all triggers
|
||||
for (auto *trigger : this->triggers_)
|
||||
trigger->process(uid_.uiduint8_t, uid_.size);
|
||||
|
||||
// 2. Find a binary sensor
|
||||
for (auto *tag : this->binary_sensors_) {
|
||||
if (tag->process(uid_.uiduint8_t, uid_.size)) {
|
||||
// 2.1 if found, do not dump
|
||||
report = false;
|
||||
if (status == STATUS_WAITING) {
|
||||
return;
|
||||
}
|
||||
awaiting_comm_ = false;
|
||||
ESP_LOGV(TAG, "finished communication status: %d, state: %d", status, state_);
|
||||
}
|
||||
|
||||
if (report) {
|
||||
char buf[32];
|
||||
format_uid(buf, uid_.uiduint8_t, uid_.size);
|
||||
ESP_LOGD(TAG, "Found new tag '%s'", buf);
|
||||
}
|
||||
}
|
||||
switch (state_) {
|
||||
case STATE_PICC_REQUEST_A: {
|
||||
if (status == STATUS_TIMEOUT) { // no tag present
|
||||
for (auto *obj : this->binary_sensors_)
|
||||
obj->on_scan_end(); // reset the binary sensors
|
||||
ESP_LOGV(TAG, "CMD_REQA -> TIMEOUT (no tag present) %d", status);
|
||||
state_ = STATE_DONE;
|
||||
} else if (status != STATUS_OK) {
|
||||
ESP_LOGW(TAG, "CMD_REQA -> Not OK %d", status);
|
||||
state_ = STATE_DONE;
|
||||
} else if (back_length_ != 2) { // || *valid_bits_ != 0) { // ATQA must be exactly 16 bits.
|
||||
ESP_LOGW(TAG, "CMD_REQA -> OK, but unexpacted back_length_ of %d", back_length_);
|
||||
state_ = STATE_DONE;
|
||||
} else {
|
||||
state_ = STATE_READ_SERIAL;
|
||||
}
|
||||
if (state_ == STATE_DONE) {
|
||||
// Don't wait another loop cycle
|
||||
pcd_antenna_off_();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case STATE_READ_SERIAL: {
|
||||
ESP_LOGV(TAG, "STATE_READ_SERIAL (%d)", status);
|
||||
switch (uid_idx_) {
|
||||
case 0:
|
||||
buffer_[0] = PICC_CMD_SEL_CL1;
|
||||
break;
|
||||
case 3:
|
||||
buffer_[0] = PICC_CMD_SEL_CL2;
|
||||
break;
|
||||
case 6:
|
||||
buffer_[0] = PICC_CMD_SEL_CL3;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "uid_idx_ invalid, uid_idx_ = %d", uid_idx_);
|
||||
state_ = STATE_DONE;
|
||||
}
|
||||
buffer_[1] = 32;
|
||||
pcd_transceive_data_(2);
|
||||
state_ = STATE_SELECT_SERIAL;
|
||||
break;
|
||||
}
|
||||
case STATE_SELECT_SERIAL: {
|
||||
buffer_[1] = 0x70; // select
|
||||
// todo: set CRC
|
||||
buffer_[6] = buffer_[2] ^ buffer_[3] ^ buffer_[4] ^ buffer_[5];
|
||||
pcd_calculate_crc_(buffer_, 7);
|
||||
state_ = STATE_SELECT_SERIAL_DONE;
|
||||
break;
|
||||
}
|
||||
case STATE_SELECT_SERIAL_DONE: {
|
||||
send_len_ = 6;
|
||||
pcd_transceive_data_(9);
|
||||
state_ = STATE_READ_SERIAL_DONE;
|
||||
break;
|
||||
}
|
||||
case STATE_READ_SERIAL_DONE: {
|
||||
if (status != STATUS_OK || back_length_ != 3) {
|
||||
if (status == STATUS_TIMEOUT)
|
||||
ESP_LOGV(TAG, "STATE_READ_SERIAL_DONE -> TIMEOUT (no tag present) %d", status);
|
||||
else
|
||||
ESP_LOGW(TAG, "Unexpected response. Read status is %d. Read bytes: %d (%s)", status, back_length_,
|
||||
format_buffer(buffer_, 9).c_str());
|
||||
|
||||
void RC522::update() {
|
||||
for (auto *obj : this->binary_sensors_)
|
||||
obj->on_scan_end();
|
||||
}
|
||||
state_ = STATE_DONE;
|
||||
uid_idx_ = 0;
|
||||
|
||||
pcd_antenna_off_();
|
||||
return;
|
||||
}
|
||||
|
||||
// copy the uid
|
||||
bool cascade = buffer_[2] == PICC_CMD_CT; // todo: should be determined based on select response (buffer[6])
|
||||
for (uint8_t i = 2 + cascade; i < 6; i++)
|
||||
uid_buffer_[uid_idx_++] = buffer_[i];
|
||||
ESP_LOGVV(TAG, "copied uid to idx %d last byte is 0x%x, cascade is %d", uid_idx_, uid_buffer_[uid_idx_ - 1],
|
||||
cascade);
|
||||
|
||||
if (cascade) { // there is more bytes in the UID
|
||||
state_ = STATE_READ_SERIAL;
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> rfid_uid(std::begin(uid_buffer_), std::begin(uid_buffer_) + uid_idx_);
|
||||
uid_idx_ = 0;
|
||||
// ESP_LOGD(TAG, "Processing '%s'", format_uid(rfid_uid).c_str());
|
||||
pcd_antenna_off_();
|
||||
state_ = STATE_INIT; // scan again on next update
|
||||
bool report = true;
|
||||
|
||||
for (auto *tag : this->binary_sensors_) {
|
||||
if (tag->process(rfid_uid)) {
|
||||
report = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->current_uid_ == rfid_uid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->current_uid_ = rfid_uid;
|
||||
|
||||
for (auto *trigger : this->triggers_)
|
||||
trigger->process(rfid_uid);
|
||||
|
||||
if (report) {
|
||||
ESP_LOGD(TAG, "Found new tag '%s'", format_uid(rfid_uid).c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
case STATE_DONE: {
|
||||
this->current_uid_ = {};
|
||||
state_ = STATE_INIT;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} // namespace rc522
|
||||
|
||||
/**
|
||||
* Performs a soft reset on the MFRC522 chip and waits for it to be ready again.
|
||||
@@ -176,14 +278,14 @@ void RC522::update() {
|
||||
void RC522::pcd_reset_() {
|
||||
// The datasheet does not mention how long the SoftRest command takes to complete.
|
||||
// But the MFRC522 might have been in soft power-down mode (triggered by bit 4 of CommandReg)
|
||||
// Section 8.8.2 in the datasheet says the oscillator start-up time is the start up time of the crystal + 37,74μs. Let
|
||||
// us be generous: 50ms.
|
||||
// Section 8.8.2 in the datasheet says the oscillator start-up time is the start up time of the crystal + 37,74μs.
|
||||
// Let us be generous: 50ms.
|
||||
|
||||
if (millis() - reset_timeout_ < 50)
|
||||
return;
|
||||
|
||||
if (reset_count_ == RESET_COUNT) {
|
||||
ESP_LOGV(TAG, "Soft reset...");
|
||||
ESP_LOGI(TAG, "Soft reset...");
|
||||
// Issue the SoftReset command.
|
||||
pcd_write_register(COMMAND_REG, PCD_SOFT_RESET);
|
||||
}
|
||||
@@ -199,6 +301,7 @@ void RC522::pcd_reset_() {
|
||||
|
||||
if (--reset_count_ == 0) {
|
||||
ESP_LOGE(TAG, "Unable to reset RC522.");
|
||||
this->error_code_ = RESET_FAILED;
|
||||
mark_failed();
|
||||
}
|
||||
}
|
||||
@@ -215,49 +318,13 @@ void RC522::pcd_antenna_on_() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmits a REQuest command, Type A. Invites PICCs in state IDLE to go to READY and prepare for anticollision or
|
||||
* selection. 7 bit frame. Beware: When two PICCs are in the field at the same time I often get STATUS_TIMEOUT -
|
||||
* probably due do bad antenna design.
|
||||
*
|
||||
* @return STATUS_OK on success, STATUS_??? otherwise.
|
||||
* Turns the antenna off by disabling pins TX1 and TX2.
|
||||
*/
|
||||
RC522::StatusCode RC522::picc_request_a_(
|
||||
uint8_t *buffer_atqa, ///< The buffer to store the ATQA (Answer to request) in
|
||||
uint8_t *buffer_size ///< Buffer size, at least two uint8_ts. Also number of uint8_ts returned if STATUS_OK.
|
||||
) {
|
||||
return picc_reqa_or_wupa_(PICC_CMD_REQA, buffer_atqa, buffer_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmits REQA or WUPA commands.
|
||||
* Beware: When two PICCs are in the field at the same time I often get STATUS_TIMEOUT - probably due do bad antenna
|
||||
* design.
|
||||
*
|
||||
* @return STATUS_OK on success, STATUS_??? otherwise.
|
||||
*/
|
||||
RC522::StatusCode RC522::picc_reqa_or_wupa_(
|
||||
uint8_t command, ///< The command to send - PICC_CMD_REQA or PICC_CMD_WUPA
|
||||
uint8_t *buffer_atqa, ///< The buffer to store the ATQA (Answer to request) in
|
||||
uint8_t *buffer_size ///< Buffer size, at least two uint8_ts. Also number of uint8_ts returned if STATUS_OK.
|
||||
) {
|
||||
uint8_t valid_bits;
|
||||
RC522::StatusCode status;
|
||||
|
||||
if (buffer_atqa == nullptr || *buffer_size < 2) { // The ATQA response is 2 uint8_ts long.
|
||||
return STATUS_NO_ROOM;
|
||||
void RC522::pcd_antenna_off_() {
|
||||
uint8_t value = pcd_read_register(TX_CONTROL_REG);
|
||||
if ((value & 0x03) != 0x00) {
|
||||
pcd_write_register(TX_CONTROL_REG, value & ~0x03);
|
||||
}
|
||||
pcd_clear_register_bit_mask_(COLL_REG, 0x80); // ValuesAfterColl=1 => Bits received after collision are cleared.
|
||||
valid_bits = 7; // For REQA and WUPA we need the short frame format - transmit only 7 bits of the last (and only)
|
||||
// uint8_t. TxLastBits = BitFramingReg[2..0]
|
||||
status = pcd_transceive_data_(&command, 1, buffer_atqa, buffer_size, &valid_bits);
|
||||
if (status != STATUS_OK)
|
||||
return status;
|
||||
if (*buffer_size != 2 || valid_bits != 0) { // ATQA must be exactly 16 bits.
|
||||
ESP_LOGVV(TAG, "picc_reqa_or_wupa_() -> STATUS_ERROR");
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
|
||||
return STATUS_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,140 +347,86 @@ void RC522::pcd_clear_register_bit_mask_(PcdRegister reg, ///< The register to
|
||||
pcd_write_register(reg, tmp & (~mask)); // clear bit mask
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the Transceive command.
|
||||
* CRC validation can only be done if backData and backLen are specified.
|
||||
*
|
||||
* @return STATUS_OK on success, STATUS_??? otherwise.
|
||||
*/
|
||||
RC522::StatusCode RC522::pcd_transceive_data_(
|
||||
uint8_t *send_data, ///< Pointer to the data to transfer to the FIFO.
|
||||
uint8_t send_len, ///< Number of uint8_ts to transfer to the FIFO.
|
||||
uint8_t *back_data, ///< nullptr or pointer to buffer if data should be read back after executing the command.
|
||||
uint8_t *back_len, ///< In: Max number of uint8_ts to write to *backData. Out: The number of uint8_ts returned.
|
||||
uint8_t
|
||||
*valid_bits, ///< In/Out: The number of valid bits in the last uint8_t. 0 for 8 valid bits. Default nullptr.
|
||||
uint8_t rx_align, ///< In: Defines the bit position in backData[0] for the first bit received. Default 0.
|
||||
bool check_crc ///< In: True => The last two uint8_ts of the response is assumed to be a CRC_A that must be
|
||||
///< validated.
|
||||
) {
|
||||
uint8_t wait_i_rq = 0x30; // RxIRq and IdleIRq
|
||||
auto ret = pcd_communicate_with_picc_(PCD_TRANSCEIVE, wait_i_rq, send_data, send_len, back_data, back_len, valid_bits,
|
||||
rx_align, check_crc);
|
||||
|
||||
if (ret == STATUS_OK && *back_len == 5)
|
||||
ESP_LOGVV(TAG, "pcd_transceive_data_(..., %d, ) -> %d [%x, %x, %x, %x, %x]", send_len, ret, back_data[0],
|
||||
back_data[1], back_data[2], back_data[3], back_data[4]);
|
||||
else
|
||||
ESP_LOGVV(TAG, "pcd_transceive_data_(..., %d, ... ) -> %d", send_len, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers data to the MFRC522 FIFO, executes a command, waits for completion and transfers data back from the FIFO.
|
||||
* CRC validation can only be done if backData and backLen are specified.
|
||||
*
|
||||
* @return STATUS_OK on success, STATUS_??? otherwise.
|
||||
*/
|
||||
RC522::StatusCode RC522::pcd_communicate_with_picc_(
|
||||
uint8_t command, ///< The command to execute. One of the PCD_Command enums.
|
||||
uint8_t wait_i_rq, ///< The bits in the ComIrqReg register that signals successful completion of the command.
|
||||
uint8_t *send_data, ///< Pointer to the data to transfer to the FIFO.
|
||||
uint8_t send_len, ///< Number of uint8_ts to transfer to the FIFO.
|
||||
uint8_t *back_data, ///< nullptr or pointer to buffer if data should be read back after executing the command.
|
||||
uint8_t *back_len, ///< In: Max number of uint8_ts to write to *backData. Out: The number of uint8_ts returned.
|
||||
uint8_t *valid_bits, ///< In/Out: The number of valid bits in the last uint8_t. 0 for 8 valid bits.
|
||||
uint8_t rx_align, ///< In: Defines the bit position in backData[0] for the first bit received. Default 0.
|
||||
bool check_crc ///< In: True => The last two uint8_ts of the response is assumed to be a CRC_A that must be
|
||||
///< validated.
|
||||
) {
|
||||
ESP_LOGVV(TAG, "pcd_communicate_with_picc_(%d, %d,... %d)", command, wait_i_rq, check_crc);
|
||||
|
||||
void RC522::pcd_transceive_data_(uint8_t send_len) {
|
||||
ESP_LOGV(TAG, "PCD TRANSCEIVE: RX: %s", format_buffer(buffer_, send_len).c_str());
|
||||
delayMicroseconds(1000); // we need 1 ms delay between antenna on and those communication commands
|
||||
send_len_ = send_len;
|
||||
// Prepare values for BitFramingReg
|
||||
uint8_t tx_last_bits = valid_bits ? *valid_bits : 0;
|
||||
uint8_t bit_framing =
|
||||
(rx_align << 4) + tx_last_bits; // RxAlign = BitFramingReg[6..4]. TxLastBits = BitFramingReg[2..0]
|
||||
// For REQA and WUPA we need the short frame format - transmit only 7 bits of the last (and only)
|
||||
// uint8_t. TxLastBits = BitFramingReg[2..0]
|
||||
uint8_t bit_framing = (buffer_[0] == PICC_CMD_REQA) ? 7 : 0;
|
||||
|
||||
pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop any active command.
|
||||
pcd_write_register(COM_IRQ_REG, 0x7F); // Clear all seven interrupt request bits
|
||||
pcd_write_register(FIFO_LEVEL_REG, 0x80); // FlushBuffer = 1, FIFO initialization
|
||||
pcd_write_register(FIFO_DATA_REG, send_len, send_data); // Write sendData to the FIFO
|
||||
pcd_write_register(BIT_FRAMING_REG, bit_framing); // Bit adjustments
|
||||
pcd_write_register(COMMAND_REG, command); // Execute the command
|
||||
if (command == PCD_TRANSCEIVE) {
|
||||
pcd_set_register_bit_mask_(BIT_FRAMING_REG, 0x80); // StartSend=1, transmission of data starts
|
||||
}
|
||||
pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop any active command.
|
||||
pcd_write_register(COM_IRQ_REG, 0x7F); // Clear all seven interrupt request bits
|
||||
pcd_write_register(FIFO_LEVEL_REG, 0x80); // FlushBuffer = 1, FIFO initialization
|
||||
pcd_write_register(FIFO_DATA_REG, send_len_, buffer_); // Write sendData to the FIFO
|
||||
pcd_write_register(BIT_FRAMING_REG, bit_framing); // Bit adjustments
|
||||
pcd_write_register(COMMAND_REG, PCD_TRANSCEIVE); // Execute the command
|
||||
pcd_set_register_bit_mask_(BIT_FRAMING_REG, 0x80); // StartSend=1, transmission of data starts
|
||||
awaiting_comm_ = true;
|
||||
awaiting_comm_time_ = millis();
|
||||
}
|
||||
|
||||
// Wait for the command to complete.
|
||||
// In PCD_Init() we set the TAuto flag in TModeReg. This means the timer automatically starts when the PCD stops
|
||||
// transmitting. Each iteration of the do-while-loop takes 17.86μs.
|
||||
// TODO check/modify for other architectures than Arduino Uno 16bit
|
||||
uint16_t i;
|
||||
for (i = 2000; i > 0; i--) {
|
||||
uint8_t n = pcd_read_register(
|
||||
COM_IRQ_REG); // ComIrqReg[7..0] bits are: Set1 TxIRq RxIRq IdleIRq HiAlertIRq LoAlertIRq ErrIRq TimerIRq
|
||||
if (n & wait_i_rq) { // One of the interrupts that signal success has been set.
|
||||
break;
|
||||
}
|
||||
if (n & 0x01) { // Timer interrupt - nothing received in 25ms
|
||||
return STATUS_TIMEOUT;
|
||||
}
|
||||
}
|
||||
// 35.7ms and nothing happend. Communication with the MFRC522 might be down.
|
||||
if (i == 0) {
|
||||
RC522::StatusCode RC522::await_transceive_() {
|
||||
if (millis() - awaiting_comm_time_ < 2) // wait at least 2 ms
|
||||
return STATUS_WAITING;
|
||||
uint8_t n = pcd_read_register(
|
||||
COM_IRQ_REG); // ComIrqReg[7..0] bits are: Set1 TxIRq RxIRq IdleIRq HiAlertIRq LoAlertIRq ErrIRq TimerIRq
|
||||
if (n & 0x01) { // Timer interrupt - nothing received in 25ms
|
||||
back_length_ = 0;
|
||||
error_counter_ = 0; // reset the error counter
|
||||
return STATUS_TIMEOUT;
|
||||
}
|
||||
if (!(n & WAIT_I_RQ)) { // None of the interrupts that signal success has been set.
|
||||
// Wait for the command to complete.
|
||||
if (millis() - awaiting_comm_time_ < 40)
|
||||
return STATUS_WAITING;
|
||||
back_length_ = 0;
|
||||
ESP_LOGW(TAG, "Communication with the MFRC522 might be down, reset in %d",
|
||||
10 - error_counter_); // todo: trigger reset?
|
||||
if (error_counter_++ > 10)
|
||||
setup();
|
||||
|
||||
return STATUS_TIMEOUT;
|
||||
}
|
||||
// Stop now if any errors except collisions were detected.
|
||||
uint8_t error_reg_value = pcd_read_register(
|
||||
ERROR_REG); // ErrorReg[7..0] bits are: WrErr TempErr reserved BufferOvfl CollErr CRCErr ParityErr ProtocolErr
|
||||
if (error_reg_value & 0x13) { // BufferOvfl ParityErr ProtocolErr
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
error_counter_ = 0; // reset the error counter
|
||||
|
||||
uint8_t valid_bits_local = 0;
|
||||
|
||||
// If the caller wants data back, get it from the MFRC522.
|
||||
if (back_data && back_len) {
|
||||
uint8_t n = pcd_read_register(FIFO_LEVEL_REG); // Number of uint8_ts in the FIFO
|
||||
if (n > *back_len) {
|
||||
return STATUS_NO_ROOM;
|
||||
}
|
||||
*back_len = n; // Number of uint8_ts returned
|
||||
pcd_read_register(FIFO_DATA_REG, n, back_data, rx_align); // Get received data from FIFO
|
||||
valid_bits_local =
|
||||
pcd_read_register(CONTROL_REG) & 0x07; // RxLastBits[2:0] indicates the number of valid bits in the last
|
||||
// received uint8_t. If this value is 000b, the whole uint8_t is valid.
|
||||
if (valid_bits) {
|
||||
*valid_bits = valid_bits_local;
|
||||
}
|
||||
}
|
||||
n = pcd_read_register(FIFO_LEVEL_REG); // Number of uint8_ts in the FIFO
|
||||
if (n > sizeof(buffer_))
|
||||
return STATUS_NO_ROOM;
|
||||
if (n > sizeof(buffer_) - send_len_)
|
||||
send_len_ = sizeof(buffer_) - n; // simply overwrite the sent values
|
||||
back_length_ = n; // Number of uint8_ts returned
|
||||
pcd_read_register(FIFO_DATA_REG, n, buffer_ + send_len_, rx_align_); // Get received data from FIFO
|
||||
uint8_t valid_bits_local =
|
||||
pcd_read_register(CONTROL_REG) & 0x07; // RxLastBits[2:0] indicates the number of valid bits in the last
|
||||
// received uint8_t. If this value is 000b, the whole uint8_t is valid.
|
||||
|
||||
// Tell about collisions
|
||||
if (error_reg_value & 0x08) { // CollErr
|
||||
ESP_LOGW(TAG, "collision error, received %d bytes + %d bits (but anticollision not implemented)",
|
||||
back_length_ - (valid_bits_local > 0), valid_bits_local);
|
||||
return STATUS_COLLISION;
|
||||
}
|
||||
|
||||
// Perform CRC_A validation if requested.
|
||||
if (back_data && back_len && check_crc) {
|
||||
// In this case a MIFARE Classic NAK is not OK.
|
||||
if (*back_len == 1 && valid_bits_local == 4) {
|
||||
return STATUS_MIFARE_NACK;
|
||||
}
|
||||
// We need at least the CRC_A value and all 8 bits of the last uint8_t must be received.
|
||||
if (*back_len < 2 || valid_bits_local != 0) {
|
||||
return STATUS_CRC_WRONG;
|
||||
}
|
||||
// Verify CRC_A - do our own calculation and store the control in controlBuffer.
|
||||
uint8_t control_buffer[2];
|
||||
RC522::StatusCode status = pcd_calculate_crc_(&back_data[0], *back_len - 2, &control_buffer[0]);
|
||||
if (status != STATUS_OK) {
|
||||
return status;
|
||||
}
|
||||
if ((back_data[*back_len - 2] != control_buffer[0]) || (back_data[*back_len - 1] != control_buffer[1])) {
|
||||
return STATUS_CRC_WRONG;
|
||||
}
|
||||
// Tell about collisions
|
||||
if (valid_bits_local) {
|
||||
ESP_LOGW(TAG, "only %d valid bits received, tag distance to high? Error code is 0x%x", valid_bits_local,
|
||||
error_reg_value); // TODO: is this always due to collissions?
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
ESP_LOGV(TAG, "received %d bytes: %s", back_length_, format_buffer(buffer_ + send_len_, back_length_).c_str());
|
||||
|
||||
return STATUS_OK;
|
||||
}
|
||||
@@ -424,10 +437,8 @@ RC522::StatusCode RC522::pcd_communicate_with_picc_(
|
||||
* @return STATUS_OK on success, STATUS_??? otherwise.
|
||||
*/
|
||||
|
||||
RC522::StatusCode RC522::pcd_calculate_crc_(
|
||||
uint8_t *data, ///< In: Pointer to the data to transfer to the FIFO for CRC calculation.
|
||||
uint8_t length, ///< In: The number of uint8_ts to transfer.
|
||||
uint8_t *result ///< Out: Pointer to result buffer. Result is written to result[0..1], low uint8_t first.
|
||||
void RC522::pcd_calculate_crc_(uint8_t *data, ///< In: Pointer to the data to transfer to the FIFO for CRC calculation.
|
||||
uint8_t length ///< In: The number of uint8_ts to transfer.
|
||||
) {
|
||||
ESP_LOGVV(TAG, "pcd_calculate_crc_(..., %d, ...)", length);
|
||||
pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop any active command.
|
||||
@@ -436,323 +447,50 @@ RC522::StatusCode RC522::pcd_calculate_crc_(
|
||||
pcd_write_register(FIFO_DATA_REG, length, data); // Write data to the FIFO
|
||||
pcd_write_register(COMMAND_REG, PCD_CALC_CRC); // Start the calculation
|
||||
|
||||
// Wait for the CRC calculation to complete. Each iteration of the while-loop takes 17.73μs.
|
||||
// TODO check/modify for other architectures than Arduino Uno 16bit
|
||||
awaiting_comm_ = true;
|
||||
awaiting_comm_time_ = millis();
|
||||
}
|
||||
|
||||
// Wait for the CRC calculation to complete. Each iteration of the while-loop takes 17.73us.
|
||||
for (uint16_t i = 5000; i > 0; i--) {
|
||||
// DivIrqReg[7..0] bits are: Set2 reserved reserved MfinActIRq reserved CRCIRq reserved reserved
|
||||
uint8_t n = pcd_read_register(DIV_IRQ_REG);
|
||||
if (n & 0x04) { // CRCIRq bit set - calculation done
|
||||
pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop calculating CRC for new content in the FIFO.
|
||||
// Transfer the result from the registers to the result buffer
|
||||
result[0] = pcd_read_register(CRC_RESULT_REG_L);
|
||||
result[1] = pcd_read_register(CRC_RESULT_REG_H);
|
||||
RC522::StatusCode RC522::await_crc_() {
|
||||
if (millis() - awaiting_comm_time_ < 2) // wait at least 2 ms
|
||||
return STATUS_WAITING;
|
||||
|
||||
ESP_LOGVV(TAG, "pcd_calculate_crc_() STATUS_OK");
|
||||
return STATUS_OK;
|
||||
}
|
||||
// DivIrqReg[7..0] bits are: Set2 reserved reserved MfinActIRq reserved CRCIRq reserved reserved
|
||||
uint8_t n = pcd_read_register(DIV_IRQ_REG);
|
||||
if (n & 0x04) { // CRCIRq bit set - calculation done
|
||||
pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop calculating CRC for new content in the FIFO.
|
||||
// Transfer the result from the registers to the result buffer
|
||||
buffer_[7] = pcd_read_register(CRC_RESULT_REG_L);
|
||||
buffer_[8] = pcd_read_register(CRC_RESULT_REG_H);
|
||||
|
||||
ESP_LOGVV(TAG, "pcd_calculate_crc_() STATUS_OK");
|
||||
return STATUS_OK;
|
||||
}
|
||||
ESP_LOGVV(TAG, "pcd_calculate_crc_() TIMEOUT");
|
||||
if (millis() - awaiting_comm_time_ < 89)
|
||||
return STATUS_WAITING;
|
||||
|
||||
ESP_LOGD(TAG, "pcd_calculate_crc_() TIMEOUT");
|
||||
// 89ms passed and nothing happend. Communication with the MFRC522 might be down.
|
||||
return STATUS_TIMEOUT;
|
||||
}
|
||||
/**
|
||||
* Returns STATUS_OK if a PICC responds to PICC_CMD_REQA.
|
||||
* Only "new" cards in state IDLE are invited. Sleeping cards in state HALT are ignored.
|
||||
*
|
||||
* @return STATUS_OK on success, STATUS_??? otherwise.
|
||||
*/
|
||||
|
||||
RC522::StatusCode RC522::picc_is_new_card_present_() {
|
||||
uint8_t buffer_atqa[2];
|
||||
uint8_t buffer_size = sizeof(buffer_atqa);
|
||||
|
||||
// Reset baud rates
|
||||
pcd_write_register(TX_MODE_REG, 0x00);
|
||||
pcd_write_register(RX_MODE_REG, 0x00);
|
||||
// Reset ModWidthReg
|
||||
pcd_write_register(MOD_WIDTH_REG, 0x26);
|
||||
|
||||
auto result = picc_request_a_(buffer_atqa, &buffer_size);
|
||||
|
||||
ESP_LOGV(TAG, "picc_is_new_card_present_() -> %d", result);
|
||||
bool RC522BinarySensor::process(std::vector<uint8_t> &data) {
|
||||
bool result = true;
|
||||
if (data.size() != this->uid_.size())
|
||||
result = false;
|
||||
else {
|
||||
for (uint8_t i = 0; i < data.size(); i++) {
|
||||
if (data[i] != this->uid_[i]) {
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->publish_state(result);
|
||||
this->found_ = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple wrapper around PICC_Select.
|
||||
* Returns true if a UID could be read.
|
||||
* Remember to call PICC_IsNewCardPresent(), PICC_RequestA() or PICC_WakeupA() first.
|
||||
* The read UID is available in the class variable uid.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
bool RC522::picc_read_card_serial_() {
|
||||
RC522::StatusCode result = picc_select_(&this->uid_);
|
||||
ESP_LOGVV(TAG, "picc_select_(...) -> %d", result);
|
||||
return (result == STATUS_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmits SELECT/ANTICOLLISION commands to select a single PICC.
|
||||
* Before calling this function the PICCs must be placed in the READY(*) state by calling PICC_RequestA() or
|
||||
* PICC_WakeupA(). On success:
|
||||
* - The chosen PICC is in state ACTIVE(*) and all other PICCs have returned to state IDLE/HALT. (Figure 7 of the
|
||||
* ISO/IEC 14443-3 draft.)
|
||||
* - The UID size and value of the chosen PICC is returned in *uid along with the SAK.
|
||||
*
|
||||
* A PICC UID consists of 4, 7 or 10 uint8_ts.
|
||||
* Only 4 uint8_ts can be specified in a SELECT command, so for the longer UIDs two or three iterations are used:
|
||||
* UID size Number of UID uint8_ts Cascade levels Example of PICC
|
||||
* ======== =================== ============== ===============
|
||||
* single 4 1 MIFARE Classic
|
||||
* double 7 2 MIFARE Ultralight
|
||||
* triple 10 3 Not currently in use?
|
||||
*
|
||||
* @return STATUS_OK on success, STATUS_??? otherwise.
|
||||
*/
|
||||
RC522::StatusCode RC522::picc_select_(
|
||||
Uid *uid, ///< Pointer to Uid struct. Normally output, but can also be used to supply a known UID.
|
||||
uint8_t valid_bits ///< The number of known UID bits supplied in *uid. Normally 0. If set you must also supply
|
||||
///< uid->size.
|
||||
) {
|
||||
bool uid_complete;
|
||||
bool select_done;
|
||||
bool use_cascade_tag;
|
||||
uint8_t cascade_level = 1;
|
||||
RC522::StatusCode result;
|
||||
uint8_t count;
|
||||
uint8_t check_bit;
|
||||
uint8_t index;
|
||||
uint8_t uid_index; // The first index in uid->uiduint8_t[] that is used in the current Cascade Level.
|
||||
int8_t current_level_known_bits; // The number of known UID bits in the current Cascade Level.
|
||||
uint8_t buffer[9]; // The SELECT/ANTICOLLISION commands uses a 7 uint8_t standard frame + 2 uint8_ts CRC_A
|
||||
uint8_t buffer_used; // The number of uint8_ts used in the buffer, ie the number of uint8_ts to transfer to the FIFO.
|
||||
uint8_t rx_align; // Used in BitFramingReg. Defines the bit position for the first bit received.
|
||||
uint8_t tx_last_bits; // Used in BitFramingReg. The number of valid bits in the last transmitted uint8_t.
|
||||
uint8_t *response_buffer;
|
||||
uint8_t response_length;
|
||||
|
||||
// Description of buffer structure:
|
||||
// uint8_t 0: SEL Indicates the Cascade Level: PICC_CMD_SEL_CL1, PICC_CMD_SEL_CL2 or PICC_CMD_SEL_CL3
|
||||
// uint8_t 1: NVB Number of Valid Bits (in complete command, not just the UID): High nibble: complete
|
||||
// uint8_ts,
|
||||
// Low nibble: Extra bits. uint8_t 2: UID-data or CT See explanation below. CT means Cascade Tag. uint8_t
|
||||
// 3: UID-data uint8_t 4: UID-data uint8_t 5: UID-data uint8_t 6: BCC Block Check Character - XOR of
|
||||
// uint8_ts 2-5 uint8_t 7: CRC_A uint8_t 8: CRC_A The BCC and CRC_A are only transmitted if we know all the UID bits
|
||||
// of the current Cascade Level.
|
||||
//
|
||||
// Description of uint8_ts 2-5: (Section 6.5.4 of the ISO/IEC 14443-3 draft: UID contents and cascade levels)
|
||||
// UID size Cascade level uint8_t2 uint8_t3 uint8_t4 uint8_t5
|
||||
// ======== ============= ===== ===== ===== =====
|
||||
// 4 uint8_ts 1 uid0 uid1 uid2 uid3
|
||||
// 7 uint8_ts 1 CT uid0 uid1 uid2
|
||||
// 2 uid3 uid4 uid5 uid6
|
||||
// 10 uint8_ts 1 CT uid0 uid1 uid2
|
||||
// 2 CT uid3 uid4 uid5
|
||||
// 3 uid6 uid7 uid8 uid9
|
||||
|
||||
// Sanity checks
|
||||
if (valid_bits > 80) {
|
||||
return STATUS_INVALID;
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, "picc_select_(&, %d)", valid_bits);
|
||||
|
||||
// Prepare MFRC522
|
||||
pcd_clear_register_bit_mask_(COLL_REG, 0x80); // ValuesAfterColl=1 => Bits received after collision are cleared.
|
||||
|
||||
// Repeat Cascade Level loop until we have a complete UID.
|
||||
uid_complete = false;
|
||||
while (!uid_complete) {
|
||||
// Set the Cascade Level in the SEL uint8_t, find out if we need to use the Cascade Tag in uint8_t 2.
|
||||
switch (cascade_level) {
|
||||
case 1:
|
||||
buffer[0] = PICC_CMD_SEL_CL1;
|
||||
uid_index = 0;
|
||||
use_cascade_tag = valid_bits && uid->size > 4; // When we know that the UID has more than 4 uint8_ts
|
||||
break;
|
||||
|
||||
case 2:
|
||||
buffer[0] = PICC_CMD_SEL_CL2;
|
||||
uid_index = 3;
|
||||
use_cascade_tag = valid_bits && uid->size > 7; // When we know that the UID has more than 7 uint8_ts
|
||||
break;
|
||||
|
||||
case 3:
|
||||
buffer[0] = PICC_CMD_SEL_CL3;
|
||||
uid_index = 6;
|
||||
use_cascade_tag = false; // Never used in CL3.
|
||||
break;
|
||||
|
||||
default:
|
||||
return STATUS_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
// How many UID bits are known in this Cascade Level?
|
||||
current_level_known_bits = valid_bits - (8 * uid_index);
|
||||
if (current_level_known_bits < 0) {
|
||||
current_level_known_bits = 0;
|
||||
}
|
||||
// Copy the known bits from uid->uiduint8_t[] to buffer[]
|
||||
index = 2; // destination index in buffer[]
|
||||
if (use_cascade_tag) {
|
||||
buffer[index++] = PICC_CMD_CT;
|
||||
}
|
||||
uint8_t uint8_ts_to_copy = current_level_known_bits / 8 +
|
||||
(current_level_known_bits % 8
|
||||
? 1
|
||||
: 0); // The number of uint8_ts needed to represent the known bits for this level.
|
||||
if (uint8_ts_to_copy) {
|
||||
uint8_t maxuint8_ts =
|
||||
use_cascade_tag ? 3 : 4; // Max 4 uint8_ts in each Cascade Level. Only 3 left if we use the Cascade Tag
|
||||
if (uint8_ts_to_copy > maxuint8_ts) {
|
||||
uint8_ts_to_copy = maxuint8_ts;
|
||||
}
|
||||
for (count = 0; count < uint8_ts_to_copy; count++) {
|
||||
buffer[index++] = uid->uiduint8_t[uid_index + count];
|
||||
}
|
||||
}
|
||||
// Now that the data has been copied we need to include the 8 bits in CT in currentLevelKnownBits
|
||||
if (use_cascade_tag) {
|
||||
current_level_known_bits += 8;
|
||||
}
|
||||
|
||||
// Repeat anti collision loop until we can transmit all UID bits + BCC and receive a SAK - max 32 iterations.
|
||||
select_done = false;
|
||||
while (!select_done) {
|
||||
// Find out how many bits and uint8_ts to send and receive.
|
||||
if (current_level_known_bits >= 32) { // All UID bits in this Cascade Level are known. This is a SELECT.
|
||||
|
||||
if (response_length < 4) {
|
||||
ESP_LOGW(TAG, "Not enough data received.");
|
||||
return STATUS_INVALID;
|
||||
}
|
||||
|
||||
// Serial.print(F("SELECT: currentLevelKnownBits=")); Serial.println(currentLevelKnownBits, DEC);
|
||||
buffer[1] = 0x70; // NVB - Number of Valid Bits: Seven whole uint8_ts
|
||||
// Calculate BCC - Block Check Character
|
||||
buffer[6] = buffer[2] ^ buffer[3] ^ buffer[4] ^ buffer[5];
|
||||
// Calculate CRC_A
|
||||
result = pcd_calculate_crc_(buffer, 7, &buffer[7]);
|
||||
if (result != STATUS_OK) {
|
||||
return result;
|
||||
}
|
||||
tx_last_bits = 0; // 0 => All 8 bits are valid.
|
||||
buffer_used = 9;
|
||||
// Store response in the last 3 uint8_ts of buffer (BCC and CRC_A - not needed after tx)
|
||||
response_buffer = &buffer[6];
|
||||
response_length = 3;
|
||||
} else { // This is an ANTICOLLISION.
|
||||
// Serial.print(F("ANTICOLLISION: currentLevelKnownBits=")); Serial.println(currentLevelKnownBits, DEC);
|
||||
tx_last_bits = current_level_known_bits % 8;
|
||||
count = current_level_known_bits / 8; // Number of whole uint8_ts in the UID part.
|
||||
index = 2 + count; // Number of whole uint8_ts: SEL + NVB + UIDs
|
||||
buffer[1] = (index << 4) + tx_last_bits; // NVB - Number of Valid Bits
|
||||
buffer_used = index + (tx_last_bits ? 1 : 0);
|
||||
// Store response in the unused part of buffer
|
||||
response_buffer = &buffer[index];
|
||||
response_length = sizeof(buffer) - index;
|
||||
}
|
||||
|
||||
// Set bit adjustments
|
||||
rx_align = tx_last_bits; // Having a separate variable is overkill. But it makes the next line easier to read.
|
||||
pcd_write_register(
|
||||
BIT_FRAMING_REG,
|
||||
(rx_align << 4) + tx_last_bits); // RxAlign = BitFramingReg[6..4]. TxLastBits = BitFramingReg[2..0]
|
||||
|
||||
// Transmit the buffer and receive the response.
|
||||
result = pcd_transceive_data_(buffer, buffer_used, response_buffer, &response_length, &tx_last_bits, rx_align);
|
||||
if (result == STATUS_COLLISION) { // More than one PICC in the field => collision.
|
||||
uint8_t value_of_coll_reg = pcd_read_register(
|
||||
COLL_REG); // CollReg[7..0] bits are: ValuesAfterColl reserved CollPosNotValid CollPos[4:0]
|
||||
if (value_of_coll_reg & 0x20) { // CollPosNotValid
|
||||
return STATUS_COLLISION; // Without a valid collision position we cannot continue
|
||||
}
|
||||
uint8_t collision_pos = value_of_coll_reg & 0x1F; // Values 0-31, 0 means bit 32.
|
||||
if (collision_pos == 0) {
|
||||
collision_pos = 32;
|
||||
}
|
||||
if (collision_pos <= current_level_known_bits) { // No progress - should not happen
|
||||
return STATUS_INTERNAL_ERROR;
|
||||
}
|
||||
// Choose the PICC with the bit set.
|
||||
current_level_known_bits = collision_pos;
|
||||
count = current_level_known_bits % 8; // The bit to modify
|
||||
check_bit = (current_level_known_bits - 1) % 8;
|
||||
index = 1 + (current_level_known_bits / 8) + (count ? 1 : 0); // First uint8_t is index 0.
|
||||
if (response_length > 2) // Note: Otherwise buffer[index] might be not initialized
|
||||
buffer[index] |= (1 << check_bit);
|
||||
} else if (result != STATUS_OK) {
|
||||
return result;
|
||||
} else { // STATUS_OK
|
||||
if (current_level_known_bits >= 32) { // This was a SELECT.
|
||||
select_done = true; // No more anticollision
|
||||
// We continue below outside the while.
|
||||
} else { // This was an ANTICOLLISION.
|
||||
// We now have all 32 bits of the UID in this Cascade Level
|
||||
current_level_known_bits = 32;
|
||||
// Run loop again to do the SELECT.
|
||||
}
|
||||
}
|
||||
} // End of while (!selectDone)
|
||||
|
||||
// We do not check the CBB - it was constructed by us above.
|
||||
|
||||
// Copy the found UID uint8_ts from buffer[] to uid->uiduint8_t[]
|
||||
index = (buffer[2] == PICC_CMD_CT) ? 3 : 2; // source index in buffer[]
|
||||
uint8_ts_to_copy = (buffer[2] == PICC_CMD_CT) ? 3 : 4;
|
||||
for (count = 0; count < uint8_ts_to_copy; count++) {
|
||||
uid->uiduint8_t[uid_index + count] = buffer[index++];
|
||||
}
|
||||
|
||||
// Check response SAK (Select Acknowledge)
|
||||
if (response_length != 3 || tx_last_bits != 0) { // SAK must be exactly 24 bits (1 uint8_t + CRC_A).
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
// Verify CRC_A - do our own calculation and store the control in buffer[2..3] - those uint8_ts are not needed
|
||||
// anymore.
|
||||
result = pcd_calculate_crc_(response_buffer, 1, &buffer[2]);
|
||||
if (result != STATUS_OK) {
|
||||
return result;
|
||||
}
|
||||
if ((buffer[2] != response_buffer[1]) || (buffer[3] != response_buffer[2])) {
|
||||
return STATUS_CRC_WRONG;
|
||||
}
|
||||
if (response_buffer[0] & 0x04) { // Cascade bit set - UID not complete yes
|
||||
cascade_level++;
|
||||
} else {
|
||||
uid_complete = true;
|
||||
uid->sak = response_buffer[0];
|
||||
}
|
||||
} // End of while (!uidComplete)
|
||||
|
||||
// Set correct uid->size
|
||||
uid->size = 3 * cascade_level + 1;
|
||||
|
||||
return STATUS_OK;
|
||||
}
|
||||
|
||||
bool RC522BinarySensor::process(const uint8_t *data, uint8_t len) {
|
||||
if (len != this->uid_.size())
|
||||
return false;
|
||||
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
if (data[i] != this->uid_[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
this->publish_state(true);
|
||||
this->found_ = true;
|
||||
return true;
|
||||
}
|
||||
void RC522Trigger::process(const uint8_t *uid, uint8_t uid_length) {
|
||||
char buf[32];
|
||||
format_uid(buf, uid, uid_length);
|
||||
this->trigger(std::string(buf));
|
||||
}
|
||||
void RC522Trigger::process(std::vector<uint8_t> &data) { this->trigger(format_uid(data)); }
|
||||
|
||||
} // namespace rc522
|
||||
} // namespace esphome
|
||||
|
||||
@@ -26,6 +26,33 @@ class RC522 : public PollingComponent {
|
||||
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
|
||||
|
||||
protected:
|
||||
// Return codes from the functions in this class. Remember to update GetStatusCodeName() if you add more.
|
||||
// last value set to 0xff, then compiler uses less ram, it seems some optimisations are triggered
|
||||
enum StatusCode : uint8_t {
|
||||
STATUS_OK, // Success
|
||||
STATUS_WAITING, // Waiting result from RC522 chip
|
||||
STATUS_ERROR, // Error in communication
|
||||
STATUS_COLLISION, // Collission detected
|
||||
STATUS_TIMEOUT, // Timeout in communication.
|
||||
STATUS_NO_ROOM, // A buffer is not big enough.
|
||||
STATUS_INTERNAL_ERROR, // Internal error in the code. Should not happen ;-)
|
||||
STATUS_INVALID, // Invalid argument.
|
||||
STATUS_CRC_WRONG, // The CRC_A does not match
|
||||
STATUS_MIFARE_NACK = 0xff // A MIFARE PICC responded with NAK.
|
||||
};
|
||||
|
||||
enum State {
|
||||
STATE_NONE = 0,
|
||||
STATE_SETUP,
|
||||
STATE_INIT,
|
||||
STATE_PICC_REQUEST_A,
|
||||
STATE_READ_SERIAL,
|
||||
STATE_SELECT_SERIAL,
|
||||
STATE_SELECT_SERIAL_DONE,
|
||||
STATE_READ_SERIAL_DONE,
|
||||
STATE_DONE,
|
||||
} state_{STATE_NONE};
|
||||
|
||||
enum PcdRegister : uint8_t {
|
||||
// Page 0: Command and status
|
||||
// 0x00 // reserved for future use
|
||||
@@ -150,33 +177,11 @@ class RC522 : public PollingComponent {
|
||||
PICC_CMD_UL_WRITE = 0xA2 // Writes one 4 uint8_t page to the PICC.
|
||||
};
|
||||
|
||||
// Return codes from the functions in this class. Remember to update GetStatusCodeName() if you add more.
|
||||
// last value set to 0xff, then compiler uses less ram, it seems some optimisations are triggered
|
||||
enum StatusCode : uint8_t {
|
||||
STATUS_OK, // Success
|
||||
STATUS_ERROR, // Error in communication
|
||||
STATUS_COLLISION, // Collission detected
|
||||
STATUS_TIMEOUT, // Timeout in communication.
|
||||
STATUS_NO_ROOM, // A buffer is not big enough.
|
||||
STATUS_INTERNAL_ERROR, // Internal error in the code. Should not happen ;-)
|
||||
STATUS_INVALID, // Invalid argument.
|
||||
STATUS_CRC_WRONG, // The CRC_A does not match
|
||||
STATUS_MIFARE_NACK = 0xff // A MIFARE PICC responded with NAK.
|
||||
};
|
||||
|
||||
// A struct used for passing the UID of a PICC.
|
||||
using Uid = struct {
|
||||
uint8_t size; // Number of uint8_ts in the UID. 4, 7 or 10.
|
||||
uint8_t uiduint8_t[10];
|
||||
uint8_t sak; // The SAK (Select acknowledge) uint8_t returned from the PICC after successful selection.
|
||||
};
|
||||
|
||||
Uid uid_;
|
||||
uint32_t update_wait_{0};
|
||||
|
||||
void pcd_reset_();
|
||||
void initialize_();
|
||||
void pcd_antenna_on_();
|
||||
void pcd_antenna_off_();
|
||||
|
||||
virtual uint8_t pcd_read_register(PcdRegister reg ///< The register to read from. One of the PCD_Register enums.
|
||||
) = 0;
|
||||
|
||||
@@ -202,15 +207,6 @@ class RC522 : public PollingComponent {
|
||||
uint8_t *values ///< The values to write. uint8_t array.
|
||||
) = 0;
|
||||
|
||||
StatusCode picc_request_a_(
|
||||
uint8_t *buffer_atqa, ///< The buffer to store the ATQA (Answer to request) in
|
||||
uint8_t *buffer_size ///< Buffer size, at least two uint8_ts. Also number of uint8_ts returned if STATUS_OK.
|
||||
);
|
||||
StatusCode picc_reqa_or_wupa_(
|
||||
uint8_t command, ///< The command to send - PICC_CMD_REQA or PICC_CMD_WUPA
|
||||
uint8_t *buffer_atqa, ///< The buffer to store the ATQA (Answer to request) in
|
||||
uint8_t *buffer_size ///< Buffer size, at least two uint8_ts. Also number of uint8_ts returned if STATUS_OK.
|
||||
);
|
||||
void pcd_set_register_bit_mask_(PcdRegister reg, ///< The register to update. One of the PCD_Register enums.
|
||||
uint8_t mask ///< The bits to set.
|
||||
);
|
||||
@@ -218,38 +214,33 @@ class RC522 : public PollingComponent {
|
||||
uint8_t mask ///< The bits to clear.
|
||||
);
|
||||
|
||||
StatusCode pcd_transceive_data_(uint8_t *send_data, uint8_t send_len, uint8_t *back_data, uint8_t *back_len,
|
||||
uint8_t *valid_bits = nullptr, uint8_t rx_align = 0, bool check_crc = false);
|
||||
StatusCode pcd_communicate_with_picc_(uint8_t command, uint8_t wait_i_rq, uint8_t *send_data, uint8_t send_len,
|
||||
uint8_t *back_data = nullptr, uint8_t *back_len = nullptr,
|
||||
uint8_t *valid_bits = nullptr, uint8_t rx_align = 0, bool check_crc = false);
|
||||
StatusCode pcd_calculate_crc_(
|
||||
uint8_t *data, ///< In: Pointer to the data to transfer to the FIFO for CRC calculation.
|
||||
uint8_t length, ///< In: The number of uint8_ts to transfer.
|
||||
uint8_t *result ///< Out: Pointer to result buffer. Result is written to result[0..1], low uint8_t first.
|
||||
);
|
||||
RC522::StatusCode picc_is_new_card_present_();
|
||||
bool picc_read_card_serial_();
|
||||
StatusCode picc_select_(
|
||||
Uid *uid, ///< Pointer to Uid struct. Normally output, but can also be used to supply a known UID.
|
||||
uint8_t valid_bits = 0 ///< The number of known UID bits supplied in *uid. Normally 0. If set you must also
|
||||
///< supply uid->size.
|
||||
void pcd_transceive_data_(uint8_t send_len);
|
||||
|
||||
void pcd_calculate_crc_(uint8_t *data, ///< In: Pointer to the data to transfer to the FIFO for CRC calculation.
|
||||
uint8_t length ///< In: The number of uint8_ts to transfer.
|
||||
);
|
||||
|
||||
/** Read a data frame from the RC522 and return the result as a vector.
|
||||
*
|
||||
* Note that is_ready needs to be checked first before requesting this method.
|
||||
*
|
||||
* On failure, an empty vector is returned.
|
||||
*/
|
||||
std::vector<uint8_t> r_c522_read_data_();
|
||||
bool awaiting_comm_;
|
||||
uint32_t awaiting_comm_time_;
|
||||
StatusCode await_transceive_();
|
||||
StatusCode await_crc_();
|
||||
|
||||
uint8_t buffer_[9]; ///< buffer for communication, the first bits [0..back_idx-1] are for tx ,
|
||||
///< [back_idx..back_idx+back_len] for rx
|
||||
uint8_t send_len_; // index of first byte for RX
|
||||
uint8_t back_length_; ///< In: Max number of uint8_ts to write to *backData. Out: The number of uint8_ts returned.
|
||||
uint8_t uid_buffer_[10]; // buffer to construct the uid (for 7 and 10 bit uids)
|
||||
uint8_t uid_idx_ = 0; // number of read uid bytes e.g. index of the next available position in uid_buffer
|
||||
uint8_t error_counter_ = 0; // to reset if unresponsive
|
||||
uint8_t rx_align_;
|
||||
uint8_t *valid_bits_;
|
||||
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
uint8_t reset_count_{0};
|
||||
uint32_t reset_timeout_{0};
|
||||
bool initialize_pending_{false};
|
||||
std::vector<RC522BinarySensor *> binary_sensors_;
|
||||
std::vector<RC522Trigger *> triggers_;
|
||||
std::vector<uint8_t> current_uid_;
|
||||
|
||||
enum RC522Error {
|
||||
NONE = 0,
|
||||
@@ -261,7 +252,7 @@ class RC522BinarySensor : public binary_sensor::BinarySensor {
|
||||
public:
|
||||
void set_uid(const std::vector<uint8_t> &uid) { uid_ = uid; }
|
||||
|
||||
bool process(const uint8_t *data, uint8_t len);
|
||||
bool process(std::vector<uint8_t> &data);
|
||||
|
||||
void on_scan_end() {
|
||||
if (!this->found_) {
|
||||
@@ -277,7 +268,7 @@ class RC522BinarySensor : public binary_sensor::BinarySensor {
|
||||
|
||||
class RC522Trigger : public Trigger<std::string> {
|
||||
public:
|
||||
void process(const uint8_t *uid, uint8_t uid_length);
|
||||
void process(std::vector<uint8_t> &data);
|
||||
};
|
||||
|
||||
} // namespace rc522
|
||||
|
||||
@@ -36,10 +36,6 @@ void RC522I2C::pcd_read_register(PcdRegister reg, ///< The register to read fro
|
||||
return;
|
||||
}
|
||||
|
||||
std::string buf;
|
||||
buf = "Rx";
|
||||
char cstrb[20];
|
||||
|
||||
uint8_t b = values[0];
|
||||
read_bytes(reg >> 1, values, count);
|
||||
|
||||
@@ -69,31 +65,5 @@ void RC522I2C::pcd_write_register(PcdRegister reg, ///< The register to write t
|
||||
write_bytes(reg >> 1, values, count);
|
||||
}
|
||||
|
||||
// bool RC522I2C::write_data(const std::vector<uint8_t> &data) {
|
||||
// return this->write_bytes_raw(data.data(), data.size()); }
|
||||
|
||||
// bool RC522I2C::read_data(std::vector<uint8_t> &data, uint8_t len) {
|
||||
// delay(5);
|
||||
|
||||
// std::vector<uint8_t> ready;
|
||||
// ready.resize(1);
|
||||
// uint32_t start_time = millis();
|
||||
// while (true) {
|
||||
// if (this->read_bytes_raw(ready.data(), 1)) {
|
||||
// if (ready[0] == 0x01)
|
||||
// break;
|
||||
// }
|
||||
|
||||
// if (millis() - start_time > 100) {
|
||||
// ESP_LOGV(TAG, "Timed out waiting for readiness from RC522!");
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// data.resize(len + 1);
|
||||
// this->read_bytes_raw(data.data(), len + 1);
|
||||
// return true;
|
||||
// }
|
||||
|
||||
} // namespace rc522_i2c
|
||||
} // namespace esphome
|
||||
|
||||
@@ -32,7 +32,7 @@ uint8_t RC522Spi::pcd_read_register(PcdRegister reg ///< The register to read f
|
||||
transfer_byte(0x80 | reg);
|
||||
value = read_byte();
|
||||
disable();
|
||||
ESP_LOGV(TAG, "read_register_(%x) -> %x", reg, value);
|
||||
ESP_LOGVV(TAG, "read_register_(%d) -> %d", reg, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -45,9 +45,11 @@ void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read fro
|
||||
uint8_t *values, ///< uint8_t array to store the values in.
|
||||
uint8_t rx_align ///< Only bit positions rxAlign..7 in values[0] are updated.
|
||||
) {
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
std::string buf;
|
||||
buf = "Rx";
|
||||
char cstrb[20];
|
||||
#endif
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -68,25 +70,30 @@ void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read fro
|
||||
values[0] = (values[0] & ~mask) | (value & mask);
|
||||
index++;
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
sprintf(cstrb, " %x", values[0]);
|
||||
buf.append(cstrb);
|
||||
#endif
|
||||
}
|
||||
while (index < count) {
|
||||
values[index] = transfer_byte(address); // Read value and tell that we want to read the same address again.
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
sprintf(cstrb, " %x", values[index]);
|
||||
buf.append(cstrb);
|
||||
#endif
|
||||
|
||||
index++;
|
||||
}
|
||||
values[index] = transfer_byte(0); // Read the final uint8_t. Send 0 to stop reading.
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
buf = buf + " ";
|
||||
sprintf(cstrb, "%x", values[index]);
|
||||
buf.append(cstrb);
|
||||
|
||||
ESP_LOGVV(TAG, "read_register_array_(%x, %d, , %d) -> %s", reg, count, rx_align, buf.c_str());
|
||||
|
||||
#endif
|
||||
disable();
|
||||
}
|
||||
|
||||
@@ -108,21 +115,25 @@ void RC522Spi::pcd_write_register(PcdRegister reg, ///< The register to write t
|
||||
uint8_t count, ///< The number of uint8_ts to write to the register
|
||||
uint8_t *values ///< The values to write. uint8_t array.
|
||||
) {
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
std::string buf;
|
||||
buf = "Tx";
|
||||
char cstrb[20];
|
||||
#endif
|
||||
|
||||
enable();
|
||||
transfer_byte(reg);
|
||||
char cstrb[20];
|
||||
|
||||
for (uint8_t index = 0; index < count; index++) {
|
||||
transfer_byte(values[index]);
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
sprintf(cstrb, " %x", values[index]);
|
||||
buf.append(cstrb);
|
||||
#endif
|
||||
}
|
||||
disable();
|
||||
ESP_LOGVV(TAG, "write_register_(%x, %d) -> %s", reg, count, buf.c_str());
|
||||
ESP_LOGVV(TAG, "write_register_(%d, %d) -> %s", reg, count, buf.c_str());
|
||||
}
|
||||
|
||||
} // namespace rc522_spi
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import math
|
||||
from typing import Optional
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@@ -180,8 +181,12 @@ SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
def sensor_schema(unit_of_measurement_, icon_, accuracy_decimals_, device_class_):
|
||||
# type: (str, str, int, str) -> cv.Schema
|
||||
def sensor_schema(
|
||||
unit_of_measurement_: str,
|
||||
icon_: str,
|
||||
accuracy_decimals_: int,
|
||||
device_class_: Optional[str] = DEVICE_CLASS_EMPTY,
|
||||
) -> cv.Schema:
|
||||
schema = SENSOR_SCHEMA
|
||||
if unit_of_measurement_ != UNIT_EMPTY:
|
||||
schema = schema.extend(
|
||||
|
||||
@@ -148,10 +148,10 @@ void SlidingWindowMovingAverageFilter::set_window_size(size_t window_size) { thi
|
||||
optional<float> SlidingWindowMovingAverageFilter::new_value(float value) {
|
||||
if (!isnan(value)) {
|
||||
if (this->queue_.size() == this->window_size_) {
|
||||
this->sum_ -= this->queue_.front();
|
||||
this->queue_.pop();
|
||||
this->sum_ -= this->queue_[0];
|
||||
this->queue_.pop_front();
|
||||
}
|
||||
this->queue_.push(value);
|
||||
this->queue_.push_back(value);
|
||||
this->sum_ += value;
|
||||
}
|
||||
float average;
|
||||
@@ -161,8 +161,16 @@ optional<float> SlidingWindowMovingAverageFilter::new_value(float value) {
|
||||
average = this->sum_ / this->queue_.size();
|
||||
ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f) -> %f", this, value, average);
|
||||
|
||||
if (++this->send_at_ >= this->send_every_) {
|
||||
this->send_at_ = 0;
|
||||
if (++this->send_at_ % this->send_every_ == 0) {
|
||||
if (this->send_at_ >= 10000) {
|
||||
// Recalculate to prevent floating point error accumulating
|
||||
this->sum_ = 0;
|
||||
for (auto v : this->queue_)
|
||||
this->sum_ += v;
|
||||
average = this->sum_ / this->queue_.size();
|
||||
this->send_at_ = 0;
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f) SENDING", this, value);
|
||||
return average;
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ class SlidingWindowMovingAverageFilter : public Filter {
|
||||
|
||||
protected:
|
||||
float sum_{0.0};
|
||||
std::queue<float> queue_;
|
||||
std::deque<float> queue_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
size_t window_size_;
|
||||
|
||||
@@ -52,6 +52,8 @@ void Servo::loop() {
|
||||
|
||||
void Servo::write(float value) {
|
||||
value = clamp(value, -1.0f, 1.0f);
|
||||
if (this->target_value_ == value)
|
||||
this->internal_write(value);
|
||||
this->target_value_ = value;
|
||||
this->source_value_ = this->current_value_;
|
||||
this->state_ = STATE_ATTACHED;
|
||||
|
||||
0
esphome/components/sgp40/__init__.py
Normal file
0
esphome/components/sgp40/__init__.py
Normal file
629
esphome/components/sgp40/sensirion_voc_algorithm.cpp
Normal file
629
esphome/components/sgp40/sensirion_voc_algorithm.cpp
Normal file
@@ -0,0 +1,629 @@
|
||||
|
||||
#include "sensirion_voc_algorithm.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sgp40 {
|
||||
|
||||
/* The VOC code were originally created by
|
||||
* https://github.com/Sensirion/embedded-sgp
|
||||
* The fixed point arithmetic parts of this code were originally created by
|
||||
* https://github.com/PetteriAimonen/libfixmath
|
||||
*/
|
||||
|
||||
/*!< the maximum value of fix16_t */
|
||||
#define FIX16_MAXIMUM 0x7FFFFFFF
|
||||
/*!< the minimum value of fix16_t */
|
||||
static const uint32_t FIX16_MINIMUM = 0x80000000;
|
||||
/*!< the value used to indicate overflows when FIXMATH_NO_OVERFLOW is not
|
||||
* specified */
|
||||
static const uint32_t FIX16_OVERFLOW = 0x80000000;
|
||||
/*!< fix16_t value of 1 */
|
||||
const uint32_t FIX16_ONE = 0x00010000;
|
||||
|
||||
inline fix16_t fix16_from_int(int32_t a) { return a * FIX16_ONE; }
|
||||
|
||||
inline int32_t fix16_cast_to_int(fix16_t a) { return (a >> 16); }
|
||||
|
||||
/*! Multiplies the two given fix16_t's and returns the result. */
|
||||
static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1);
|
||||
|
||||
/*! Divides the first given fix16_t by the second and returns the result. */
|
||||
static fix16_t fix16_div(fix16_t a, fix16_t b);
|
||||
|
||||
/*! Returns the square root of the given fix16_t. */
|
||||
static fix16_t fix16_sqrt(fix16_t in_value);
|
||||
|
||||
/*! Returns the exponent (e^) of the given fix16_t. */
|
||||
static fix16_t fix16_exp(fix16_t in_value);
|
||||
|
||||
static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1) {
|
||||
// Each argument is divided to 16-bit parts.
|
||||
// AB
|
||||
// * CD
|
||||
// -----------
|
||||
// BD 16 * 16 -> 32 bit products
|
||||
// CB
|
||||
// AD
|
||||
// AC
|
||||
// |----| 64 bit product
|
||||
int32_t a = (in_arg0 >> 16), c = (in_arg1 >> 16);
|
||||
uint32_t b = (in_arg0 & 0xFFFF), d = (in_arg1 & 0xFFFF);
|
||||
|
||||
int32_t ac = a * c;
|
||||
int32_t ad_cb = a * d + c * b;
|
||||
uint32_t bd = b * d;
|
||||
|
||||
int32_t product_hi = ac + (ad_cb >> 16); // NOLINT
|
||||
|
||||
// Handle carry from lower 32 bits to upper part of result.
|
||||
uint32_t ad_cb_temp = ad_cb << 16; // NOLINT
|
||||
uint32_t product_lo = bd + ad_cb_temp;
|
||||
if (product_lo < bd)
|
||||
product_hi++;
|
||||
|
||||
#ifndef FIXMATH_NO_OVERFLOW
|
||||
// The upper 17 bits should all be the same (the sign).
|
||||
if (product_hi >> 31 != product_hi >> 15)
|
||||
return FIX16_OVERFLOW;
|
||||
#endif
|
||||
|
||||
#ifdef FIXMATH_NO_ROUNDING
|
||||
return (product_hi << 16) | (product_lo >> 16);
|
||||
#else
|
||||
// Subtracting 0x8000 (= 0.5) and then using signed right shift
|
||||
// achieves proper rounding to result-1, except in the corner
|
||||
// case of negative numbers and lowest word = 0x8000.
|
||||
// To handle that, we also have to subtract 1 for negative numbers.
|
||||
uint32_t product_lo_tmp = product_lo;
|
||||
product_lo -= 0x8000;
|
||||
product_lo -= (uint32_t) product_hi >> 31;
|
||||
if (product_lo > product_lo_tmp)
|
||||
product_hi--;
|
||||
|
||||
// Discard the lowest 16 bits. Note that this is not exactly the same
|
||||
// as dividing by 0x10000. For example if product = -1, result will
|
||||
// also be -1 and not 0. This is compensated by adding +1 to the result
|
||||
// and compensating this in turn in the rounding above.
|
||||
fix16_t result = (product_hi << 16) | (product_lo >> 16); // NOLINT
|
||||
result += 1;
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
static fix16_t fix16_div(fix16_t a, fix16_t b) {
|
||||
// This uses the basic binary restoring division algorithm.
|
||||
// It appears to be faster to do the whole division manually than
|
||||
// trying to compose a 64-bit divide out of 32-bit divisions on
|
||||
// platforms without hardware divide.
|
||||
|
||||
if (b == 0)
|
||||
return FIX16_MINIMUM;
|
||||
|
||||
uint32_t remainder = (a >= 0) ? a : (-a);
|
||||
uint32_t divider = (b >= 0) ? b : (-b);
|
||||
|
||||
uint32_t quotient = 0;
|
||||
uint32_t bit = 0x10000;
|
||||
|
||||
/* The algorithm requires D >= R */
|
||||
while (divider < remainder) {
|
||||
divider <<= 1;
|
||||
bit <<= 1;
|
||||
}
|
||||
|
||||
#ifndef FIXMATH_NO_OVERFLOW
|
||||
if (!bit)
|
||||
return FIX16_OVERFLOW;
|
||||
#endif
|
||||
|
||||
if (divider & 0x80000000) {
|
||||
// Perform one step manually to avoid overflows later.
|
||||
// We know that divider's bottom bit is 0 here.
|
||||
if (remainder >= divider) {
|
||||
quotient |= bit;
|
||||
remainder -= divider;
|
||||
}
|
||||
divider >>= 1;
|
||||
bit >>= 1;
|
||||
}
|
||||
|
||||
/* Main division loop */
|
||||
while (bit && remainder) {
|
||||
if (remainder >= divider) {
|
||||
quotient |= bit;
|
||||
remainder -= divider;
|
||||
}
|
||||
|
||||
remainder <<= 1;
|
||||
bit >>= 1;
|
||||
}
|
||||
|
||||
#ifndef FIXMATH_NO_ROUNDING
|
||||
if (remainder >= divider) {
|
||||
quotient++;
|
||||
}
|
||||
#endif
|
||||
|
||||
fix16_t result = quotient;
|
||||
|
||||
/* Figure out the sign of result */
|
||||
if ((a ^ b) & 0x80000000) {
|
||||
#ifndef FIXMATH_NO_OVERFLOW
|
||||
if (result == FIX16_MINIMUM)
|
||||
return FIX16_OVERFLOW;
|
||||
#endif
|
||||
|
||||
result = -result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static fix16_t fix16_sqrt(fix16_t in_value) {
|
||||
// It is assumed that x is not negative
|
||||
|
||||
uint32_t num = in_value;
|
||||
uint32_t result = 0;
|
||||
uint32_t bit;
|
||||
uint8_t n;
|
||||
|
||||
bit = (uint32_t) 1 << 30;
|
||||
while (bit > num)
|
||||
bit >>= 2;
|
||||
|
||||
// The main part is executed twice, in order to avoid
|
||||
// using 64 bit values in computations.
|
||||
for (n = 0; n < 2; n++) {
|
||||
// First we get the top 24 bits of the answer.
|
||||
while (bit) {
|
||||
if (num >= result + bit) {
|
||||
num -= result + bit;
|
||||
result = (result >> 1) + bit;
|
||||
} else {
|
||||
result = (result >> 1);
|
||||
}
|
||||
bit >>= 2;
|
||||
}
|
||||
|
||||
if (n == 0) {
|
||||
// Then process it again to get the lowest 8 bits.
|
||||
if (num > 65535) {
|
||||
// The remainder 'num' is too large to be shifted left
|
||||
// by 16, so we have to add 1 to result manually and
|
||||
// adjust 'num' accordingly.
|
||||
// num = a - (result + 0.5)^2
|
||||
// = num + result^2 - (result + 0.5)^2
|
||||
// = num - result - 0.5
|
||||
num -= result;
|
||||
num = (num << 16) - 0x8000;
|
||||
result = (result << 16) + 0x8000;
|
||||
} else {
|
||||
num <<= 16;
|
||||
result <<= 16;
|
||||
}
|
||||
|
||||
bit = 1 << 14;
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef FIXMATH_NO_ROUNDING
|
||||
// Finally, if next bit would have been 1, round the result upwards.
|
||||
if (num > result) {
|
||||
result++;
|
||||
}
|
||||
#endif
|
||||
|
||||
return (fix16_t) result;
|
||||
}
|
||||
|
||||
static fix16_t fix16_exp(fix16_t in_value) {
|
||||
// Function to approximate exp(); optimized more for code size than speed
|
||||
|
||||
// exp(x) for x = +/- {1, 1/8, 1/64, 1/512}
|
||||
fix16_t x = in_value;
|
||||
static const uint8_t NUM_EXP_VALUES = 4;
|
||||
static const fix16_t EXP_POS_VALUES[4] = {F16(2.7182818), F16(1.1331485), F16(1.0157477), F16(1.0019550)};
|
||||
static const fix16_t EXP_NEG_VALUES[4] = {F16(0.3678794), F16(0.8824969), F16(0.9844964), F16(0.9980488)};
|
||||
const fix16_t* exp_values;
|
||||
|
||||
fix16_t res, arg;
|
||||
uint16_t i;
|
||||
|
||||
if (x >= F16(10.3972))
|
||||
return FIX16_MAXIMUM;
|
||||
if (x <= F16(-11.7835))
|
||||
return 0;
|
||||
|
||||
if (x < 0) {
|
||||
x = -x;
|
||||
exp_values = EXP_NEG_VALUES;
|
||||
} else {
|
||||
exp_values = EXP_POS_VALUES;
|
||||
}
|
||||
|
||||
res = FIX16_ONE;
|
||||
arg = FIX16_ONE;
|
||||
for (i = 0; i < NUM_EXP_VALUES; i++) {
|
||||
while (x >= arg) {
|
||||
res = fix16_mul(res, exp_values[i]);
|
||||
x -= arg;
|
||||
}
|
||||
arg >>= 3;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static void voc_algorithm_init_instances(VocAlgorithmParams* params);
|
||||
static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams* params);
|
||||
static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams* params);
|
||||
static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams* params, fix16_t std_initial,
|
||||
fix16_t tau_mean_variance_hours,
|
||||
fix16_t gating_max_duration_minutes);
|
||||
static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams* params, fix16_t mean, fix16_t std,
|
||||
fix16_t uptime_gamma);
|
||||
static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams* params);
|
||||
static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams* params);
|
||||
static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams* params,
|
||||
fix16_t voc_index_from_prior);
|
||||
static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams* params, fix16_t sraw,
|
||||
fix16_t voc_index_from_prior);
|
||||
static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams* params);
|
||||
static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams* params, fix16_t l,
|
||||
fix16_t x0, fix16_t k);
|
||||
static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams* params, fix16_t sample);
|
||||
static void voc_algorithm_mox_model_init(VocAlgorithmParams* params);
|
||||
static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams* params, fix16_t sraw_std, fix16_t sraw_mean);
|
||||
static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams* params, fix16_t sraw);
|
||||
static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams* params);
|
||||
static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams* params, fix16_t offset);
|
||||
static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams* params, fix16_t sample);
|
||||
static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams* params);
|
||||
static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams* params);
|
||||
static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams* params, fix16_t sample);
|
||||
|
||||
void voc_algorithm_init(VocAlgorithmParams* params) {
|
||||
params->mVoc_Index_Offset = F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT);
|
||||
params->mTau_Mean_Variance_Hours = F16(VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS);
|
||||
params->mGating_Max_Duration_Minutes = F16(VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES);
|
||||
params->mSraw_Std_Initial = F16(VOC_ALGORITHM_SRAW_STD_INITIAL);
|
||||
params->mUptime = F16(0.);
|
||||
params->mSraw = F16(0.);
|
||||
params->mVoc_Index = 0;
|
||||
voc_algorithm_init_instances(params);
|
||||
}
|
||||
|
||||
static void voc_algorithm_init_instances(VocAlgorithmParams* params) {
|
||||
voc_algorithm_mean_variance_estimator_init(params);
|
||||
voc_algorithm_mean_variance_estimator_set_parameters(
|
||||
params, params->mSraw_Std_Initial, params->mTau_Mean_Variance_Hours, params->mGating_Max_Duration_Minutes);
|
||||
voc_algorithm_mox_model_init(params);
|
||||
voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params),
|
||||
voc_algorithm_mean_variance_estimator_get_mean(params));
|
||||
voc_algorithm_sigmoid_scaled_init(params);
|
||||
voc_algorithm_sigmoid_scaled_set_parameters(params, params->mVoc_Index_Offset);
|
||||
voc_algorithm_adaptive_lowpass_init(params);
|
||||
voc_algorithm_adaptive_lowpass_set_parameters(params);
|
||||
}
|
||||
|
||||
void voc_algorithm_get_states(VocAlgorithmParams* params, int32_t* state0, int32_t* state1) {
|
||||
*state0 = voc_algorithm_mean_variance_estimator_get_mean(params);
|
||||
*state1 = voc_algorithm_mean_variance_estimator_get_std(params);
|
||||
}
|
||||
|
||||
void voc_algorithm_set_states(VocAlgorithmParams* params, int32_t state0, int32_t state1) {
|
||||
voc_algorithm_mean_variance_estimator_set_states(params, state0, state1, F16(VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA));
|
||||
params->mSraw = state0;
|
||||
}
|
||||
|
||||
void voc_algorithm_set_tuning_parameters(VocAlgorithmParams* params, int32_t voc_index_offset,
|
||||
int32_t learning_time_hours, int32_t gating_max_duration_minutes,
|
||||
int32_t std_initial) {
|
||||
params->mVoc_Index_Offset = (fix16_from_int(voc_index_offset));
|
||||
params->mTau_Mean_Variance_Hours = (fix16_from_int(learning_time_hours));
|
||||
params->mGating_Max_Duration_Minutes = (fix16_from_int(gating_max_duration_minutes));
|
||||
params->mSraw_Std_Initial = (fix16_from_int(std_initial));
|
||||
voc_algorithm_init_instances(params);
|
||||
}
|
||||
|
||||
void voc_algorithm_process(VocAlgorithmParams* params, int32_t sraw, int32_t* voc_index) {
|
||||
if ((params->mUptime <= F16(VOC_ALGORITHM_INITIAL_BLACKOUT))) {
|
||||
params->mUptime = (params->mUptime + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
|
||||
} else {
|
||||
if (((sraw > 0) && (sraw < 65000))) {
|
||||
if ((sraw < 20001)) {
|
||||
sraw = 20001;
|
||||
} else if ((sraw > 52767)) {
|
||||
sraw = 52767;
|
||||
}
|
||||
params->mSraw = (fix16_from_int((sraw - 20000)));
|
||||
}
|
||||
params->mVoc_Index = voc_algorithm_mox_model_process(params, params->mSraw);
|
||||
params->mVoc_Index = voc_algorithm_sigmoid_scaled_process(params, params->mVoc_Index);
|
||||
params->mVoc_Index = voc_algorithm_adaptive_lowpass_process(params, params->mVoc_Index);
|
||||
if ((params->mVoc_Index < F16(0.5))) {
|
||||
params->mVoc_Index = F16(0.5);
|
||||
}
|
||||
if ((params->mSraw > F16(0.))) {
|
||||
voc_algorithm_mean_variance_estimator_process(params, params->mSraw, params->mVoc_Index);
|
||||
voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params),
|
||||
voc_algorithm_mean_variance_estimator_get_mean(params));
|
||||
}
|
||||
}
|
||||
*voc_index = (fix16_cast_to_int((params->mVoc_Index + F16(0.5))));
|
||||
}
|
||||
|
||||
static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams* params) {
|
||||
voc_algorithm_mean_variance_estimator_set_parameters(params, F16(0.), F16(0.), F16(0.));
|
||||
voc_algorithm_mean_variance_estimator_init_instances(params);
|
||||
}
|
||||
|
||||
static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams* params) {
|
||||
voc_algorithm_mean_variance_estimator_sigmoid_init(params);
|
||||
}
|
||||
|
||||
static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams* params, fix16_t std_initial,
|
||||
fix16_t tau_mean_variance_hours,
|
||||
fix16_t gating_max_duration_minutes) {
|
||||
params->m_Mean_Variance_Estimator__Gating_Max_Duration_Minutes = gating_max_duration_minutes;
|
||||
params->m_Mean_Variance_Estimator___Initialized = false;
|
||||
params->m_Mean_Variance_Estimator___Mean = F16(0.);
|
||||
params->m_Mean_Variance_Estimator___Sraw_Offset = F16(0.);
|
||||
params->m_Mean_Variance_Estimator___Std = std_initial;
|
||||
params->m_Mean_Variance_Estimator___Gamma =
|
||||
(fix16_div(F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * (VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.))),
|
||||
(tau_mean_variance_hours + F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.)))));
|
||||
params->m_Mean_Variance_Estimator___Gamma_Initial_Mean =
|
||||
F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) /
|
||||
(VOC_ALGORITHM_TAU_INITIAL_MEAN + VOC_ALGORITHM_SAMPLING_INTERVAL)));
|
||||
params->m_Mean_Variance_Estimator___Gamma_Initial_Variance =
|
||||
F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) /
|
||||
(VOC_ALGORITHM_TAU_INITIAL_VARIANCE + VOC_ALGORITHM_SAMPLING_INTERVAL)));
|
||||
params->m_Mean_Variance_Estimator__Gamma_Mean = F16(0.);
|
||||
params->m_Mean_Variance_Estimator__Gamma_Variance = F16(0.);
|
||||
params->m_Mean_Variance_Estimator___Uptime_Gamma = F16(0.);
|
||||
params->m_Mean_Variance_Estimator___Uptime_Gating = F16(0.);
|
||||
params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = F16(0.);
|
||||
}
|
||||
|
||||
static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams* params, fix16_t mean, fix16_t std,
|
||||
fix16_t uptime_gamma) {
|
||||
params->m_Mean_Variance_Estimator___Mean = mean;
|
||||
params->m_Mean_Variance_Estimator___Std = std;
|
||||
params->m_Mean_Variance_Estimator___Uptime_Gamma = uptime_gamma;
|
||||
params->m_Mean_Variance_Estimator___Initialized = true;
|
||||
}
|
||||
|
||||
static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams* params) {
|
||||
return params->m_Mean_Variance_Estimator___Std;
|
||||
}
|
||||
|
||||
static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams* params) {
|
||||
return (params->m_Mean_Variance_Estimator___Mean + params->m_Mean_Variance_Estimator___Sraw_Offset);
|
||||
}
|
||||
|
||||
static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams* params,
|
||||
fix16_t voc_index_from_prior) {
|
||||
fix16_t uptime_limit;
|
||||
fix16_t sigmoid_gamma_mean;
|
||||
fix16_t gamma_mean;
|
||||
fix16_t gating_threshold_mean;
|
||||
fix16_t sigmoid_gating_mean;
|
||||
fix16_t sigmoid_gamma_variance;
|
||||
fix16_t gamma_variance;
|
||||
fix16_t gating_threshold_variance;
|
||||
fix16_t sigmoid_gating_variance;
|
||||
|
||||
uptime_limit = F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX - VOC_ALGORITHM_SAMPLING_INTERVAL));
|
||||
if ((params->m_Mean_Variance_Estimator___Uptime_Gamma < uptime_limit)) {
|
||||
params->m_Mean_Variance_Estimator___Uptime_Gamma =
|
||||
(params->m_Mean_Variance_Estimator___Uptime_Gamma + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
|
||||
}
|
||||
if ((params->m_Mean_Variance_Estimator___Uptime_Gating < uptime_limit)) {
|
||||
params->m_Mean_Variance_Estimator___Uptime_Gating =
|
||||
(params->m_Mean_Variance_Estimator___Uptime_Gating + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
|
||||
}
|
||||
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_MEAN),
|
||||
F16(VOC_ALGORITHM_INIT_TRANSITION_MEAN));
|
||||
sigmoid_gamma_mean =
|
||||
voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator___Uptime_Gamma);
|
||||
gamma_mean =
|
||||
(params->m_Mean_Variance_Estimator___Gamma +
|
||||
(fix16_mul((params->m_Mean_Variance_Estimator___Gamma_Initial_Mean - params->m_Mean_Variance_Estimator___Gamma),
|
||||
sigmoid_gamma_mean)));
|
||||
gating_threshold_mean = (F16(VOC_ALGORITHM_GATING_THRESHOLD) +
|
||||
(fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)),
|
||||
voc_algorithm_mean_variance_estimator_sigmoid_process(
|
||||
params, params->m_Mean_Variance_Estimator___Uptime_Gating))));
|
||||
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_mean,
|
||||
F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION));
|
||||
sigmoid_gating_mean = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior);
|
||||
params->m_Mean_Variance_Estimator__Gamma_Mean = (fix16_mul(sigmoid_gating_mean, gamma_mean));
|
||||
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(
|
||||
params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_VARIANCE), F16(VOC_ALGORITHM_INIT_TRANSITION_VARIANCE));
|
||||
sigmoid_gamma_variance =
|
||||
voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator___Uptime_Gamma);
|
||||
gamma_variance = (params->m_Mean_Variance_Estimator___Gamma +
|
||||
(fix16_mul((params->m_Mean_Variance_Estimator___Gamma_Initial_Variance -
|
||||
params->m_Mean_Variance_Estimator___Gamma),
|
||||
(sigmoid_gamma_variance - sigmoid_gamma_mean))));
|
||||
gating_threshold_variance =
|
||||
(F16(VOC_ALGORITHM_GATING_THRESHOLD) +
|
||||
(fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)),
|
||||
voc_algorithm_mean_variance_estimator_sigmoid_process(
|
||||
params, params->m_Mean_Variance_Estimator___Uptime_Gating))));
|
||||
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_variance,
|
||||
F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION));
|
||||
sigmoid_gating_variance = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior);
|
||||
params->m_Mean_Variance_Estimator__Gamma_Variance = (fix16_mul(sigmoid_gating_variance, gamma_variance));
|
||||
params->m_Mean_Variance_Estimator___Gating_Duration_Minutes =
|
||||
(params->m_Mean_Variance_Estimator___Gating_Duration_Minutes +
|
||||
(fix16_mul(F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 60.)),
|
||||
((fix16_mul((F16(1.) - sigmoid_gating_mean), F16((1. + VOC_ALGORITHM_GATING_MAX_RATIO)))) -
|
||||
F16(VOC_ALGORITHM_GATING_MAX_RATIO)))));
|
||||
if ((params->m_Mean_Variance_Estimator___Gating_Duration_Minutes < F16(0.))) {
|
||||
params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = F16(0.);
|
||||
}
|
||||
if ((params->m_Mean_Variance_Estimator___Gating_Duration_Minutes >
|
||||
params->m_Mean_Variance_Estimator__Gating_Max_Duration_Minutes)) {
|
||||
params->m_Mean_Variance_Estimator___Uptime_Gating = F16(0.);
|
||||
}
|
||||
}
|
||||
|
||||
static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams* params, fix16_t sraw,
|
||||
fix16_t voc_index_from_prior) {
|
||||
fix16_t delta_sgp;
|
||||
fix16_t c;
|
||||
fix16_t additional_scaling;
|
||||
|
||||
if ((!params->m_Mean_Variance_Estimator___Initialized)) {
|
||||
params->m_Mean_Variance_Estimator___Initialized = true;
|
||||
params->m_Mean_Variance_Estimator___Sraw_Offset = sraw;
|
||||
params->m_Mean_Variance_Estimator___Mean = F16(0.);
|
||||
} else {
|
||||
if (((params->m_Mean_Variance_Estimator___Mean >= F16(100.)) ||
|
||||
(params->m_Mean_Variance_Estimator___Mean <= F16(-100.)))) {
|
||||
params->m_Mean_Variance_Estimator___Sraw_Offset =
|
||||
(params->m_Mean_Variance_Estimator___Sraw_Offset + params->m_Mean_Variance_Estimator___Mean);
|
||||
params->m_Mean_Variance_Estimator___Mean = F16(0.);
|
||||
}
|
||||
sraw = (sraw - params->m_Mean_Variance_Estimator___Sraw_Offset);
|
||||
voc_algorithm_mean_variance_estimator_calculate_gamma(params, voc_index_from_prior);
|
||||
delta_sgp = (fix16_div((sraw - params->m_Mean_Variance_Estimator___Mean),
|
||||
F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING)));
|
||||
if ((delta_sgp < F16(0.))) {
|
||||
c = (params->m_Mean_Variance_Estimator___Std - delta_sgp);
|
||||
} else {
|
||||
c = (params->m_Mean_Variance_Estimator___Std + delta_sgp);
|
||||
}
|
||||
additional_scaling = F16(1.);
|
||||
if ((c > F16(1440.))) {
|
||||
additional_scaling = F16(4.);
|
||||
}
|
||||
params->m_Mean_Variance_Estimator___Std = (fix16_mul(
|
||||
fix16_sqrt((fix16_mul(additional_scaling, (F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING) -
|
||||
params->m_Mean_Variance_Estimator__Gamma_Variance)))),
|
||||
fix16_sqrt(((fix16_mul(params->m_Mean_Variance_Estimator___Std,
|
||||
(fix16_div(params->m_Mean_Variance_Estimator___Std,
|
||||
(fix16_mul(F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING),
|
||||
additional_scaling)))))) +
|
||||
(fix16_mul((fix16_div((fix16_mul(params->m_Mean_Variance_Estimator__Gamma_Variance, delta_sgp)),
|
||||
additional_scaling)),
|
||||
delta_sgp))))));
|
||||
params->m_Mean_Variance_Estimator___Mean = (params->m_Mean_Variance_Estimator___Mean +
|
||||
(fix16_mul(params->m_Mean_Variance_Estimator__Gamma_Mean, delta_sgp)));
|
||||
}
|
||||
}
|
||||
|
||||
static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams* params) {
|
||||
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(0.), F16(0.), F16(0.));
|
||||
}
|
||||
|
||||
static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams* params, fix16_t l,
|
||||
fix16_t x0, fix16_t k) {
|
||||
params->m_Mean_Variance_Estimator___Sigmoid__L = l;
|
||||
params->m_Mean_Variance_Estimator___Sigmoid__K = k;
|
||||
params->m_Mean_Variance_Estimator___Sigmoid__X0 = x0;
|
||||
}
|
||||
|
||||
static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams* params, fix16_t sample) {
|
||||
fix16_t x;
|
||||
|
||||
x = (fix16_mul(params->m_Mean_Variance_Estimator___Sigmoid__K,
|
||||
(sample - params->m_Mean_Variance_Estimator___Sigmoid__X0)));
|
||||
if ((x < F16(-50.))) {
|
||||
return params->m_Mean_Variance_Estimator___Sigmoid__L;
|
||||
} else if ((x > F16(50.))) {
|
||||
return F16(0.);
|
||||
} else {
|
||||
return (fix16_div(params->m_Mean_Variance_Estimator___Sigmoid__L, (F16(1.) + fix16_exp(x))));
|
||||
}
|
||||
}
|
||||
|
||||
static void voc_algorithm_mox_model_init(VocAlgorithmParams* params) {
|
||||
voc_algorithm_mox_model_set_parameters(params, F16(1.), F16(0.));
|
||||
}
|
||||
|
||||
static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams* params, fix16_t sraw_std, fix16_t sraw_mean) {
|
||||
params->m_Mox_Model__Sraw_Std = sraw_std;
|
||||
params->m_Mox_Model__Sraw_Mean = sraw_mean;
|
||||
}
|
||||
|
||||
static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams* params, fix16_t sraw) {
|
||||
return (fix16_mul((fix16_div((sraw - params->m_Mox_Model__Sraw_Mean),
|
||||
(-(params->m_Mox_Model__Sraw_Std + F16(VOC_ALGORITHM_SRAW_STD_BONUS))))),
|
||||
F16(VOC_ALGORITHM_VOC_INDEX_GAIN)));
|
||||
}
|
||||
|
||||
static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams* params) {
|
||||
voc_algorithm_sigmoid_scaled_set_parameters(params, F16(0.));
|
||||
}
|
||||
|
||||
static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams* params, fix16_t offset) {
|
||||
params->m_Sigmoid_Scaled__Offset = offset;
|
||||
}
|
||||
|
||||
static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams* params, fix16_t sample) {
|
||||
fix16_t x;
|
||||
fix16_t shift;
|
||||
|
||||
x = (fix16_mul(F16(VOC_ALGORITHM_SIGMOID_K), (sample - F16(VOC_ALGORITHM_SIGMOID_X0))));
|
||||
if ((x < F16(-50.))) {
|
||||
return F16(VOC_ALGORITHM_SIGMOID_L);
|
||||
} else if ((x > F16(50.))) {
|
||||
return F16(0.);
|
||||
} else {
|
||||
if ((sample >= F16(0.))) {
|
||||
shift =
|
||||
(fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) - (fix16_mul(F16(5.), params->m_Sigmoid_Scaled__Offset))), F16(4.)));
|
||||
return ((fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) + shift), (F16(1.) + fix16_exp(x)))) - shift);
|
||||
} else {
|
||||
return (fix16_mul((fix16_div(params->m_Sigmoid_Scaled__Offset, F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT))),
|
||||
(fix16_div(F16(VOC_ALGORITHM_SIGMOID_L), (F16(1.) + fix16_exp(x))))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams* params) {
|
||||
voc_algorithm_adaptive_lowpass_set_parameters(params);
|
||||
}
|
||||
|
||||
static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams* params) {
|
||||
params->m_Adaptive_Lowpass__A1 =
|
||||
F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_FAST + VOC_ALGORITHM_SAMPLING_INTERVAL)));
|
||||
params->m_Adaptive_Lowpass__A2 =
|
||||
F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_SLOW + VOC_ALGORITHM_SAMPLING_INTERVAL)));
|
||||
params->m_Adaptive_Lowpass___Initialized = false;
|
||||
}
|
||||
|
||||
static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams* params, fix16_t sample) {
|
||||
fix16_t abs_delta;
|
||||
fix16_t f1;
|
||||
fix16_t tau_a;
|
||||
fix16_t a3;
|
||||
|
||||
if ((!params->m_Adaptive_Lowpass___Initialized)) {
|
||||
params->m_Adaptive_Lowpass___X1 = sample;
|
||||
params->m_Adaptive_Lowpass___X2 = sample;
|
||||
params->m_Adaptive_Lowpass___X3 = sample;
|
||||
params->m_Adaptive_Lowpass___Initialized = true;
|
||||
}
|
||||
params->m_Adaptive_Lowpass___X1 =
|
||||
((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass__A1), params->m_Adaptive_Lowpass___X1)) +
|
||||
(fix16_mul(params->m_Adaptive_Lowpass__A1, sample)));
|
||||
params->m_Adaptive_Lowpass___X2 =
|
||||
((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass__A2), params->m_Adaptive_Lowpass___X2)) +
|
||||
(fix16_mul(params->m_Adaptive_Lowpass__A2, sample)));
|
||||
abs_delta = (params->m_Adaptive_Lowpass___X1 - params->m_Adaptive_Lowpass___X2);
|
||||
if ((abs_delta < F16(0.))) {
|
||||
abs_delta = (-abs_delta);
|
||||
}
|
||||
f1 = fix16_exp((fix16_mul(F16(VOC_ALGORITHM_LP_ALPHA), abs_delta)));
|
||||
tau_a =
|
||||
((fix16_mul(F16((VOC_ALGORITHM_LP_TAU_SLOW - VOC_ALGORITHM_LP_TAU_FAST)), f1)) + F16(VOC_ALGORITHM_LP_TAU_FAST));
|
||||
a3 = (fix16_div(F16(VOC_ALGORITHM_SAMPLING_INTERVAL), (F16(VOC_ALGORITHM_SAMPLING_INTERVAL) + tau_a)));
|
||||
params->m_Adaptive_Lowpass___X3 =
|
||||
((fix16_mul((F16(1.) - a3), params->m_Adaptive_Lowpass___X3)) + (fix16_mul(a3, sample)));
|
||||
return params->m_Adaptive_Lowpass___X3;
|
||||
}
|
||||
} // namespace sgp40
|
||||
} // namespace esphome
|
||||
147
esphome/components/sgp40/sensirion_voc_algorithm.h
Normal file
147
esphome/components/sgp40/sensirion_voc_algorithm.h
Normal file
@@ -0,0 +1,147 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
namespace esphome {
|
||||
namespace sgp40 {
|
||||
|
||||
/* The VOC code were originally created by
|
||||
* https://github.com/Sensirion/embedded-sgp
|
||||
* The fixed point arithmetic parts of this code were originally created by
|
||||
* https://github.com/PetteriAimonen/libfixmath
|
||||
*/
|
||||
|
||||
using fix16_t = int32_t;
|
||||
|
||||
#define F16(x) ((fix16_t)(((x) >= 0) ? ((x) *65536.0 + 0.5) : ((x) *65536.0 - 0.5)))
|
||||
|
||||
static const float VOC_ALGORITHM_SAMPLING_INTERVAL(1.);
|
||||
static const float VOC_ALGORITHM_INITIAL_BLACKOUT(45.);
|
||||
static const float VOC_ALGORITHM_VOC_INDEX_GAIN(230.);
|
||||
static const float VOC_ALGORITHM_SRAW_STD_INITIAL(50.);
|
||||
static const float VOC_ALGORITHM_SRAW_STD_BONUS(220.);
|
||||
static const float VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS(12.);
|
||||
static const float VOC_ALGORITHM_TAU_INITIAL_MEAN(20.);
|
||||
static const float VOC_ALGORITHM_INIT_DURATION_MEAN((3600. * 0.75));
|
||||
static const float VOC_ALGORITHM_INIT_TRANSITION_MEAN(0.01);
|
||||
static const float VOC_ALGORITHM_TAU_INITIAL_VARIANCE(2500.);
|
||||
static const float VOC_ALGORITHM_INIT_DURATION_VARIANCE((3600. * 1.45));
|
||||
static const float VOC_ALGORITHM_INIT_TRANSITION_VARIANCE(0.01);
|
||||
static const float VOC_ALGORITHM_GATING_THRESHOLD(340.);
|
||||
static const float VOC_ALGORITHM_GATING_THRESHOLD_INITIAL(510.);
|
||||
static const float VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION(0.09);
|
||||
static const float VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES((60. * 3.));
|
||||
static const float VOC_ALGORITHM_GATING_MAX_RATIO(0.3);
|
||||
static const float VOC_ALGORITHM_SIGMOID_L(500.);
|
||||
static const float VOC_ALGORITHM_SIGMOID_K(-0.0065);
|
||||
static const float VOC_ALGORITHM_SIGMOID_X0(213.);
|
||||
static const float VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT(100.);
|
||||
static const float VOC_ALGORITHM_LP_TAU_FAST(20.0);
|
||||
static const float VOC_ALGORITHM_LP_TAU_SLOW(500.0);
|
||||
static const float VOC_ALGORITHM_LP_ALPHA(-0.2);
|
||||
static const float VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA((3. * 3600.));
|
||||
static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING(64.);
|
||||
static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX(32767.);
|
||||
|
||||
/**
|
||||
* Struct to hold all the states of the VOC algorithm.
|
||||
*/
|
||||
struct VocAlgorithmParams {
|
||||
fix16_t mVoc_Index_Offset;
|
||||
fix16_t mTau_Mean_Variance_Hours;
|
||||
fix16_t mGating_Max_Duration_Minutes;
|
||||
fix16_t mSraw_Std_Initial;
|
||||
fix16_t mUptime;
|
||||
fix16_t mSraw;
|
||||
fix16_t mVoc_Index;
|
||||
fix16_t m_Mean_Variance_Estimator__Gating_Max_Duration_Minutes;
|
||||
bool m_Mean_Variance_Estimator___Initialized;
|
||||
fix16_t m_Mean_Variance_Estimator___Mean;
|
||||
fix16_t m_Mean_Variance_Estimator___Sraw_Offset;
|
||||
fix16_t m_Mean_Variance_Estimator___Std;
|
||||
fix16_t m_Mean_Variance_Estimator___Gamma;
|
||||
fix16_t m_Mean_Variance_Estimator___Gamma_Initial_Mean;
|
||||
fix16_t m_Mean_Variance_Estimator___Gamma_Initial_Variance;
|
||||
fix16_t m_Mean_Variance_Estimator__Gamma_Mean;
|
||||
fix16_t m_Mean_Variance_Estimator__Gamma_Variance;
|
||||
fix16_t m_Mean_Variance_Estimator___Uptime_Gamma;
|
||||
fix16_t m_Mean_Variance_Estimator___Uptime_Gating;
|
||||
fix16_t m_Mean_Variance_Estimator___Gating_Duration_Minutes;
|
||||
fix16_t m_Mean_Variance_Estimator___Sigmoid__L;
|
||||
fix16_t m_Mean_Variance_Estimator___Sigmoid__K;
|
||||
fix16_t m_Mean_Variance_Estimator___Sigmoid__X0;
|
||||
fix16_t m_Mox_Model__Sraw_Std;
|
||||
fix16_t m_Mox_Model__Sraw_Mean;
|
||||
fix16_t m_Sigmoid_Scaled__Offset;
|
||||
fix16_t m_Adaptive_Lowpass__A1;
|
||||
fix16_t m_Adaptive_Lowpass__A2;
|
||||
bool m_Adaptive_Lowpass___Initialized;
|
||||
fix16_t m_Adaptive_Lowpass___X1;
|
||||
fix16_t m_Adaptive_Lowpass___X2;
|
||||
fix16_t m_Adaptive_Lowpass___X3;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize the VOC algorithm parameters. Call this once at the beginning or
|
||||
* whenever the sensor stopped measurements.
|
||||
* @param params Pointer to the VocAlgorithmParams struct
|
||||
*/
|
||||
void voc_algorithm_init(VocAlgorithmParams *params);
|
||||
|
||||
/**
|
||||
* Get current algorithm states. Retrieved values can be used in
|
||||
* voc_algorithm_set_states() to resume operation after a short interruption,
|
||||
* skipping initial learning phase. This feature can only be used after at least
|
||||
* 3 hours of continuous operation.
|
||||
* @param params Pointer to the VocAlgorithmParams struct
|
||||
* @param state0 State0 to be stored
|
||||
* @param state1 State1 to be stored
|
||||
*/
|
||||
void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1);
|
||||
|
||||
/**
|
||||
* Set previously retrieved algorithm states to resume operation after a short
|
||||
* interruption, skipping initial learning phase. This feature should not be
|
||||
* used after inerruptions of more than 10 minutes. Call this once after
|
||||
* voc_algorithm_init() and the optional voc_algorithm_set_tuning_parameters(), if
|
||||
* desired. Otherwise, the algorithm will start with initial learning phase.
|
||||
* @param params Pointer to the VocAlgorithmParams struct
|
||||
* @param state0 State0 to be restored
|
||||
* @param state1 State1 to be restored
|
||||
*/
|
||||
void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1);
|
||||
|
||||
/**
|
||||
* Set parameters to customize the VOC algorithm. Call this once after
|
||||
* voc_algorithm_init(), if desired. Otherwise, the default values will be used.
|
||||
*
|
||||
* @param params Pointer to the VocAlgorithmParams struct
|
||||
* @param voc_index_offset VOC index representing typical (average)
|
||||
* conditions. Range 1..250, default 100
|
||||
* @param learning_time_hours Time constant of long-term estimator.
|
||||
* Past events will be forgotten after about
|
||||
* twice the learning time.
|
||||
* Range 1..72 [hours], default 12 [hours]
|
||||
* @param gating_max_duration_minutes Maximum duration of gating (freeze of
|
||||
* estimator during high VOC index signal).
|
||||
* 0 (no gating) or range 1..720 [minutes],
|
||||
* default 180 [minutes]
|
||||
* @param std_initial Initial estimate for standard deviation.
|
||||
* Lower value boosts events during initial
|
||||
* learning period, but may result in larger
|
||||
* device-to-device variations.
|
||||
* Range 10..500, default 50
|
||||
*/
|
||||
void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset,
|
||||
int32_t learning_time_hours, int32_t gating_max_duration_minutes,
|
||||
int32_t std_initial);
|
||||
|
||||
/**
|
||||
* Calculate the VOC index value from the raw sensor value.
|
||||
*
|
||||
* @param params Pointer to the VocAlgorithmParams struct
|
||||
* @param sraw Raw value from the SGP40 sensor
|
||||
* @param voc_index Calculated VOC index value from the raw sensor value. Zero
|
||||
* during initial blackout period and 1..500 afterwards
|
||||
*/
|
||||
void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index);
|
||||
} // namespace sgp40
|
||||
} // namespace esphome
|
||||
57
esphome/components/sgp40/sensor.py
Normal file
57
esphome/components/sgp40/sensor.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import CONF_ID, DEVICE_CLASS_EMPTY, ICON_RADIATOR, UNIT_EMPTY
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
CODEOWNERS = ["@SenexCrenshaw"]
|
||||
|
||||
sgp40_ns = cg.esphome_ns.namespace("sgp40")
|
||||
SGP40Component = sgp40_ns.class_(
|
||||
"SGP40Component", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONF_COMPENSATION = "compensation"
|
||||
CONF_HUMIDITY_SOURCE = "humidity_source"
|
||||
CONF_TEMPERATURE_SOURCE = "temperature_source"
|
||||
CONF_STORE_BASELINE = "store_baseline"
|
||||
CONF_VOC_BASELINE = "voc_baseline"
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(UNIT_EMPTY, ICON_RADIATOR, 0, DEVICE_CLASS_EMPTY)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SGP40Component),
|
||||
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
|
||||
cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
|
||||
cv.Optional(CONF_COMPENSATION): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor),
|
||||
cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor),
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x59))
|
||||
)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
yield cg.register_component(var, config)
|
||||
yield i2c.register_i2c_device(var, config)
|
||||
yield sensor.register_sensor(var, config)
|
||||
|
||||
if CONF_COMPENSATION in config:
|
||||
compensation_config = config[CONF_COMPENSATION]
|
||||
sens = yield cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
|
||||
cg.add(var.set_humidity_sensor(sens))
|
||||
sens = yield cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
|
||||
cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE]))
|
||||
|
||||
if CONF_VOC_BASELINE in config:
|
||||
cg.add(var.set_voc_baseline(CONF_VOC_BASELINE))
|
||||
314
esphome/components/sgp40/sgp40.cpp
Normal file
314
esphome/components/sgp40/sgp40.cpp
Normal file
@@ -0,0 +1,314 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "sgp40.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sgp40 {
|
||||
|
||||
static const char *TAG = "sgp40";
|
||||
|
||||
void SGP40Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up SGP40...");
|
||||
|
||||
// Serial Number identification
|
||||
if (!this->write_command_(SGP40_CMD_GET_SERIAL_ID)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint16_t raw_serial_number[3];
|
||||
|
||||
if (!this->read_data_(raw_serial_number, 3)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) |
|
||||
(uint64_t(raw_serial_number[2]));
|
||||
ESP_LOGD(TAG, "Serial Number: %llu", this->serial_number_);
|
||||
|
||||
// Featureset identification for future use
|
||||
if (!this->write_command_(SGP40_CMD_GET_FEATURESET)) {
|
||||
ESP_LOGD(TAG, "raw_featureset write_command_ failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint16_t raw_featureset[1];
|
||||
if (!this->read_data_(raw_featureset, 1)) {
|
||||
ESP_LOGD(TAG, "raw_featureset read_data_ failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->featureset_ = raw_featureset[0];
|
||||
if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) {
|
||||
ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF),
|
||||
SGP40_FEATURESET);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF));
|
||||
|
||||
voc_algorithm_init(&this->voc_algorithm_params_);
|
||||
|
||||
if (this->store_baseline_) {
|
||||
// Hash with compilation time
|
||||
// This ensures the baseline storage is cleared after OTA
|
||||
uint32_t hash = fnv1_hash(App.get_compilation_time());
|
||||
this->pref_ = global_preferences.make_preference<SGP40Baselines>(hash, true);
|
||||
|
||||
if (this->pref_.load(&this->baselines_storage_)) {
|
||||
this->state0_ = this->baselines_storage_.state0;
|
||||
this->state1_ = this->baselines_storage_.state1;
|
||||
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0,
|
||||
baselines_storage_.state1);
|
||||
}
|
||||
|
||||
// Initialize storage timestamp
|
||||
this->seconds_since_last_store_ = 0;
|
||||
|
||||
if (this->baselines_storage_.state0 > 0 && this->baselines_storage_.state1 > 0) {
|
||||
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0,
|
||||
baselines_storage_.state1);
|
||||
voc_algorithm_set_states(&this->voc_algorithm_params_, this->baselines_storage_.state0,
|
||||
this->baselines_storage_.state1);
|
||||
}
|
||||
}
|
||||
|
||||
this->self_test_();
|
||||
}
|
||||
|
||||
void SGP40Component::self_test_() {
|
||||
ESP_LOGD(TAG, "selfTest started");
|
||||
if (!this->write_command_(SGP40_CMD_SELF_TEST)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
ESP_LOGD(TAG, "selfTest communicatin failed");
|
||||
this->mark_failed();
|
||||
}
|
||||
|
||||
this->set_timeout(250, [this]() {
|
||||
uint16_t reply[1];
|
||||
if (!this->read_data_(reply, 1)) {
|
||||
ESP_LOGD(TAG, "selfTest read_data_ failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply[0] == 0xD400) {
|
||||
ESP_LOGD(TAG, "selfTest completed");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "selfTest failed");
|
||||
this->mark_failed();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Combined the measured gasses, temperature, and humidity
|
||||
* to calculate the VOC Index
|
||||
*
|
||||
* @param temperature The measured temperature in degrees C
|
||||
* @param humidity The measured relative humidity in % rH
|
||||
* @return int32_t The VOC Index
|
||||
*/
|
||||
int32_t SGP40Component::measure_voc_index_() {
|
||||
int32_t voc_index;
|
||||
|
||||
uint16_t sraw = measure_raw_();
|
||||
|
||||
if (sraw == UINT16_MAX)
|
||||
return UINT16_MAX;
|
||||
|
||||
this->status_clear_warning();
|
||||
|
||||
voc_algorithm_process(&voc_algorithm_params_, sraw, &voc_index);
|
||||
|
||||
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
|
||||
// much
|
||||
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
|
||||
voc_algorithm_get_states(&voc_algorithm_params_, &this->state0_, &this->state1_);
|
||||
if (abs(this->baselines_storage_.state0 - this->state0_) > MAXIMUM_STORAGE_DIFF ||
|
||||
abs(this->baselines_storage_.state1 - this->state1_) > MAXIMUM_STORAGE_DIFF) {
|
||||
this->seconds_since_last_store_ = 0;
|
||||
this->baselines_storage_.state0 = this->state0_;
|
||||
this->baselines_storage_.state1 = this->state1_;
|
||||
|
||||
if (this->pref_.save(&this->baselines_storage_)) {
|
||||
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->baselines_storage_.state0,
|
||||
baselines_storage_.state1);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Could not store VOC baselines");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return voc_index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Return the raw gas measurement
|
||||
*
|
||||
* @param temperature The measured temperature in degrees C
|
||||
* @param humidity The measured relative humidity in % rH
|
||||
* @return uint16_t The current raw gas measurement
|
||||
*/
|
||||
uint16_t SGP40Component::measure_raw_() {
|
||||
float humidity = NAN;
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
humidity = this->humidity_sensor_->state;
|
||||
}
|
||||
if (isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
|
||||
humidity = 50;
|
||||
}
|
||||
|
||||
float temperature = NAN;
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
temperature = float(this->temperature_sensor_->state);
|
||||
}
|
||||
if (isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
|
||||
temperature = 25;
|
||||
}
|
||||
|
||||
uint8_t command[8];
|
||||
|
||||
command[0] = 0x26;
|
||||
command[1] = 0x0F;
|
||||
|
||||
uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100));
|
||||
command[2] = rhticks >> 8;
|
||||
command[3] = rhticks & 0xFF;
|
||||
command[4] = generate_crc_(command + 2, 2);
|
||||
uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175);
|
||||
command[5] = tempticks >> 8;
|
||||
command[6] = tempticks & 0xFF;
|
||||
command[7] = generate_crc_(command + 5, 2);
|
||||
|
||||
if (!this->write_bytes_raw(command, 8)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "write_bytes_raw error");
|
||||
return UINT16_MAX;
|
||||
}
|
||||
delay(250); // NOLINT
|
||||
uint16_t raw_data[1];
|
||||
|
||||
if (!this->read_data_(raw_data, 1)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "read_data_ error");
|
||||
return UINT16_MAX;
|
||||
}
|
||||
return raw_data[0];
|
||||
}
|
||||
|
||||
uint8_t SGP40Component::generate_crc_(const uint8_t *data, uint8_t datalen) {
|
||||
// calculates 8-Bit checksum with given polynomial
|
||||
uint8_t crc = SGP40_CRC8_INIT;
|
||||
|
||||
for (uint8_t i = 0; i < datalen; i++) {
|
||||
crc ^= data[i];
|
||||
for (uint8_t b = 0; b < 8; b++) {
|
||||
if (crc & 0x80)
|
||||
crc = (crc << 1) ^ SGP40_CRC8_POLYNOMIAL;
|
||||
else
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
void SGP40Component::update() {
|
||||
this->seconds_since_last_store_ += this->update_interval_ / 1000;
|
||||
|
||||
uint32_t voc_index = this->measure_voc_index_();
|
||||
|
||||
if (this->samples_read_++ < this->samples_to_stabalize_) {
|
||||
ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_,
|
||||
this->samples_to_stabalize_, voc_index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (voc_index != UINT16_MAX) {
|
||||
this->status_clear_warning();
|
||||
this->publish_state(voc_index);
|
||||
} else {
|
||||
this->status_set_warning();
|
||||
}
|
||||
}
|
||||
|
||||
void SGP40Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "SGP40:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
switch (this->error_code_) {
|
||||
case COMMUNICATION_FAILED:
|
||||
ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown setup error!");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Serial number: %llu", this->serial_number_);
|
||||
ESP_LOGCONFIG(TAG, " Minimum Samples: %f", VOC_ALGORITHM_INITIAL_BLACKOUT);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Compensation:");
|
||||
LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Compensation: No source configured");
|
||||
}
|
||||
}
|
||||
|
||||
bool SGP40Component::write_command_(uint16_t command) {
|
||||
// Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
|
||||
return this->write_byte(command >> 8, command & 0xFF);
|
||||
}
|
||||
|
||||
uint8_t SGP40Component::sht_crc_(uint8_t data1, uint8_t data2) {
|
||||
uint8_t bit;
|
||||
uint8_t crc = 0xFF;
|
||||
|
||||
crc ^= data1;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80)
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
else
|
||||
crc = (crc << 1);
|
||||
}
|
||||
|
||||
crc ^= data2;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80)
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
else
|
||||
crc = (crc << 1);
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool SGP40Component::read_data_(uint16_t *data, uint8_t len) {
|
||||
const uint8_t num_bytes = len * 3;
|
||||
std::vector<uint8_t> buf(num_bytes);
|
||||
|
||||
if (!this->parent_->raw_receive(this->address_, buf.data(), num_bytes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
const uint8_t j = 3 * i;
|
||||
uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
|
||||
if (crc != buf[j + 2]) {
|
||||
ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
|
||||
return false;
|
||||
}
|
||||
data[i] = (buf[j] << 8) | buf[j + 1];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace sgp40
|
||||
} // namespace esphome
|
||||
92
esphome/components/sgp40/sgp40.h
Normal file
92
esphome/components/sgp40/sgp40.h
Normal file
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "sensirion_voc_algorithm.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome {
|
||||
namespace sgp40 {
|
||||
|
||||
struct SGP40Baselines {
|
||||
int32_t state0;
|
||||
int32_t state1;
|
||||
} PACKED; // NOLINT
|
||||
|
||||
// commands and constants
|
||||
static const uint8_t SGP40_FEATURESET = 0x0020; ///< The required set for this library
|
||||
static const uint8_t SGP40_CRC8_POLYNOMIAL = 0x31; ///< Seed for SGP40's CRC polynomial
|
||||
static const uint8_t SGP40_CRC8_INIT = 0xFF; ///< Init value for CRC
|
||||
static const uint8_t SGP40_WORD_LEN = 2; ///< 2 bytes per word
|
||||
|
||||
// Commands
|
||||
|
||||
static const uint16_t SGP40_CMD_GET_SERIAL_ID = 0x3682;
|
||||
static const uint16_t SGP40_CMD_GET_FEATURESET = 0x202f;
|
||||
static const uint16_t SGP40_CMD_SELF_TEST = 0x280e;
|
||||
|
||||
// Shortest time interval of 3H for storing baseline values.
|
||||
// Prevents wear of the flash because of too many write operations
|
||||
const long SHORTEST_BASELINE_STORE_INTERVAL = 10800;
|
||||
|
||||
// Store anyway if the baseline difference exceeds the max storage diff value
|
||||
const long MAXIMUM_STORAGE_DIFF = 50;
|
||||
|
||||
class SGP40Component;
|
||||
|
||||
/// This class implements support for the Sensirion sgp40 i2c GAS (VOC) sensors.
|
||||
class SGP40Component : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice {
|
||||
public:
|
||||
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
|
||||
void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
|
||||
|
||||
protected:
|
||||
/// Input sensor for humidity and temperature compensation.
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
bool write_command_(uint16_t command);
|
||||
bool read_data_(uint16_t *data, uint8_t len);
|
||||
int16_t sensirion_init_sensors_();
|
||||
int16_t sgp40_probe_();
|
||||
uint8_t sht_crc_(uint8_t data1, uint8_t data2);
|
||||
uint64_t serial_number_;
|
||||
uint16_t featureset_;
|
||||
int32_t measure_voc_index_();
|
||||
uint8_t generate_crc_(const uint8_t *data, uint8_t datalen);
|
||||
uint16_t measure_raw_();
|
||||
ESPPreferenceObject pref_;
|
||||
long seconds_since_last_store_;
|
||||
SGP40Baselines baselines_storage_;
|
||||
VocAlgorithmParams voc_algorithm_params_;
|
||||
bool store_baseline_;
|
||||
int32_t state0_;
|
||||
int32_t state1_;
|
||||
uint8_t samples_read_ = 0;
|
||||
uint8_t samples_to_stabalize_ = static_cast<int8_t>(VOC_ALGORITHM_INITIAL_BLACKOUT) * 2;
|
||||
|
||||
/**
|
||||
* @brief Request the sensor to perform a self-test, returning the result
|
||||
*
|
||||
* @return true: success false:failure
|
||||
*/
|
||||
void self_test_();
|
||||
enum ErrorCode {
|
||||
COMMUNICATION_FAILED,
|
||||
MEASUREMENT_INIT_FAILED,
|
||||
INVALID_ID,
|
||||
UNSUPPORTED_ID,
|
||||
UNKNOWN
|
||||
} error_code_{UNKNOWN};
|
||||
};
|
||||
} // namespace sgp40
|
||||
} // namespace esphome
|
||||
0
esphome/components/sht4x/__init__.py
Normal file
0
esphome/components/sht4x/__init__.py
Normal file
92
esphome/components/sht4x/sensor.py
Normal file
92
esphome/components/sht4x/sensor.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_HUMIDITY,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
ICON_THERMOMETER,
|
||||
ICON_WATER_PERCENT,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@sjtrny"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
sht4x_ns = cg.esphome_ns.namespace("sht4x")
|
||||
|
||||
SHT4XComponent = sht4x_ns.class_("SHT4XComponent", cg.PollingComponent, i2c.I2CDevice)
|
||||
|
||||
CONF_PRECISION = "precision"
|
||||
SHT4XPRECISION = sht4x_ns.enum("SHT4XPRECISION")
|
||||
PRECISION_OPTIONS = {
|
||||
"High": SHT4XPRECISION.SHT4X_PRECISION_HIGH,
|
||||
"Med": SHT4XPRECISION.SHT4X_PRECISION_MED,
|
||||
"Low": SHT4XPRECISION.SHT4X_PRECISION_LOW,
|
||||
}
|
||||
|
||||
CONF_HEATER_POWER = "heater_power"
|
||||
SHT4XHEATERPOWER = sht4x_ns.enum("SHT4XHEATERPOWER")
|
||||
HEATER_POWER_OPTIONS = {
|
||||
"High": SHT4XHEATERPOWER.SHT4X_HEATERPOWER_HIGH,
|
||||
"Med": SHT4XHEATERPOWER.SHT4X_HEATERPOWER_MED,
|
||||
"Low": SHT4XHEATERPOWER.SHT4X_HEATERPOWER_LOW,
|
||||
}
|
||||
|
||||
CONF_HEATER_TIME = "heater_time"
|
||||
SHT4XHEATERTIME = sht4x_ns.enum("SHT4XHEATERTIME")
|
||||
HEATER_TIME_OPTIONS = {
|
||||
"Long": SHT4XHEATERTIME.SHT4X_HEATERTIME_LONG,
|
||||
"Short": SHT4XHEATERTIME.SHT4X_HEATERTIME_SHORT,
|
||||
}
|
||||
|
||||
CONF_HEATER_MAX_DUTY = "heater_max_duty"
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SHT4XComponent),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
UNIT_CELSIUS, ICON_THERMOMETER, 2, DEVICE_CLASS_TEMPERATURE
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
UNIT_PERCENT, ICON_WATER_PERCENT, 2, DEVICE_CLASS_HUMIDITY
|
||||
),
|
||||
cv.Optional(CONF_PRECISION, default="High"): cv.enum(PRECISION_OPTIONS),
|
||||
cv.Optional(CONF_HEATER_POWER, default="High"): cv.enum(
|
||||
HEATER_POWER_OPTIONS
|
||||
),
|
||||
cv.Optional(CONF_HEATER_TIME, default="Long"): cv.enum(HEATER_TIME_OPTIONS),
|
||||
cv.Optional(CONF_HEATER_MAX_DUTY, default=0.0): cv.float_range(
|
||||
min=0.0, max=0.05
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x44))
|
||||
)
|
||||
|
||||
TYPES = {
|
||||
CONF_TEMPERATURE: "set_temp_sensor",
|
||||
CONF_HUMIDITY: "set_humidity_sensor",
|
||||
}
|
||||
|
||||
|
||||
def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
yield cg.register_component(var, config)
|
||||
yield i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_precision_value(config[CONF_PRECISION]))
|
||||
cg.add(var.set_heater_power_value(config[CONF_HEATER_POWER]))
|
||||
cg.add(var.set_heater_time_value(config[CONF_HEATER_TIME]))
|
||||
cg.add(var.set_heater_duty_value(config[CONF_HEATER_MAX_DUTY]))
|
||||
|
||||
for key, funcName in TYPES.items():
|
||||
|
||||
if key in config:
|
||||
sens = yield sensor.new_sensor(config[key])
|
||||
cg.add(getattr(var, funcName)(sens))
|
||||
89
esphome/components/sht4x/sht4x.cpp
Normal file
89
esphome/components/sht4x/sht4x.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
#include "sht4x.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sht4x {
|
||||
|
||||
static const char *TAG = "sht4x";
|
||||
|
||||
static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0};
|
||||
|
||||
void SHT4XComponent::start_heater_() {
|
||||
uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]};
|
||||
|
||||
ESP_LOGD(TAG, "Heater turning on");
|
||||
this->write_bytes_raw(cmd, 1);
|
||||
}
|
||||
|
||||
void SHT4XComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up sht4x...");
|
||||
|
||||
if (this->duty_cycle_ > 0.0) {
|
||||
uint32_t heater_interval = (uint32_t)(this->heater_time_ / this->duty_cycle_);
|
||||
ESP_LOGD(TAG, "Heater interval: %i", heater_interval);
|
||||
|
||||
if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) {
|
||||
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
|
||||
this->heater_command_ = 0x39;
|
||||
} else {
|
||||
this->heater_command_ = 0x32;
|
||||
}
|
||||
} else if (this->heater_power_ == SHT4X_HEATERPOWER_MED) {
|
||||
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
|
||||
this->heater_command_ = 0x2F;
|
||||
} else {
|
||||
this->heater_command_ = 0x24;
|
||||
}
|
||||
} else {
|
||||
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
|
||||
this->heater_command_ = 0x1E;
|
||||
} else {
|
||||
this->heater_command_ = 0x15;
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Heater command: %x", this->heater_command_);
|
||||
|
||||
this->set_interval(heater_interval, std::bind(&SHT4XComponent::start_heater_, this));
|
||||
}
|
||||
}
|
||||
|
||||
void SHT4XComponent::dump_config() { LOG_I2C_DEVICE(this); }
|
||||
|
||||
void SHT4XComponent::update() {
|
||||
uint8_t cmd[] = {MEASURECOMMANDS[this->precision_]};
|
||||
|
||||
// Send command
|
||||
this->write_bytes_raw(cmd, 1);
|
||||
|
||||
this->set_timeout(10, [this]() {
|
||||
const uint8_t num_bytes = 6;
|
||||
uint8_t buffer[num_bytes];
|
||||
|
||||
// Read measurement
|
||||
bool read_status = this->read_bytes_raw(buffer, num_bytes);
|
||||
|
||||
if (read_status) {
|
||||
// Evaluate and publish measurements
|
||||
if (this->temp_sensor_ != nullptr) {
|
||||
// Temp is contained in the first 16 bits
|
||||
float sensor_value_temp = (buffer[0] << 8) + buffer[1];
|
||||
float temp = -45 + 175 * sensor_value_temp / 65535;
|
||||
|
||||
this->temp_sensor_->publish_state(temp);
|
||||
}
|
||||
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
// Relative humidity is in the last 16 bits
|
||||
float sensor_value_rh = (buffer[3] << 8) + buffer[4];
|
||||
float rh = -6 + 125 * sensor_value_rh / 65535;
|
||||
|
||||
this->humidity_sensor_->publish_state(rh);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Sensor read failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace sht4x
|
||||
} // namespace esphome
|
||||
45
esphome/components/sht4x/sht4x.h
Normal file
45
esphome/components/sht4x/sht4x.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sht4x {
|
||||
|
||||
enum SHT4XPRECISION { SHT4X_PRECISION_HIGH = 0, SHT4X_PRECISION_MED, SHT4X_PRECISION_LOW };
|
||||
|
||||
enum SHT4XHEATERPOWER { SHT4X_HEATERPOWER_HIGH, SHT4X_HEATERPOWER_MED, SHT4X_HEATERPOWER_LOW };
|
||||
|
||||
enum SHT4XHEATERTIME { SHT4X_HEATERTIME_LONG = 1100, SHT4X_HEATERTIME_SHORT = 110 };
|
||||
|
||||
class SHT4XComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
void set_precision_value(SHT4XPRECISION precision) { this->precision_ = precision; };
|
||||
void set_heater_power_value(SHT4XHEATERPOWER heater_power) { this->heater_power_ = heater_power; };
|
||||
void set_heater_time_value(SHT4XHEATERTIME heater_time) { this->heater_time_ = heater_time; };
|
||||
void set_heater_duty_value(float duty_cycle) { this->duty_cycle_ = duty_cycle; };
|
||||
|
||||
void set_temp_sensor(sensor::Sensor *temp_sensor) { this->temp_sensor_ = temp_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
|
||||
protected:
|
||||
SHT4XPRECISION precision_;
|
||||
SHT4XHEATERPOWER heater_power_;
|
||||
SHT4XHEATERTIME heater_time_;
|
||||
float duty_cycle_;
|
||||
|
||||
void start_heater_();
|
||||
uint8_t heater_command_;
|
||||
|
||||
sensor::Sensor *temp_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace sht4x
|
||||
} // namespace esphome
|
||||
@@ -1,8 +1,16 @@
|
||||
import re
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome.components import time
|
||||
from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID
|
||||
from esphome.const import (
|
||||
CONF_TIME_ID,
|
||||
CONF_ID,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
sun_ns = cg.esphome_ns.namespace("sun")
|
||||
@@ -14,15 +22,13 @@ SunTrigger = sun_ns.class_(
|
||||
SunCondition = sun_ns.class_("SunCondition", automation.Condition)
|
||||
|
||||
CONF_SUN_ID = "sun_id"
|
||||
CONF_LATITUDE = "latitude"
|
||||
CONF_LONGITUDE = "longitude"
|
||||
CONF_ELEVATION = "elevation"
|
||||
CONF_ON_SUNRISE = "on_sunrise"
|
||||
CONF_ON_SUNSET = "on_sunset"
|
||||
|
||||
# Default sun elevation is a bit below horizon because sunset
|
||||
# means time when the entire sun disk is below the horizon
|
||||
DEFAULT_ELEVATION = -0.883
|
||||
DEFAULT_ELEVATION = -0.83333
|
||||
|
||||
ELEVATION_MAP = {
|
||||
"sunrise": 0.0,
|
||||
@@ -45,12 +51,54 @@ def elevation(value):
|
||||
return cv.float_range(min=-180, max=180)(value)
|
||||
|
||||
|
||||
# Parses sexagesimal values like 22°57′7″S
|
||||
LAT_LON_REGEX = re.compile(
|
||||
r"([+\-])?\s*"
|
||||
r"(?:([0-9]+)\s*°)?\s*"
|
||||
r"(?:([0-9]+)\s*[′\'])?\s*"
|
||||
r'(?:([0-9]+)\s*[″"])?\s*'
|
||||
r"([NESW])?"
|
||||
)
|
||||
|
||||
|
||||
def parse_latlon(value):
|
||||
if isinstance(value, str) and value.endswith("°"):
|
||||
# strip trailing degree character
|
||||
value = value[:-1]
|
||||
try:
|
||||
return cv.float_(value)
|
||||
except cv.Invalid:
|
||||
pass
|
||||
|
||||
value = cv.string_strict(value)
|
||||
m = LAT_LON_REGEX.match(value)
|
||||
|
||||
if m is None:
|
||||
raise cv.Invalid("Invalid format for latitude/longitude")
|
||||
sign = m.group(1)
|
||||
deg = m.group(2)
|
||||
minute = m.group(3)
|
||||
second = m.group(4)
|
||||
d = m.group(5)
|
||||
|
||||
val = float(deg or 0) + float(minute or 0) / 60 + float(second or 0) / 3600
|
||||
if sign == "-":
|
||||
val *= -1
|
||||
if d and d in "SW":
|
||||
val *= -1
|
||||
return val
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Sun),
|
||||
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
cv.Required(CONF_LATITUDE): cv.float_range(min=-90, max=90),
|
||||
cv.Required(CONF_LONGITUDE): cv.float_range(min=-180, max=180),
|
||||
cv.Required(CONF_LATITUDE): cv.All(
|
||||
parse_latlon, cv.float_range(min=-90, max=90)
|
||||
),
|
||||
cv.Required(CONF_LONGITUDE): cv.All(
|
||||
parse_latlon, cv.float_range(min=-180, max=180)
|
||||
),
|
||||
cv.Optional(CONF_ON_SUNRISE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger),
|
||||
|
||||
@@ -1,176 +1,319 @@
|
||||
#include "sun.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
/*
|
||||
The formulas/algorithms in this module are based on the book
|
||||
"Astronomical algorithms" by Jean Meeus (2nd edition)
|
||||
|
||||
The target accuracy of this implementation is ~1min for sunrise/sunset calculations,
|
||||
and 6 arcminutes for elevation/azimuth. As such, some of the advanced correction factors
|
||||
like exact nutation are not included. But in some testing the accuracy appears to be within range
|
||||
for random spots around the globe.
|
||||
*/
|
||||
|
||||
namespace esphome {
|
||||
namespace sun {
|
||||
|
||||
using namespace esphome::sun::internal;
|
||||
|
||||
static const char *TAG = "sun";
|
||||
|
||||
#undef PI
|
||||
#undef degrees
|
||||
#undef radians
|
||||
#undef sq
|
||||
|
||||
/* Usually, ESPHome uses single-precision floating point values
|
||||
* because those tend to be accurate enough and are more efficient.
|
||||
*
|
||||
* However, some of the data in this class has to be quite accurate, so double is
|
||||
* used everywhere.
|
||||
*/
|
||||
static const double PI = 3.141592653589793;
|
||||
static const double TAU = 6.283185307179586;
|
||||
static const double TO_RADIANS = PI / 180.0;
|
||||
static const double TO_DEGREES = 180.0 / PI;
|
||||
static const double EARTH_TILT = 23.44 * TO_RADIANS;
|
||||
static const num_t PI = 3.141592653589793;
|
||||
inline num_t degrees(num_t rad) { return rad * 180 / PI; }
|
||||
inline num_t radians(num_t deg) { return deg * PI / 180; }
|
||||
inline num_t arcdeg(num_t deg, num_t minutes, num_t seconds) { return deg + minutes / 60 + seconds / 3600; }
|
||||
inline num_t sq(num_t x) { return x * x; }
|
||||
inline num_t cb(num_t x) { return x * x * x; }
|
||||
|
||||
optional<time::ESPTime> Sun::sunrise(double elevation) {
|
||||
auto time = this->time_->now();
|
||||
if (!time.is_valid())
|
||||
return {};
|
||||
double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, true);
|
||||
if (isnan(sun_time))
|
||||
return {};
|
||||
uint32_t epoch = this->calc_epoch_(time, sun_time);
|
||||
return time::ESPTime::from_epoch_local(epoch);
|
||||
}
|
||||
optional<time::ESPTime> Sun::sunset(double elevation) {
|
||||
auto time = this->time_->now();
|
||||
if (!time.is_valid())
|
||||
return {};
|
||||
double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, false);
|
||||
if (isnan(sun_time))
|
||||
return {};
|
||||
uint32_t epoch = this->calc_epoch_(time, sun_time);
|
||||
return time::ESPTime::from_epoch_local(epoch);
|
||||
}
|
||||
double Sun::elevation() {
|
||||
auto time = this->current_sun_time_();
|
||||
if (isnan(time))
|
||||
return NAN;
|
||||
return this->elevation_(time);
|
||||
}
|
||||
double Sun::azimuth() {
|
||||
auto time = this->current_sun_time_();
|
||||
if (isnan(time))
|
||||
return NAN;
|
||||
return this->azimuth_(time);
|
||||
}
|
||||
// like clamp, but with doubles
|
||||
double clampd(double val, double min, double max) {
|
||||
if (val < min)
|
||||
return min;
|
||||
if (val > max)
|
||||
return max;
|
||||
return val;
|
||||
}
|
||||
double Sun::sun_declination_(double sun_time) {
|
||||
double n = sun_time - 1.0;
|
||||
// maximum declination
|
||||
const double tot = -sin(EARTH_TILT);
|
||||
num_t GeoLocation::latitude_rad() const { return radians(latitude); }
|
||||
num_t GeoLocation::longitude_rad() const { return radians(longitude); }
|
||||
num_t EquatorialCoordinate::right_ascension_rad() const { return radians(right_ascension); }
|
||||
num_t EquatorialCoordinate::declination_rad() const { return radians(declination); }
|
||||
num_t HorizontalCoordinate::elevation_rad() const { return radians(elevation); }
|
||||
num_t HorizontalCoordinate::azimuth_rad() const { return radians(azimuth); }
|
||||
|
||||
// eccentricity of the earth's orbit (ellipse)
|
||||
double eccentricity = 0.0167;
|
||||
|
||||
// days since perihelion (January 3rd)
|
||||
double days_since_perihelion = n - 2;
|
||||
// days since december solstice (december 22)
|
||||
double days_since_december_solstice = n + 10;
|
||||
const double c = TAU / 365.24;
|
||||
double v = cos(c * days_since_december_solstice + 2 * eccentricity * sin(c * days_since_perihelion));
|
||||
// Make sure value is in range (double error may lead to results slightly larger than 1)
|
||||
double x = clampd(tot * v, -1.0, 1.0);
|
||||
return asin(x);
|
||||
num_t julian_day(time::ESPTime moment) {
|
||||
// p. 59
|
||||
// UT -> JD, TT -> JDE
|
||||
int y = moment.year;
|
||||
int m = moment.month;
|
||||
num_t d = moment.day_of_month;
|
||||
d += moment.hour / 24.0;
|
||||
d += moment.minute / (24.0 * 60);
|
||||
d += moment.second / (24.0 * 60 * 60);
|
||||
if (m <= 2) {
|
||||
y -= 1;
|
||||
m += 12;
|
||||
}
|
||||
int a = y / 100;
|
||||
int b = 2 - a + a / 4;
|
||||
return ((int) (365.25 * (y + 4716))) + ((int) (30.6001 * (m + 1))) + d + b - 1524.5;
|
||||
}
|
||||
double Sun::elevation_ratio_(double sun_time) {
|
||||
double decl = this->sun_declination_(sun_time);
|
||||
double hangle = this->hour_angle_(sun_time);
|
||||
double a = sin(this->latitude_rad_()) * sin(decl);
|
||||
double b = cos(this->latitude_rad_()) * cos(decl) * cos(hangle);
|
||||
double val = clampd(a + b, -1.0, 1.0);
|
||||
return val;
|
||||
num_t delta_t(time::ESPTime moment) {
|
||||
// approximation for 2005-2050 from NASA (https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html)
|
||||
int t = moment.year - 2000;
|
||||
return 62.92 + t * (0.32217 + t * 0.005589);
|
||||
}
|
||||
double Sun::latitude_rad_() { return this->latitude_ * TO_RADIANS; }
|
||||
double Sun::hour_angle_(double sun_time) {
|
||||
double time_of_day = fmod(sun_time, 1.0) * 24.0;
|
||||
return -PI * (time_of_day - 12) / 12;
|
||||
// Perform a fractional module operation where the result will always be positive (wrapping around)
|
||||
num_t wmod(num_t x, num_t y) {
|
||||
num_t res = fmod(x, y);
|
||||
if (res < 0)
|
||||
res += y;
|
||||
return res;
|
||||
}
|
||||
double Sun::elevation_(double sun_time) { return this->elevation_rad_(sun_time) * TO_DEGREES; }
|
||||
double Sun::elevation_rad_(double sun_time) { return asin(this->elevation_ratio_(sun_time)); }
|
||||
double Sun::zenith_rad_(double sun_time) { return acos(this->elevation_ratio_(sun_time)); }
|
||||
double Sun::azimuth_rad_(double sun_time) {
|
||||
double hangle = -this->hour_angle_(sun_time);
|
||||
double decl = this->sun_declination_(sun_time);
|
||||
double zen = this->zenith_rad_(sun_time);
|
||||
double nom = cos(zen) * sin(this->latitude_rad_()) - sin(decl);
|
||||
double denom = sin(zen) * cos(this->latitude_rad_());
|
||||
double v = clampd(nom / denom, -1.0, 1.0);
|
||||
double az = PI - acos(v);
|
||||
if (hangle > 0)
|
||||
az = -az;
|
||||
if (az < 0)
|
||||
az += TAU;
|
||||
return az;
|
||||
|
||||
num_t internal::Moment::jd() const { return julian_day(dt); }
|
||||
|
||||
num_t internal::Moment::jde() const {
|
||||
// dt is in UT1, but JDE is based on TT
|
||||
// so add deltaT factor
|
||||
return jd() + delta_t(dt) / (60 * 60 * 24);
|
||||
}
|
||||
double Sun::azimuth_(double sun_time) { return this->azimuth_rad_(sun_time) * TO_DEGREES; }
|
||||
double Sun::calc_sun_time_(const time::ESPTime &time) {
|
||||
// Time as seen at 0° longitude
|
||||
if (!time.is_valid())
|
||||
return NAN;
|
||||
|
||||
double base = (time.day_of_year + time.hour / 24.0 + time.minute / 24.0 / 60.0 + time.second / 24.0 / 60.0 / 60.0);
|
||||
// Add longitude correction
|
||||
double add = this->longitude_ / 360.0;
|
||||
return base + add;
|
||||
}
|
||||
uint32_t Sun::calc_epoch_(time::ESPTime base, double sun_time) {
|
||||
sun_time -= this->longitude_ / 360.0;
|
||||
base.day_of_year = uint32_t(floor(sun_time));
|
||||
struct SunAtTime {
|
||||
num_t jde;
|
||||
num_t t;
|
||||
|
||||
sun_time = (sun_time - base.day_of_year) * 24.0;
|
||||
base.hour = uint32_t(floor(sun_time));
|
||||
|
||||
sun_time = (sun_time - base.hour) * 60.0;
|
||||
base.minute = uint32_t(floor(sun_time));
|
||||
|
||||
sun_time = (sun_time - base.minute) * 60.0;
|
||||
base.second = uint32_t(floor(sun_time));
|
||||
|
||||
base.recalc_timestamp_utc(true);
|
||||
return base.timestamp;
|
||||
}
|
||||
double Sun::sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising) {
|
||||
// Use binary search, newton's method would be better but binary search already
|
||||
// converges quite well (19 cycles) and much simpler. Function is guaranteed to be
|
||||
// monotonous.
|
||||
double lo, hi;
|
||||
if (rising) {
|
||||
lo = day_of_year + 0.0;
|
||||
hi = day_of_year + 0.5;
|
||||
} else {
|
||||
lo = day_of_year + 1.0;
|
||||
hi = day_of_year + 0.5;
|
||||
SunAtTime(num_t jde) : jde(jde) {
|
||||
// eq 25.1, p. 163; julian centuries from the epoch J2000.0
|
||||
t = (jde - 2451545) / 36525.0;
|
||||
}
|
||||
|
||||
double min_elevation = this->elevation_(lo);
|
||||
double max_elevation = this->elevation_(hi);
|
||||
if (elevation < min_elevation || elevation > max_elevation)
|
||||
return NAN;
|
||||
|
||||
// Accuracy: 0.1s
|
||||
const double accuracy = 1.0 / (24.0 * 60.0 * 60.0 * 10.0);
|
||||
|
||||
while (fabs(hi - lo) > accuracy) {
|
||||
double mid = (lo + hi) / 2.0;
|
||||
double value = this->elevation_(mid) - elevation;
|
||||
if (value < 0) {
|
||||
lo = mid;
|
||||
} else if (value > 0) {
|
||||
hi = mid;
|
||||
} else {
|
||||
lo = hi = mid;
|
||||
break;
|
||||
}
|
||||
num_t mean_obliquity() const {
|
||||
// eq. 22.2, p. 147; mean obliquity of the ecliptic
|
||||
num_t epsilon_0 = (+arcdeg(23, 26, 21.448) - arcdeg(0, 0, 46.8150) * t - arcdeg(0, 0, 0.00059) * sq(t) +
|
||||
arcdeg(0, 0, 0.001813) * cb(t));
|
||||
return epsilon_0;
|
||||
}
|
||||
|
||||
return (lo + hi) / 2.0;
|
||||
num_t omega() const {
|
||||
// eq. 25.8, p. 165; correction factor for obliquity of the ecliptic
|
||||
// in degrees
|
||||
num_t omega = 125.05 - 1934.136 * t;
|
||||
return omega;
|
||||
}
|
||||
|
||||
num_t true_obliquity() const {
|
||||
// eq. 25.8, p. 165; correction factor for obliquity of the ecliptic
|
||||
num_t delta_epsilon = 0.00256 * cos(radians(omega()));
|
||||
num_t epsilon = mean_obliquity() + delta_epsilon;
|
||||
return epsilon;
|
||||
}
|
||||
|
||||
num_t mean_longitude() const {
|
||||
// eq 25.2, p. 163; geometric mean longitude = mean equinox of the date in degrees
|
||||
num_t l0 = 280.46646 + 36000.76983 * t + 0.0003032 * sq(t);
|
||||
return wmod(l0, 360);
|
||||
}
|
||||
|
||||
num_t eccentricity() const {
|
||||
// eq 25.4, p. 163; eccentricity of earth's orbit
|
||||
num_t e = 0.016708634 - 0.000042037 * t - 0.0000001267 * sq(t);
|
||||
return e;
|
||||
}
|
||||
|
||||
num_t mean_anomaly() const {
|
||||
// eq 25.3, p. 163; mean anomaly of the sun in degrees
|
||||
num_t m = 357.52911 + 35999.05029 * t - 0.0001537 * sq(t);
|
||||
return wmod(m, 360);
|
||||
}
|
||||
|
||||
num_t equation_of_center() const {
|
||||
// p. 164; sun's equation of the center c in degrees
|
||||
num_t m_rad = radians(mean_anomaly());
|
||||
num_t c = ((1.914602 - 0.004817 * t - 0.000014 * sq(t)) * sin(m_rad) + (0.019993 - 0.000101 * t) * sin(2 * m_rad) +
|
||||
0.000289 * sin(3 * m_rad));
|
||||
return wmod(c, 360);
|
||||
}
|
||||
|
||||
num_t true_longitude() const {
|
||||
// p. 164; sun's true longitude in degrees
|
||||
num_t x = mean_longitude() + equation_of_center();
|
||||
return wmod(x, 360);
|
||||
}
|
||||
|
||||
num_t true_anomaly() const {
|
||||
// p. 164; sun's true anomaly in degrees
|
||||
num_t x = mean_anomaly() + equation_of_center();
|
||||
return wmod(x, 360);
|
||||
}
|
||||
|
||||
num_t apparent_longitude() const {
|
||||
// p. 164; sun's apparent longitude = true equinox in degrees
|
||||
num_t x = true_longitude() - 0.00569 - 0.00478 * sin(radians(omega()));
|
||||
return wmod(x, 360);
|
||||
}
|
||||
|
||||
EquatorialCoordinate equatorial_coordinate() const {
|
||||
num_t epsilon_rad = radians(true_obliquity());
|
||||
// eq. 25.6; p. 165; sun's right ascension alpha
|
||||
num_t app_lon_rad = radians(apparent_longitude());
|
||||
num_t right_ascension_rad = atan2(cos(epsilon_rad) * sin(app_lon_rad), cos(app_lon_rad));
|
||||
num_t declination_rad = asin(sin(epsilon_rad) * sin(app_lon_rad));
|
||||
return EquatorialCoordinate{degrees(right_ascension_rad), degrees(declination_rad)};
|
||||
}
|
||||
|
||||
num_t equation_of_time() const {
|
||||
// chapter 28, p. 185
|
||||
num_t epsilon_half = radians(true_obliquity() / 2);
|
||||
num_t y = sq(tan(epsilon_half));
|
||||
num_t l2 = 2 * mean_longitude();
|
||||
num_t l2_rad = radians(l2);
|
||||
num_t e = eccentricity();
|
||||
num_t m = mean_anomaly();
|
||||
num_t m_rad = radians(m);
|
||||
num_t sin_m = sin(m_rad);
|
||||
num_t eot = (y * sin(l2_rad) - 2 * e * sin_m + 4 * e * y * sin_m * cos(l2_rad) - 1 / 2.0 * sq(y) * sin(2 * l2_rad) -
|
||||
5 / 4.0 * sq(e) * sin(2 * m_rad));
|
||||
return degrees(eot);
|
||||
}
|
||||
|
||||
void debug() const {
|
||||
// debug output like in example 25.a, p. 165
|
||||
ESP_LOGV(TAG, "jde: %f", jde);
|
||||
ESP_LOGV(TAG, "T: %f", t);
|
||||
ESP_LOGV(TAG, "L_0: %f", mean_longitude());
|
||||
ESP_LOGV(TAG, "M: %f", mean_anomaly());
|
||||
ESP_LOGV(TAG, "e: %f", eccentricity());
|
||||
ESP_LOGV(TAG, "C: %f", equation_of_center());
|
||||
ESP_LOGV(TAG, "Odot: %f", true_longitude());
|
||||
ESP_LOGV(TAG, "Omega: %f", omega());
|
||||
ESP_LOGV(TAG, "lambda: %f", apparent_longitude());
|
||||
ESP_LOGV(TAG, "epsilon_0: %f", mean_obliquity());
|
||||
ESP_LOGV(TAG, "epsilon: %f", true_obliquity());
|
||||
ESP_LOGV(TAG, "v: %f", true_anomaly());
|
||||
auto eq = equatorial_coordinate();
|
||||
ESP_LOGV(TAG, "right_ascension: %f", eq.right_ascension);
|
||||
ESP_LOGV(TAG, "declination: %f", eq.declination);
|
||||
}
|
||||
};
|
||||
|
||||
struct SunAtLocation {
|
||||
GeoLocation location;
|
||||
|
||||
num_t greenwich_sidereal_time(Moment moment) const {
|
||||
// Return the greenwich mean sidereal time for this instant in degrees
|
||||
// see chapter 12, p. 87
|
||||
num_t jd = moment.jd();
|
||||
// eq 12.1, p.87; jd for 0h UT of this date
|
||||
time::ESPTime moment_0h = moment.dt;
|
||||
moment_0h.hour = moment_0h.minute = moment_0h.second = 0;
|
||||
num_t jd0 = Moment{moment_0h}.jd();
|
||||
num_t t = (jd0 - 2451545) / 36525;
|
||||
// eq. 12.4, p.88
|
||||
num_t gmst = (+280.46061837 + 360.98564736629 * (jd - 2451545) + 0.000387933 * sq(t) - (1 / 38710000.0) * cb(t));
|
||||
return wmod(gmst, 360);
|
||||
}
|
||||
|
||||
HorizontalCoordinate true_coordinate(Moment moment) const {
|
||||
auto eq = SunAtTime(moment.jde()).equatorial_coordinate();
|
||||
num_t gmst = greenwich_sidereal_time(moment);
|
||||
// do not apply any nutation correction (not important for our target accuracy)
|
||||
num_t nutation_corr = 0;
|
||||
|
||||
num_t ra = eq.right_ascension;
|
||||
num_t alpha = gmst + nutation_corr + location.longitude - ra;
|
||||
alpha = wmod(alpha, 360);
|
||||
num_t alpha_rad = radians(alpha);
|
||||
|
||||
num_t sin_lat = sin(location.latitude_rad());
|
||||
num_t cos_lat = cos(location.latitude_rad());
|
||||
num_t sin_elevation = (+sin_lat * sin(eq.declination_rad()) + cos_lat * cos(eq.declination_rad()) * cos(alpha_rad));
|
||||
num_t elevation_rad = asin(sin_elevation);
|
||||
num_t azimuth_rad = atan2(sin(alpha_rad), cos(alpha_rad) * sin_lat - tan(eq.declination_rad()) * cos_lat);
|
||||
return HorizontalCoordinate{degrees(elevation_rad), degrees(azimuth_rad) + 180};
|
||||
}
|
||||
|
||||
optional<time::ESPTime> sunrise(time::ESPTime date, num_t zenith) const { return event(true, date, zenith); }
|
||||
optional<time::ESPTime> sunset(time::ESPTime date, num_t zenith) const { return event(false, date, zenith); }
|
||||
optional<time::ESPTime> event(bool rise, time::ESPTime date, num_t zenith) const {
|
||||
// couldn't get the method described in chapter 15 to work,
|
||||
// so instead this is based on the algorithm in time4j
|
||||
// https://github.com/MenoData/Time4J/blob/master/base/src/main/java/net/time4j/calendar/astro/StdSolarCalculator.java
|
||||
auto m = local_event_(date, 12); // noon
|
||||
num_t jde = julian_day(m);
|
||||
num_t new_h = 0, old_h;
|
||||
do {
|
||||
old_h = new_h;
|
||||
auto x = local_hour_angle_(jde + old_h / 86400, rise, zenith);
|
||||
if (!x.has_value())
|
||||
return {};
|
||||
new_h = *x;
|
||||
} while (std::abs(new_h - old_h) >= 15);
|
||||
time_t new_timestamp = m.timestamp + (time_t) new_h;
|
||||
return time::ESPTime::from_epoch_local(new_timestamp);
|
||||
}
|
||||
|
||||
protected:
|
||||
optional<num_t> local_hour_angle_(num_t jde, bool rise, num_t zenith) const {
|
||||
auto pos = SunAtTime(jde).equatorial_coordinate();
|
||||
num_t dec_rad = pos.declination_rad();
|
||||
num_t lat_rad = location.latitude_rad();
|
||||
num_t num = cos(radians(zenith)) - (sin(dec_rad) * sin(lat_rad));
|
||||
num_t denom = cos(dec_rad) * cos(lat_rad);
|
||||
num_t cos_h = num / denom;
|
||||
if (cos_h > 1 || cos_h < -1)
|
||||
return {};
|
||||
num_t hour_angle = degrees(acos(cos_h)) * 240;
|
||||
if (rise)
|
||||
hour_angle *= -1;
|
||||
return hour_angle;
|
||||
}
|
||||
|
||||
time::ESPTime local_event_(time::ESPTime date, int hour) const {
|
||||
// input date should be in UTC, and hour/minute/second fields 0
|
||||
num_t added_d = hour / 24.0 - location.longitude / 360;
|
||||
num_t jd = julian_day(date) + added_d;
|
||||
|
||||
num_t eot = SunAtTime(jd).equation_of_time() * 240;
|
||||
time_t new_timestamp = (time_t)(date.timestamp + added_d * 86400 - eot);
|
||||
return time::ESPTime::from_epoch_utc(new_timestamp);
|
||||
}
|
||||
};
|
||||
|
||||
HorizontalCoordinate Sun::calc_coords_() {
|
||||
SunAtLocation sun{location_};
|
||||
Moment m{time_->utcnow()};
|
||||
if (!m.dt.is_valid())
|
||||
return HorizontalCoordinate{NAN, NAN};
|
||||
|
||||
// uncomment to print some debug output
|
||||
/*
|
||||
SunAtTime st{m.jde()};
|
||||
st.debug();
|
||||
*/
|
||||
return sun.true_coordinate(m);
|
||||
}
|
||||
optional<time::ESPTime> Sun::calc_event_(bool rising, double zenith) {
|
||||
SunAtLocation sun{location_};
|
||||
auto now = this->time_->utcnow();
|
||||
if (!now.is_valid())
|
||||
return {};
|
||||
// Calculate UT1 timestamp at 0h
|
||||
auto today = now;
|
||||
today.hour = today.minute = today.second = 0;
|
||||
today.recalc_timestamp_utc();
|
||||
|
||||
auto it = sun.event(rising, today, zenith);
|
||||
if (it.has_value() && it->timestamp < now.timestamp) {
|
||||
// We're calculating *next* sunrise/sunset, but calculated event
|
||||
// is today, so try again tomorrow
|
||||
time_t new_timestamp = today.timestamp + 24 * 60 * 60;
|
||||
today = time::ESPTime::from_epoch_utc(new_timestamp);
|
||||
it = sun.event(rising, today, zenith);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
optional<time::ESPTime> Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); }
|
||||
optional<time::ESPTime> Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); }
|
||||
double Sun::elevation() { return this->calc_coords_().elevation; }
|
||||
double Sun::azimuth() { return this->calc_coords_().azimuth; }
|
||||
|
||||
} // namespace sun
|
||||
} // namespace esphome
|
||||
|
||||
@@ -8,85 +8,72 @@
|
||||
namespace esphome {
|
||||
namespace sun {
|
||||
|
||||
namespace internal {
|
||||
|
||||
/* Usually, ESPHome uses single-precision floating point values
|
||||
* because those tend to be accurate enough and are more efficient.
|
||||
*
|
||||
* However, some of the data in this class has to be quite accurate, so double is
|
||||
* used everywhere.
|
||||
*/
|
||||
using num_t = double;
|
||||
struct GeoLocation {
|
||||
num_t latitude;
|
||||
num_t longitude;
|
||||
|
||||
num_t latitude_rad() const;
|
||||
num_t longitude_rad() const;
|
||||
};
|
||||
|
||||
struct Moment {
|
||||
time::ESPTime dt;
|
||||
|
||||
num_t jd() const;
|
||||
num_t jde() const;
|
||||
};
|
||||
|
||||
struct EquatorialCoordinate {
|
||||
num_t right_ascension;
|
||||
num_t declination;
|
||||
|
||||
num_t right_ascension_rad() const;
|
||||
num_t declination_rad() const;
|
||||
};
|
||||
|
||||
struct HorizontalCoordinate {
|
||||
num_t elevation;
|
||||
num_t azimuth;
|
||||
|
||||
num_t elevation_rad() const;
|
||||
num_t azimuth_rad() const;
|
||||
};
|
||||
|
||||
} // namespace internal
|
||||
|
||||
class Sun {
|
||||
public:
|
||||
void set_time(time::RealTimeClock *time) { time_ = time; }
|
||||
time::RealTimeClock *get_time() const { return time_; }
|
||||
void set_latitude(double latitude) { latitude_ = latitude; }
|
||||
void set_longitude(double longitude) { longitude_ = longitude; }
|
||||
void set_latitude(double latitude) { location_.latitude = latitude; }
|
||||
void set_longitude(double longitude) { location_.longitude = longitude; }
|
||||
|
||||
optional<time::ESPTime> sunrise(double elevation = 0.0);
|
||||
optional<time::ESPTime> sunset(double elevation = 0.0);
|
||||
optional<time::ESPTime> sunrise(double elevation);
|
||||
optional<time::ESPTime> sunset(double elevation);
|
||||
|
||||
double elevation();
|
||||
double azimuth();
|
||||
|
||||
protected:
|
||||
double current_sun_time_() { return this->calc_sun_time_(this->time_->utcnow()); }
|
||||
|
||||
/** Calculate the declination of the sun in rad.
|
||||
*
|
||||
* See https://en.wikipedia.org/wiki/Position_of_the_Sun#Declination_of_the_Sun_as_seen_from_Earth
|
||||
*
|
||||
* Accuracy: ±0.2°
|
||||
*
|
||||
* @param sun_time The day of the year, 1 means January 1st. See calc_sun_time_.
|
||||
* @return Sun declination in degrees
|
||||
*/
|
||||
double sun_declination_(double sun_time);
|
||||
|
||||
double elevation_ratio_(double sun_time);
|
||||
|
||||
/** Calculate the hour angle based on the sun time of day in hours.
|
||||
*
|
||||
* Positive in morning, 0 at noon, negative in afternoon.
|
||||
*
|
||||
* @param sun_time Sun time, see calc_sun_time_.
|
||||
* @return Hour angle in rad.
|
||||
*/
|
||||
double hour_angle_(double sun_time);
|
||||
|
||||
double elevation_(double sun_time);
|
||||
|
||||
double elevation_rad_(double sun_time);
|
||||
|
||||
double zenith_rad_(double sun_time);
|
||||
|
||||
double azimuth_rad_(double sun_time);
|
||||
|
||||
double azimuth_(double sun_time);
|
||||
|
||||
/** Return the sun time given by the time_ object.
|
||||
*
|
||||
* Sun time is defined as doubleing point day of year.
|
||||
* Integer part encodes the day of the year (1=January 1st)
|
||||
* Decimal part encodes time of day (1/24 = 1 hour)
|
||||
*/
|
||||
double calc_sun_time_(const time::ESPTime &time);
|
||||
|
||||
uint32_t calc_epoch_(time::ESPTime base, double sun_time);
|
||||
|
||||
/** Calculate the sun time of day
|
||||
*
|
||||
* @param day_of_year
|
||||
* @param elevation
|
||||
* @param rising
|
||||
* @return
|
||||
*/
|
||||
double sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising);
|
||||
|
||||
double latitude_rad_();
|
||||
internal::HorizontalCoordinate calc_coords_();
|
||||
optional<time::ESPTime> calc_event_(bool rising, double zenith);
|
||||
|
||||
time::RealTimeClock *time_;
|
||||
/// Latitude in degrees, range: -90 to 90.
|
||||
double latitude_;
|
||||
/// Longitude in degrees, range: -180 to 180.
|
||||
double longitude_;
|
||||
internal::GeoLocation location_;
|
||||
};
|
||||
|
||||
class SunTrigger : public Trigger<>, public PollingComponent, public Parented<Sun> {
|
||||
public:
|
||||
SunTrigger() : PollingComponent(1000) {}
|
||||
SunTrigger() : PollingComponent(60000) {}
|
||||
|
||||
void set_sunrise(bool sunrise) { sunrise_ = sunrise; }
|
||||
void set_elevation(double elevation) { elevation_ = elevation; }
|
||||
|
||||
30
esphome/components/tca9548a/__init__.py
Normal file
30
esphome/components/tca9548a/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c
|
||||
from esphome.const import CONF_ID, CONF_SCAN
|
||||
|
||||
CODEOWNERS = ["@andreashergert1984"]
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
tca9548a_ns = cg.esphome_ns.namespace("tca9548a")
|
||||
TCA9548AComponent = tca9548a_ns.class_(
|
||||
"TCA9548AComponent", cg.PollingComponent, i2c.I2CMultiplexer
|
||||
)
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(TCA9548AComponent),
|
||||
cv.Optional(CONF_SCAN, default=True): cv.boolean,
|
||||
}
|
||||
).extend(i2c.i2c_device_schema(0x70))
|
||||
|
||||
|
||||
def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add_define("USE_I2C_MULTIPLEXER")
|
||||
yield cg.register_component(var, config)
|
||||
yield i2c.register_i2c_device(var, config)
|
||||
cg.add(var.set_scan(config[CONF_SCAN]))
|
||||
41
esphome/components/tca9548a/tca9548a.cpp
Normal file
41
esphome/components/tca9548a/tca9548a.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#include "tca9548a.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace tca9548a {
|
||||
|
||||
static const char *TAG = "tca9548a";
|
||||
|
||||
void TCA9548AComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up TCA9548A...");
|
||||
uint8_t status = 0;
|
||||
if (!this->read_byte(0x00, &status)) {
|
||||
ESP_LOGI(TAG, "TCA9548A failed");
|
||||
return;
|
||||
}
|
||||
// out of range to make sure on first set_channel a new one will be set
|
||||
this->current_channelno_ = 8;
|
||||
ESP_LOGCONFIG(TAG, "Channels currently open: %d", status);
|
||||
}
|
||||
void TCA9548AComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "TCA9548A:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->scan_) {
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
ESP_LOGCONFIG(TAG, "Activating channel: %d", i);
|
||||
this->set_channel(i);
|
||||
this->parent_->dump_config();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TCA9548AComponent::set_channel(uint8_t channelno) {
|
||||
if (this->current_channelno_ != channelno) {
|
||||
this->current_channelno_ = channelno;
|
||||
uint8_t channelbyte = 1 << channelno;
|
||||
this->write_byte(0x70, channelbyte);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace tca9548a
|
||||
} // namespace esphome
|
||||
22
esphome/components/tca9548a/tca9548a.h
Normal file
22
esphome/components/tca9548a/tca9548a.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace tca9548a {
|
||||
|
||||
class TCA9548AComponent : public Component, public i2c::I2CMultiplexer {
|
||||
public:
|
||||
void set_scan(bool scan) { scan_ = scan; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update();
|
||||
void set_channel(uint8_t channelno) override;
|
||||
|
||||
protected:
|
||||
bool scan_;
|
||||
uint8_t current_channelno_;
|
||||
};
|
||||
} // namespace tca9548a
|
||||
} // namespace esphome
|
||||
@@ -138,7 +138,7 @@ def convert_tz(pytz_obj):
|
||||
_tz_dst_str(dst_ends_local),
|
||||
)
|
||||
_LOGGER.info(
|
||||
"Detected timezone '%s' with UTC offset %s and daylight savings time from "
|
||||
"Detected timezone '%s' with UTC offset %s and daylight saving time from "
|
||||
"%s to %s",
|
||||
tzname_off,
|
||||
_tz_timedelta(utcoffset_off),
|
||||
|
||||
@@ -30,7 +30,7 @@ struct ESPTime {
|
||||
uint8_t month;
|
||||
/// year
|
||||
uint16_t year;
|
||||
/// daylight savings time flag
|
||||
/// daylight saving time flag
|
||||
bool is_dst;
|
||||
union {
|
||||
ESPDEPRECATED(".time is deprecated, use .timestamp instead") time_t time;
|
||||
|
||||
0
esphome/components/tof10120/__init__.py
Normal file
0
esphome/components/tof10120/__init__.py
Normal file
26
esphome/components/tof10120/sensor.py
Normal file
26
esphome/components/tof10120/sensor.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import CONF_ID, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL
|
||||
|
||||
CODEOWNERS = ["@wstrzalka"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
tof10120_ns = cg.esphome_ns.namespace("tof10120")
|
||||
TOF10120Sensor = tof10120_ns.class_(
|
||||
"TOF10120Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 3)
|
||||
.extend({cv.GenerateID(): cv.declare_id(TOF10120Sensor)})
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x52))
|
||||
)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
yield cg.register_component(var, config)
|
||||
yield sensor.register_sensor(var, config)
|
||||
yield i2c.register_i2c_device(var, config)
|
||||
53
esphome/components/tof10120/tof10120_sensor.cpp
Normal file
53
esphome/components/tof10120/tof10120_sensor.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "tof10120_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
// Very basic support for TOF10120 distance sensor
|
||||
|
||||
namespace esphome {
|
||||
namespace tof10120 {
|
||||
|
||||
static const char *TAG = "tof10120";
|
||||
static const uint8_t TOF10120_READ_DISTANCE_CMD[] = {0x00};
|
||||
static const uint8_t TOF10120_DEFAULT_DELAY = 30;
|
||||
|
||||
static const uint8_t TOF10120_DIR_SEND_REGISTER = 0x0e;
|
||||
static const uint8_t TOF10120_DISTANCE_REGISTER = 0x00;
|
||||
|
||||
static const uint16_t TOF10120_OUT_OF_RANGE_VALUE = 2000;
|
||||
|
||||
void TOF10120Sensor::dump_config() {
|
||||
LOG_SENSOR("", "TOF10120", this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_I2C_DEVICE(this);
|
||||
}
|
||||
|
||||
void TOF10120Sensor::setup() {}
|
||||
|
||||
void TOF10120Sensor::update() {
|
||||
if (!this->write_bytes(TOF10120_DISTANCE_REGISTER, TOF10120_READ_DISTANCE_CMD, sizeof(TOF10120_READ_DISTANCE_CMD))) {
|
||||
ESP_LOGE(TAG, "Communication with TOF10120 failed on write");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t data[2];
|
||||
if (!this->read_bytes(TOF10120_DISTANCE_REGISTER, data, 2, TOF10120_DEFAULT_DELAY)) {
|
||||
ESP_LOGE(TAG, "Communication with TOF10120 failed on read");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t distance_mm = (data[0] << 8) | data[1];
|
||||
ESP_LOGI(TAG, "Data read: %dmm", distance_mm);
|
||||
|
||||
if (distance_mm == TOF10120_OUT_OF_RANGE_VALUE) {
|
||||
ESP_LOGW(TAG, "Distance measurement out of range");
|
||||
this->publish_state(NAN);
|
||||
} else {
|
||||
this->publish_state(distance_mm / 1000.0);
|
||||
}
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
} // namespace tof10120
|
||||
} // namespace esphome
|
||||
19
esphome/components/tof10120/tof10120_sensor.h
Normal file
19
esphome/components/tof10120/tof10120_sensor.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace tof10120 {
|
||||
|
||||
class TOF10120Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void update() override;
|
||||
};
|
||||
} // namespace tof10120
|
||||
} // namespace esphome
|
||||
@@ -1,13 +1,14 @@
|
||||
from esphome.components import fan
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
from esphome.const import CONF_OUTPUT_ID, CONF_SWITCH_DATAPOINT
|
||||
from esphome.const import CONF_OUTPUT_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT
|
||||
from .. import tuya_ns, CONF_TUYA_ID, Tuya
|
||||
|
||||
DEPENDENCIES = ["tuya"]
|
||||
|
||||
CONF_SPEED_DATAPOINT = "speed_datapoint"
|
||||
CONF_OSCILLATION_DATAPOINT = "oscillation_datapoint"
|
||||
CONF_DIRECTION_DATAPOINT = "direction_datapoint"
|
||||
|
||||
TuyaFan = tuya_ns.class_("TuyaFan", cg.Component)
|
||||
|
||||
@@ -19,6 +20,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT),
|
||||
@@ -26,13 +29,13 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||
yield cg.register_component(var, config)
|
||||
parent = yield cg.get_variable(config[CONF_TUYA_ID])
|
||||
state = yield fan.create_fan_state(config)
|
||||
|
||||
paren = yield cg.get_variable(config[CONF_TUYA_ID])
|
||||
fan_ = yield fan.create_fan_state(config)
|
||||
cg.add(var.set_tuya_parent(paren))
|
||||
cg.add(var.set_fan(fan_))
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_OUTPUT_ID], parent, state, config[CONF_SPEED_COUNT]
|
||||
)
|
||||
yield cg.register_component(var, config)
|
||||
|
||||
if CONF_SPEED_DATAPOINT in config:
|
||||
cg.add(var.set_speed_id(config[CONF_SPEED_DATAPOINT]))
|
||||
@@ -40,3 +43,5 @@ def to_code(config):
|
||||
cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT]))
|
||||
if CONF_OSCILLATION_DATAPOINT in config:
|
||||
cg.add(var.set_oscillation_id(config[CONF_OSCILLATION_DATAPOINT]))
|
||||
if CONF_DIRECTION_DATAPOINT in config:
|
||||
cg.add(var.set_direction_id(config[CONF_DIRECTION_DATAPOINT]))
|
||||
|
||||
@@ -8,18 +8,15 @@ namespace tuya {
|
||||
static const char *TAG = "tuya.fan";
|
||||
|
||||
void TuyaFan::setup() {
|
||||
auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(), false, 3);
|
||||
auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(),
|
||||
this->direction_id_.has_value(), this->speed_count_);
|
||||
this->fan_->set_traits(traits);
|
||||
|
||||
if (this->speed_id_.has_value()) {
|
||||
this->parent_->register_listener(*this->speed_id_, [this](TuyaDatapoint datapoint) {
|
||||
auto call = this->fan_->make_call();
|
||||
if (datapoint.value_enum == 0x0)
|
||||
call.set_speed(1);
|
||||
else if (datapoint.value_enum == 0x1)
|
||||
call.set_speed(2);
|
||||
else if (datapoint.value_enum == 0x2)
|
||||
call.set_speed(3);
|
||||
if (datapoint.value_enum < this->speed_count_)
|
||||
call.set_speed(datapoint.value_enum + 1);
|
||||
else
|
||||
ESP_LOGCONFIG(TAG, "Speed has invalid value %d", datapoint.value_enum);
|
||||
ESP_LOGD(TAG, "MCU reported speed of: %d", datapoint.value_enum);
|
||||
@@ -42,17 +39,29 @@ void TuyaFan::setup() {
|
||||
ESP_LOGD(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool));
|
||||
});
|
||||
}
|
||||
if (this->direction_id_.has_value()) {
|
||||
this->parent_->register_listener(*this->direction_id_, [this](TuyaDatapoint datapoint) {
|
||||
auto call = this->fan_->make_call();
|
||||
call.set_direction(datapoint.value_bool ? fan::FAN_DIRECTION_REVERSE : fan::FAN_DIRECTION_FORWARD);
|
||||
call.perform();
|
||||
ESP_LOGD(TAG, "MCU reported reverse direction is: %s", ONOFF(datapoint.value_bool));
|
||||
});
|
||||
}
|
||||
|
||||
this->fan_->add_on_state_callback([this]() { this->write_state(); });
|
||||
}
|
||||
|
||||
void TuyaFan::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Tuya Fan:");
|
||||
ESP_LOGCONFIG(TAG, " Speed count %d", this->speed_count_);
|
||||
if (this->speed_id_.has_value())
|
||||
ESP_LOGCONFIG(TAG, " Speed has datapoint ID %u", *this->speed_id_);
|
||||
if (this->switch_id_.has_value())
|
||||
ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_);
|
||||
if (this->oscillation_id_.has_value())
|
||||
ESP_LOGCONFIG(TAG, " Oscillation has datapoint ID %u", *this->oscillation_id_);
|
||||
if (this->direction_id_.has_value())
|
||||
ESP_LOGCONFIG(TAG, " Direction has datapoint ID %u", *this->direction_id_);
|
||||
}
|
||||
|
||||
void TuyaFan::write_state() {
|
||||
@@ -72,6 +81,15 @@ void TuyaFan::write_state() {
|
||||
this->parent_->set_datapoint_value(datapoint);
|
||||
ESP_LOGD(TAG, "Setting oscillating: %s", ONOFF(this->fan_->oscillating));
|
||||
}
|
||||
if (this->direction_id_.has_value()) {
|
||||
TuyaDatapoint datapoint{};
|
||||
datapoint.id = *this->direction_id_;
|
||||
datapoint.type = TuyaDatapointType::BOOLEAN;
|
||||
bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE;
|
||||
datapoint.value_bool = enable;
|
||||
this->parent_->set_datapoint_value(datapoint);
|
||||
ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable));
|
||||
}
|
||||
if (this->speed_id_.has_value()) {
|
||||
TuyaDatapoint datapoint{};
|
||||
datapoint.id = *this->speed_id_;
|
||||
|
||||
@@ -9,25 +9,28 @@ namespace tuya {
|
||||
|
||||
class TuyaFan : public Component {
|
||||
public:
|
||||
TuyaFan(Tuya *parent, fan::FanState *fan, int speed_count) : parent_(parent), fan_(fan), speed_count_(speed_count) {}
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; }
|
||||
void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; }
|
||||
void set_oscillation_id(uint8_t oscillation_id) { this->oscillation_id_ = oscillation_id; }
|
||||
void set_fan(fan::FanState *fan) { this->fan_ = fan; }
|
||||
void set_tuya_parent(Tuya *parent) { this->parent_ = parent; }
|
||||
void set_direction_id(uint8_t direction_id) { this->direction_id_ = direction_id; }
|
||||
void write_state();
|
||||
|
||||
protected:
|
||||
void update_speed_(uint32_t value);
|
||||
void update_switch_(uint32_t value);
|
||||
void update_oscillation_(uint32_t value);
|
||||
void update_direction_(uint32_t value);
|
||||
|
||||
Tuya *parent_;
|
||||
optional<uint8_t> speed_id_{};
|
||||
optional<uint8_t> switch_id_{};
|
||||
optional<uint8_t> oscillation_id_{};
|
||||
optional<uint8_t> direction_id_{};
|
||||
fan::FanState *fan_;
|
||||
int speed_count_{};
|
||||
};
|
||||
|
||||
} // namespace tuya
|
||||
|
||||
@@ -14,8 +14,8 @@ void ULN2003::setup() {
|
||||
this->loop();
|
||||
}
|
||||
void ULN2003::loop() {
|
||||
bool at_target = this->has_reached_target();
|
||||
if (at_target) {
|
||||
int dir = this->should_step_();
|
||||
if (dir == 0 && this->has_reached_target()) {
|
||||
this->high_freq_.stop();
|
||||
|
||||
if (this->sleep_when_done_) {
|
||||
@@ -28,8 +28,6 @@ void ULN2003::loop() {
|
||||
}
|
||||
} else {
|
||||
this->high_freq_.start();
|
||||
|
||||
int dir = this->should_step_();
|
||||
this->current_uln_pos_ += dir;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
from esphome.components.network import add_mdns_library
|
||||
from esphome.const import (
|
||||
CONF_AP,
|
||||
CONF_BSSID,
|
||||
@@ -22,6 +23,7 @@ from esphome.const import (
|
||||
CONF_STATIC_IP,
|
||||
CONF_SUBNET,
|
||||
CONF_USE_ADDRESS,
|
||||
CONF_ENABLE_MDNS,
|
||||
CONF_PRIORITY,
|
||||
CONF_IDENTITY,
|
||||
CONF_CERTIFICATE_AUTHORITY,
|
||||
@@ -187,6 +189,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
|
||||
cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA,
|
||||
cv.Optional(CONF_AP): WIFI_NETWORK_AP,
|
||||
cv.Optional(CONF_ENABLE_MDNS, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
|
||||
cv.Optional(
|
||||
CONF_REBOOT_TIMEOUT, default="15min"
|
||||
@@ -298,6 +301,9 @@ def to_code(config):
|
||||
|
||||
cg.add_define("USE_WIFI")
|
||||
|
||||
if config[CONF_ENABLE_MDNS]:
|
||||
add_mdns_library()
|
||||
|
||||
# Register at end for OTA safe mode
|
||||
yield cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ void WiFiComponent::setup() {
|
||||
}
|
||||
|
||||
this->wifi_apply_hostname_();
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
#if defined(ARDUINO_ARCH_ESP32) && defined(USE_MDNS)
|
||||
network_setup_mdns();
|
||||
#endif
|
||||
}
|
||||
@@ -171,7 +171,7 @@ void WiFiComponent::setup_ap_config_() {
|
||||
|
||||
this->ap_setup_ = this->wifi_start_ap_(this->ap_);
|
||||
ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip().toString().c_str());
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS)
|
||||
network_setup_mdns(this->wifi_soft_ap_ip(), 1);
|
||||
#endif
|
||||
|
||||
@@ -466,7 +466,7 @@ void WiFiComponent::check_connecting_finished() {
|
||||
ESP_LOGD(TAG, "Disabling AP...");
|
||||
this->wifi_mode_({}, false);
|
||||
}
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS)
|
||||
network_setup_mdns(this->wifi_sta_ip_(), 0);
|
||||
#endif
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
|
||||
|
||||
@@ -5,7 +5,7 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
ICON_EMPTY,
|
||||
UNIT_DECIBEL,
|
||||
UNIT_DECIBEL_MILLIWATT,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["wifi"]
|
||||
@@ -15,7 +15,9 @@ WiFiSignalSensor = wifi_signal_ns.class_(
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(UNIT_DECIBEL, ICON_EMPTY, 0, DEVICE_CLASS_SIGNAL_STRENGTH)
|
||||
sensor.sensor_schema(
|
||||
UNIT_DECIBEL_MILLIWATT, ICON_EMPTY, 0, DEVICE_CLASS_SIGNAL_STRENGTH
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(WiFiSignalSensor),
|
||||
|
||||
@@ -92,8 +92,14 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p
|
||||
|
||||
switch (protocol) {
|
||||
case WLED_NOTIFIER:
|
||||
if (!parse_notifier_frame_(it, payload, size))
|
||||
return false;
|
||||
// Hyperion Port
|
||||
if (port_ == 19446) {
|
||||
if (!parse_drgb_frame_(it, payload, size))
|
||||
return false;
|
||||
} else {
|
||||
if (!parse_notifier_frame_(it, payload, size))
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case WARLS:
|
||||
|
||||
@@ -103,7 +103,7 @@ bool parse_xiaomi_message(const std::vector<uint8_t> &message, XiaomiParseResult
|
||||
return false;
|
||||
}
|
||||
|
||||
while (payload_length > 0) {
|
||||
while (payload_length > 3) {
|
||||
if (payload[payload_offset + 1] != 0x10) {
|
||||
ESP_LOGVV(TAG, "parse_xiaomi_message(): fixed byte not found, stop parsing residual data.");
|
||||
break;
|
||||
@@ -171,7 +171,10 @@ optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::Service
|
||||
result.type = XiaomiParseResult::TYPE_MUE4094RT;
|
||||
result.name = "MUE4094RT";
|
||||
result.raw_offset -= 6;
|
||||
} else if ((raw[2] == 0x47) && (raw[3] == 0x03)) { // round body, e-ink display
|
||||
} else if ((raw[2] == 0x47) && (raw[3] == 0x03)) { // ClearGrass-branded, round body, e-ink display
|
||||
result.type = XiaomiParseResult::TYPE_CGG1;
|
||||
result.name = "CGG1";
|
||||
} else if ((raw[2] == 0x48) && (raw[3] == 0x0B)) { // Qingping-branded, round body, e-ink display — with bindkeys
|
||||
result.type = XiaomiParseResult::TYPE_CGG1;
|
||||
result.name = "CGG1";
|
||||
} else if ((raw[2] == 0xbc) && (raw[3] == 0x03)) { // VegTrug Grow Care Garden
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.config_validation as cv
|
||||
from esphome.components import sensor, esp32_ble_tracker
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_BINDKEY,
|
||||
CONF_HUMIDITY,
|
||||
CONF_MAC_ADDRESS,
|
||||
CONF_TEMPERATURE,
|
||||
@@ -27,6 +28,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(XiaomiCGG1),
|
||||
cv.Optional(CONF_BINDKEY): cv.bind_key,
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
|
||||
@@ -50,6 +52,8 @@ def to_code(config):
|
||||
yield esp32_ble_tracker.register_ble_device(var, config)
|
||||
|
||||
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||
if CONF_BINDKEY in config:
|
||||
cg.add(var.set_bindkey(config[CONF_BINDKEY]))
|
||||
|
||||
if CONF_TEMPERATURE in config:
|
||||
sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
|
||||
|
||||
@@ -10,6 +10,7 @@ static const char *TAG = "xiaomi_cgg1";
|
||||
|
||||
void XiaomiCGG1::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Xiaomi CGG1");
|
||||
ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str());
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||
@@ -31,8 +32,9 @@ bool XiaomiCGG1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
if (res->is_duplicate) {
|
||||
continue;
|
||||
}
|
||||
if (res->has_encryption) {
|
||||
ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device.");
|
||||
if (res->has_encryption &&
|
||||
(!(xiaomi_ble::decrypt_xiaomi_payload(const_cast<std::vector<uint8_t> &>(service_data.data), this->bindkey_,
|
||||
this->address_)))) {
|
||||
continue;
|
||||
}
|
||||
if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) {
|
||||
@@ -57,6 +59,18 @@ bool XiaomiCGG1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void XiaomiCGG1::set_bindkey(const std::string &bindkey) {
|
||||
memset(bindkey_, 0, 16);
|
||||
if (bindkey.size() != 32) {
|
||||
return;
|
||||
}
|
||||
char temp[3] = {0};
|
||||
for (int i = 0; i < 16; i++) {
|
||||
strncpy(temp, &(bindkey.c_str()[i * 2]), 2);
|
||||
bindkey_[i] = std::strtoul(temp, NULL, 16);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace xiaomi_cgg1
|
||||
} // namespace esphome
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace xiaomi_cgg1 {
|
||||
class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
|
||||
public:
|
||||
void set_address(uint64_t address) { address_ = address; }
|
||||
void set_bindkey(const std::string &bindkey);
|
||||
|
||||
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
|
||||
|
||||
@@ -24,6 +25,7 @@ class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen
|
||||
|
||||
protected:
|
||||
uint64_t address_;
|
||||
uint8_t bindkey_[16];
|
||||
sensor::Sensor *temperature_{nullptr};
|
||||
sensor::Sensor *humidity_{nullptr};
|
||||
sensor::Sensor *battery_level_{nullptr};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user