Merge branch 'dev' into bump-1.18.0b1

This commit is contained in:
Jesse Hills 2021-05-10 07:54:06 +12:00
commit 44562dbef1
No known key found for this signature in database
GPG Key ID: BEAAE804EFD8E83A
139 changed files with 7201 additions and 1297 deletions

7
.github/FUNDING.yml vendored
View File

@ -1,8 +1,3 @@
# These are supported funding model platforms
github:
patreon: ottowinter
open_collective:
ko_fi:
tidelift:
custom: https://esphome.io/guides/supporters.html
custom: https://www.nabucasa.com

View File

@ -115,7 +115,7 @@ jobs:
uses: actions/cache@v1
with:
path: ~/.platformio
key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}
key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }}
restore-keys: |
test-home-platformio-${{ matrix.test }}-
- name: Set up environment

36
.github/workflows/docker-lint-build.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Build and publish lint docker image
# Only run when docker paths change
on:
push:
branches: [dev]
paths:
- 'docker/Dockerfile.lint'
- 'requirements.txt'
- 'requirements_test.txt'
- 'platformio.ini'
- '.github/workflows/docker-lint-build.yml'
jobs:
publish-docker-lint-iage:
name: Build docker containers
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Pull for cache
run: |
docker pull "esphome/esphome-lint:latest" || true
- name: Build
run: |
docker build \
--cache-from "esphome/esphome-lint:latest" \
--file "docker/Dockerfile.lint" \
--tag "esphome/esphome-lint:latest" \
.
- name: Log in to docker hub
env:
DOCKER_USER: ${{ secrets.DOCKER_USER }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}"
- run: |
docker push "esphome/esphome-lint:latest"

View File

@ -112,7 +112,7 @@ jobs:
uses: actions/cache@v1
with:
path: ~/.platformio
key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}
key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }}
restore-keys: |
test-home-platformio-${{ matrix.test }}-
- name: Set up environment

View File

@ -111,7 +111,7 @@ jobs:
uses: actions/cache@v1
with:
path: ~/.platformio
key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}
key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }}
restore-keys: |
test-home-platformio-${{ matrix.test }}-
- name: Set up environment

View File

@ -18,8 +18,11 @@ esphome/components/animation/* @syndlex
esphome/components/api/* @OttoWinter
esphome/components/async_tcp/* @OttoWinter
esphome/components/atc_mithermometer/* @ahpohl
esphome/components/b_parasite/* @rbaron
esphome/components/bang_bang/* @OttoWinter
esphome/components/binary_sensor/* @esphome/core
esphome/components/ble_client/* @buxtronix
esphome/components/bme680_bsec/* @trvrnrth
esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/captive_portal/* @OttoWinter
esphome/components/climate/* @esphome/core
@ -34,8 +37,10 @@ esphome/components/ds1307/* @badbadc0ffee
esphome/components/exposure_notifications/* @OttoWinter
esphome/components/ezo/* @ssieb
esphome/components/fastled_base/* @OttoWinter
esphome/components/fingerprint_grow/* @OnFreund @loongyh
esphome/components/globals/* @esphome/core
esphome/components/gpio/* @esphome/core
esphome/components/gps/* @coogle
esphome/components/homeassistant/* @OttoWinter
esphome/components/i2c/* @esphome/core
esphome/components/inkbird_ibsth1_mini/* @fkirill
@ -76,6 +81,8 @@ esphome/components/rf_bridge/* @jesserockz
esphome/components/rtttl/* @glmnet
esphome/components/script/* @esphome/core
esphome/components/sensor/* @esphome/core
esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sht4x/* @sjtrny
esphome/components/shutdown/* @esphome/core
esphome/components/sim800l/* @glmnet
esphome/components/spi/* @esphome/core
@ -95,12 +102,14 @@ esphome/components/st7789v/* @kbx81
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
esphome/components/switch/* @esphome/core
esphome/components/tca9548a/* @andreashergert1984
esphome/components/tcl112/* @glmnet
esphome/components/teleinfo/* @0hax
esphome/components/thermostat/* @kbx81
esphome/components/time/* @OttoWinter
esphome/components/tm1637/* @glmnet
esphome/components/tmp102/* @timsavage
esphome/components/tof10120/* @wstrzalka
esphome/components/tuya/binary_sensor/* @jesserockz
esphome/components/tuya/climate/* @jesserockz
esphome/components/tuya/sensor/* @jesserockz

View File

@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@otto-winter.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at esphome@nabucasa.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

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

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

View 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

View 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),
&notify_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

View 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

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

View 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

View 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

View 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

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

View 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

View 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

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

View 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

View 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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &param) {
xSemaphoreGive(this->scan_end_lock_);
}
void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
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;
}

View File

@ -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 &param);
/// 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 &param);
/// 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 &param);
/// 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 &param);
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;

View 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

View File

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

View File

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

View File

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

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

View 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

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

View 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

View 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

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

View File

@ -72,7 +72,7 @@ def validate_truetype_file(value):
DEFAULT_GLYPHS = (
' !"%()+,-.:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
' !"%()+,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
)
CONF_RAW_DATA_ID = "raw_data_id"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View 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

View 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

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

View 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

View 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

View File

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

View 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

View 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

View File

@ -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°577″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),

View File

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

View File

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

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

View 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

View 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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More