mirror of
https://github.com/esphome/esphome.git
synced 2025-11-01 15:41:52 +00:00
Compare commits
92 Commits
jesserockz
...
2025.5.0b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e20440c8e | ||
|
|
0630244195 | ||
|
|
183659f527 | ||
|
|
4ea63af796 | ||
|
|
0aa7911b1b | ||
|
|
032949bc77 | ||
|
|
6f8ee65919 | ||
|
|
c5654b4cb2 | ||
|
|
410b6353fe | ||
|
|
a36e1aab8e | ||
|
|
864ae7a56c | ||
|
|
2560d2b9d0 | ||
|
|
0cf9b05afd | ||
|
|
8b65d1673a | ||
|
|
5e164b107a | ||
|
|
a83959d738 | ||
|
|
0ccc5bf714 | ||
|
|
bc0956019b | ||
|
|
49f631d6c5 | ||
|
|
a9d5eb8470 | ||
|
|
7c0546c9f0 | ||
|
|
f4eb75e4e0 | ||
|
|
5b2c19bc86 | ||
|
|
185b84b8b2 | ||
|
|
facf94699e | ||
|
|
58104229e2 | ||
|
|
50c88b7aa7 | ||
|
|
81bae96109 | ||
|
|
a3ed090594 | ||
|
|
cff1820772 | ||
|
|
bdd2774544 | ||
|
|
38790793dd | ||
|
|
dcd786d21c | ||
|
|
71e88fe9b2 | ||
|
|
11dcaf7383 | ||
|
|
dded81d622 | ||
|
|
8324b3244c | ||
|
|
401c090edd | ||
|
|
8757957e17 | ||
|
|
e2c8a5b638 | ||
|
|
7bb899bfa1 | ||
|
|
3e2359ddff | ||
|
|
04147a7f27 | ||
|
|
cae3c030d2 | ||
|
|
d7c615ec43 | ||
|
|
1294e8ccd5 | ||
|
|
37a2cb07d1 | ||
|
|
2af3994f79 | ||
|
|
0c0fe81814 | ||
|
|
82c8614315 | ||
|
|
a85dc65038 | ||
|
|
290b8bdca0 | ||
|
|
a96ed0b70a | ||
|
|
cdc1a7c646 | ||
|
|
7f59aff157 | ||
|
|
cdce59f7f9 | ||
|
|
ff1c3cb52e | ||
|
|
bec9d91419 | ||
|
|
8399d894c1 | ||
|
|
e1732c4945 | ||
|
|
ca221d6cb2 | ||
|
|
8a90ce882a | ||
|
|
b3400a1308 | ||
|
|
23fb1bed61 | ||
|
|
2b3757dff8 | ||
|
|
1da8e99d27 | ||
|
|
e94e71ded8 | ||
|
|
00f20c1e55 | ||
|
|
45d019a7e4 | ||
|
|
8465017db9 | ||
|
|
782d748210 | ||
|
|
b01d85a974 | ||
|
|
797a4c61f2 | ||
|
|
8e29437900 | ||
|
|
9e64e71cdf | ||
|
|
ef2621aa54 | ||
|
|
882273cb56 | ||
|
|
ad2b74d9b4 | ||
|
|
26669bd1b6 | ||
|
|
54ead9a6b4 | ||
|
|
d60e1f02c0 | ||
|
|
213648564c | ||
|
|
8bdbde9732 | ||
|
|
e988762576 | ||
|
|
75496849eb | ||
|
|
39b119e9cc | ||
|
|
4d43caf6c1 | ||
|
|
ce5e1a6294 | ||
|
|
88be14aaa3 | ||
|
|
1ac56b06c5 | ||
|
|
8bbc509b0b | ||
|
|
6f35d0ac88 |
11
.github/workflows/ci-api-proto.yml
vendored
11
.github/workflows/ci-api-proto.yml
vendored
@@ -57,6 +57,17 @@ jobs:
|
||||
event: 'REQUEST_CHANGES',
|
||||
body: 'You have altered the generated proto files but they do not match what is expected.\nPlease run "script/api_protobuf/api_protobuf.py" and commit the changes.'
|
||||
})
|
||||
- if: failure()
|
||||
name: Show changes
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
esphome/components/api/api_pb2.*
|
||||
esphome/components/api/api_pb2_service.*
|
||||
- if: success()
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@v7.0.1
|
||||
|
||||
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@@ -292,6 +292,11 @@ jobs:
|
||||
name: Run script/clang-tidy for ESP32 IDF
|
||||
options: --environment esp32-idf-tidy --grep USE_ESP_IDF
|
||||
pio_cache_key: tidyesp32-idf
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ZEPHYR
|
||||
options: --environment nrf52-tidy --grep USE_ZEPHYR
|
||||
pio_cache_key: tidy-zephyr
|
||||
ignore_errors: true
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
@@ -331,13 +336,13 @@ jobs:
|
||||
- name: Run clang-tidy
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
script/clang-tidy --all-headers --fix ${{ matrix.options }}
|
||||
script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
|
||||
env:
|
||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
||||
|
||||
- name: Suggested changes
|
||||
run: script/ci-suggest-changes
|
||||
run: script/ci-suggest-changes ${{ matrix.ignore_errors && '|| true' || '' }}
|
||||
# yamllint disable-line rule:line-length
|
||||
if: always()
|
||||
|
||||
|
||||
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
@@ -231,3 +231,25 @@ jobs:
|
||||
content: description
|
||||
}
|
||||
})
|
||||
|
||||
deploy-esphome-schema:
|
||||
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- init
|
||||
- deploy-manifest
|
||||
steps:
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }}
|
||||
script: |
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
owner: "esphome",
|
||||
repo: "esphome-schema",
|
||||
workflow_id: "generate-schemas.yml",
|
||||
ref: "main",
|
||||
inputs: {
|
||||
version: "${{ needs.init.outputs.tag }}",
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.11.0
|
||||
rev: v0.11.9
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -33,7 +33,7 @@ repos:
|
||||
- id: pyupgrade
|
||||
args: [--py39-plus]
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.35.1
|
||||
rev: v1.37.1
|
||||
hooks:
|
||||
- id: yamllint
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
|
||||
@@ -282,6 +282,7 @@ esphome/components/microphone/* @jesserockz @kahrendt
|
||||
esphome/components/mics_4514/* @jesserockz
|
||||
esphome/components/midea/* @dudanov
|
||||
esphome/components/midea_ir/* @dudanov
|
||||
esphome/components/mipi_spi/* @clydebarrow
|
||||
esphome/components/mitsubishi/* @RubyBailey
|
||||
esphome/components/mixer/speaker/* @kahrendt
|
||||
esphome/components/mlx90393/* @functionpointer
|
||||
@@ -398,6 +399,7 @@ esphome/components/smt100/* @piechade
|
||||
esphome/components/sn74hc165/* @jesserockz
|
||||
esphome/components/socket/* @esphome/core
|
||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
|
||||
esphome/components/sound_level/* @kahrendt
|
||||
esphome/components/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
|
||||
esphome/components/spi/* @clydebarrow @esphome/core
|
||||
|
||||
@@ -34,7 +34,7 @@ AirthingsWaveBase = airthings_wave_base_ns.class_(
|
||||
|
||||
|
||||
BASE_SCHEMA = (
|
||||
sensor.SENSOR_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
|
||||
@@ -5,6 +5,8 @@ from esphome.components import mqtt, web_server
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CODE,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_STATE,
|
||||
@@ -12,6 +14,7 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
|
||||
CODEOWNERS = ["@grahambrown11", "@hwstar"]
|
||||
@@ -78,12 +81,11 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_(
|
||||
"AlarmControlPanelCondition", automation.Condition
|
||||
)
|
||||
|
||||
ALARM_CONTROL_PANEL_SCHEMA = (
|
||||
_ALARM_CONTROL_PANEL_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AlarmControlPanel),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt.MQTTAlarmControlPanelComponent
|
||||
),
|
||||
@@ -146,6 +148,33 @@ ALARM_CONTROL_PANEL_SCHEMA = (
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def alarm_control_panel_schema(
|
||||
class_: MockObjClass,
|
||||
*,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
icon: str = cv.UNDEFINED,
|
||||
) -> cv.Schema:
|
||||
schema = {
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
}
|
||||
|
||||
for key, default, validator in [
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
(CONF_ICON, icon, cv.icon),
|
||||
]:
|
||||
if default is not cv.UNDEFINED:
|
||||
schema[cv.Optional(key, default=default)] = validator
|
||||
|
||||
return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel)
|
||||
ALARM_CONTROL_PANEL_SCHEMA.add_extra(
|
||||
cv.deprecated_schema_constant("alarm_control_panel")
|
||||
)
|
||||
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(AlarmControlPanel),
|
||||
@@ -209,6 +238,12 @@ async def register_alarm_control_panel(var, config):
|
||||
await setup_alarm_control_panel_core_(var, config)
|
||||
|
||||
|
||||
async def new_alarm_control_panel(config, *args):
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_alarm_control_panel(var, config)
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import ble_client, cover
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PIN
|
||||
from esphome.const import CONF_PIN
|
||||
|
||||
CODEOWNERS = ["@buxtronix"]
|
||||
DEPENDENCIES = ["ble_client"]
|
||||
@@ -15,9 +15,9 @@ Am43Component = am43_ns.class_(
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cover.COVER_SCHEMA.extend(
|
||||
cover.cover_schema(Am43Component)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Am43Component),
|
||||
cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF),
|
||||
cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
|
||||
}
|
||||
@@ -28,9 +28,8 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
var = await cover.new_cover(config)
|
||||
cg.add(var.set_pin(config[CONF_PIN]))
|
||||
cg.add(var.set_invert_position(config[CONF_INVERT_POSITION]))
|
||||
await cg.register_component(var, config)
|
||||
await cover.register_cover(var, config)
|
||||
await ble_client.register_ble_node(var, config)
|
||||
|
||||
@@ -33,23 +33,24 @@ service APIConnection {
|
||||
rpc execute_service (ExecuteServiceRequest) returns (void) {}
|
||||
rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {}
|
||||
|
||||
rpc cover_command (CoverCommandRequest) returns (void) {}
|
||||
rpc fan_command (FanCommandRequest) returns (void) {}
|
||||
rpc light_command (LightCommandRequest) returns (void) {}
|
||||
rpc switch_command (SwitchCommandRequest) returns (void) {}
|
||||
rpc button_command (ButtonCommandRequest) returns (void) {}
|
||||
rpc camera_image (CameraImageRequest) returns (void) {}
|
||||
rpc climate_command (ClimateCommandRequest) returns (void) {}
|
||||
rpc number_command (NumberCommandRequest) returns (void) {}
|
||||
rpc text_command (TextCommandRequest) returns (void) {}
|
||||
rpc select_command (SelectCommandRequest) returns (void) {}
|
||||
rpc button_command (ButtonCommandRequest) returns (void) {}
|
||||
rpc lock_command (LockCommandRequest) returns (void) {}
|
||||
rpc valve_command (ValveCommandRequest) returns (void) {}
|
||||
rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
|
||||
rpc cover_command (CoverCommandRequest) returns (void) {}
|
||||
rpc date_command (DateCommandRequest) returns (void) {}
|
||||
rpc time_command (TimeCommandRequest) returns (void) {}
|
||||
rpc datetime_command (DateTimeCommandRequest) returns (void) {}
|
||||
rpc fan_command (FanCommandRequest) returns (void) {}
|
||||
rpc light_command (LightCommandRequest) returns (void) {}
|
||||
rpc lock_command (LockCommandRequest) returns (void) {}
|
||||
rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
|
||||
rpc number_command (NumberCommandRequest) returns (void) {}
|
||||
rpc select_command (SelectCommandRequest) returns (void) {}
|
||||
rpc siren_command (SirenCommandRequest) returns (void) {}
|
||||
rpc switch_command (SwitchCommandRequest) returns (void) {}
|
||||
rpc text_command (TextCommandRequest) returns (void) {}
|
||||
rpc time_command (TimeCommandRequest) returns (void) {}
|
||||
rpc update_command (UpdateCommandRequest) returns (void) {}
|
||||
rpc valve_command (ValveCommandRequest) returns (void) {}
|
||||
|
||||
rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
|
||||
rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}
|
||||
@@ -655,7 +656,7 @@ message SubscribeLogsResponse {
|
||||
option (no_delay) = false;
|
||||
|
||||
LogLevel level = 1;
|
||||
string message = 3;
|
||||
bytes message = 3;
|
||||
bool send_failed = 4;
|
||||
}
|
||||
|
||||
@@ -911,6 +912,7 @@ message ClimateStateResponse {
|
||||
float target_temperature = 4;
|
||||
float target_temperature_low = 5;
|
||||
float target_temperature_high = 6;
|
||||
// For older peers, equal to preset == CLIMATE_PRESET_AWAY
|
||||
bool unused_legacy_away = 7;
|
||||
ClimateAction action = 8;
|
||||
ClimateFanMode fan_mode = 9;
|
||||
@@ -936,6 +938,7 @@ message ClimateCommandRequest {
|
||||
float target_temperature_low = 7;
|
||||
bool has_target_temperature_high = 8;
|
||||
float target_temperature_high = 9;
|
||||
// legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset
|
||||
bool unused_has_legacy_away = 10;
|
||||
bool unused_legacy_away = 11;
|
||||
bool has_fan_mode = 12;
|
||||
@@ -1038,6 +1041,49 @@ message SelectCommandRequest {
|
||||
string state = 2;
|
||||
}
|
||||
|
||||
// ==================== SIREN ====================
|
||||
message ListEntitiesSirenResponse {
|
||||
option (id) = 55;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SIREN";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string unique_id = 4;
|
||||
|
||||
string icon = 5;
|
||||
bool disabled_by_default = 6;
|
||||
repeated string tones = 7;
|
||||
bool supports_duration = 8;
|
||||
bool supports_volume = 9;
|
||||
EntityCategory entity_category = 10;
|
||||
}
|
||||
message SirenStateResponse {
|
||||
option (id) = 56;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SIREN";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
bool state = 2;
|
||||
}
|
||||
message SirenCommandRequest {
|
||||
option (id) = 57;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_SIREN";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
bool has_state = 2;
|
||||
bool state = 3;
|
||||
bool has_tone = 4;
|
||||
string tone = 5;
|
||||
bool has_duration = 6;
|
||||
uint32 duration = 7;
|
||||
bool has_volume = 8;
|
||||
float volume = 9;
|
||||
}
|
||||
|
||||
// ==================== LOCK ====================
|
||||
enum LockState {
|
||||
@@ -1207,8 +1253,8 @@ message SubscribeBluetoothLEAdvertisementsRequest {
|
||||
|
||||
message BluetoothServiceData {
|
||||
string uuid = 1;
|
||||
repeated uint32 legacy_data = 2 [deprecated = true];
|
||||
bytes data = 3; // Changed in proto version 1.7
|
||||
repeated uint32 legacy_data = 2 [deprecated = true]; // Removed in api version 1.7
|
||||
bytes data = 3; // Added in api version 1.7
|
||||
}
|
||||
message BluetoothLEAdvertisementResponse {
|
||||
option (id) = 67;
|
||||
@@ -1217,7 +1263,7 @@ message BluetoothLEAdvertisementResponse {
|
||||
option (no_delay) = true;
|
||||
|
||||
uint64 address = 1;
|
||||
string name = 2;
|
||||
bytes name = 2;
|
||||
sint32 rssi = 3;
|
||||
|
||||
repeated string service_uuids = 4;
|
||||
@@ -1504,7 +1550,7 @@ message BluetoothScannerSetModeRequest {
|
||||
BluetoothScannerMode mode = 1;
|
||||
}
|
||||
|
||||
// ==================== PUSH TO TALK ====================
|
||||
// ==================== VOICE ASSISTANT ====================
|
||||
enum VoiceAssistantSubscribeFlag {
|
||||
VOICE_ASSISTANT_SUBSCRIBE_NONE = 0;
|
||||
VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1;
|
||||
|
||||
@@ -192,15 +192,34 @@ void APIConnection::loop() {
|
||||
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) {
|
||||
uint32_t to_send = std::min((size_t) 1024, this->image_reader_.available());
|
||||
auto buffer = this->create_buffer();
|
||||
// Message will use 8 more bytes than the minimum size, and typical
|
||||
// MTU is 1500. Sometimes users will see as low as 1460 MTU.
|
||||
// If its IPv6 the header is 40 bytes, and if its IPv4
|
||||
// the header is 20 bytes. So we have 1460 - 40 = 1420 bytes
|
||||
// available for the payload. But we also need to add the size of
|
||||
// the protobuf overhead, which is 8 bytes.
|
||||
//
|
||||
// To be safe we pick 1390 bytes as the maximum size
|
||||
// to send in one go. This is the maximum size of a single packet
|
||||
// that can be sent over the network.
|
||||
// This is to avoid fragmentation of the packet.
|
||||
uint32_t to_send = std::min((size_t) 1390, this->image_reader_.available());
|
||||
bool done = this->image_reader_.available() == to_send;
|
||||
uint32_t msg_size = 0;
|
||||
ProtoSize::add_fixed_field<4>(msg_size, 1, true);
|
||||
// partial message size calculated manually since its a special case
|
||||
// 1 for the data field, varint for the data size, and the data itself
|
||||
msg_size += 1 + ProtoSize::varint(to_send) + to_send;
|
||||
ProtoSize::add_bool_field(msg_size, 1, done);
|
||||
|
||||
auto buffer = this->create_buffer(msg_size);
|
||||
// fixed32 key = 1;
|
||||
buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
|
||||
// bytes data = 2;
|
||||
buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
|
||||
// bool done = 3;
|
||||
bool done = this->image_reader_.available() == to_send;
|
||||
buffer.encode_bool(3, done);
|
||||
|
||||
bool success = this->send_buffer(buffer, 44);
|
||||
|
||||
if (success) {
|
||||
@@ -1774,12 +1793,25 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char
|
||||
if (this->log_subscription_ < level)
|
||||
return false;
|
||||
|
||||
// Send raw so that we don't copy too much
|
||||
auto buffer = this->create_buffer();
|
||||
// LogLevel level = 1;
|
||||
buffer.encode_uint32(1, static_cast<uint32_t>(level));
|
||||
// string message = 3;
|
||||
buffer.encode_string(3, line, strlen(line));
|
||||
// Pre-calculate message size to avoid reallocations
|
||||
const size_t line_length = strlen(line);
|
||||
uint32_t msg_size = 0;
|
||||
|
||||
// Add size for level field (field ID 1, varint type)
|
||||
// 1 byte for field tag + size of the level varint
|
||||
msg_size += 1 + api::ProtoSize::varint(static_cast<uint32_t>(level));
|
||||
|
||||
// Add size for string field (field ID 3, string type)
|
||||
// 1 byte for field tag + size of length varint + string length
|
||||
msg_size += 1 + api::ProtoSize::varint(static_cast<uint32_t>(line_length)) + line_length;
|
||||
|
||||
// Create a pre-sized buffer
|
||||
auto buffer = this->create_buffer(msg_size);
|
||||
|
||||
// Encode the message (SubscribeLogsResponse)
|
||||
buffer.encode_uint32(1, static_cast<uint32_t>(level)); // LogLevel level = 1
|
||||
buffer.encode_string(3, line, line_length); // string message = 3
|
||||
|
||||
// SubscribeLogsResponse - 29
|
||||
return this->send_buffer(buffer, 29);
|
||||
}
|
||||
|
||||
@@ -312,9 +312,10 @@ class APIConnection : public APIServerConnection {
|
||||
void on_fatal_error() override;
|
||||
void on_unauthenticated_access() override;
|
||||
void on_no_setup_connection() override;
|
||||
ProtoWriteBuffer create_buffer() override {
|
||||
ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
|
||||
// FIXME: ensure no recursive writes can happen
|
||||
this->proto_write_buffer_.clear();
|
||||
this->proto_write_buffer_.reserve(reserve_size);
|
||||
return {&this->proto_write_buffer_};
|
||||
}
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "proto.h"
|
||||
#include "api_pb2_size.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome {
|
||||
@@ -72,6 +73,91 @@ const char *api_error_to_str(APIError err) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
// Common implementation for writing raw data to socket
|
||||
template<typename StateEnum>
|
||||
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket,
|
||||
std::vector<uint8_t> &tx_buf, const std::string &info, StateEnum &state,
|
||||
StateEnum failed_state) {
|
||||
// This method writes data to socket or buffers it
|
||||
// Returns APIError::OK if successful (or would block, but data has been buffered)
|
||||
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to failed_state
|
||||
|
||||
if (iovcnt == 0)
|
||||
return APIError::OK; // Nothing to do, success
|
||||
|
||||
size_t total_write_len = 0;
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
ESP_LOGVV(TAG, "Sending raw: %s",
|
||||
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
|
||||
#endif
|
||||
total_write_len += iov[i].iov_len;
|
||||
}
|
||||
|
||||
if (!tx_buf.empty()) {
|
||||
// try to empty tx_buf first
|
||||
while (!tx_buf.empty()) {
|
||||
ssize_t sent = socket->write(tx_buf.data(), tx_buf.size());
|
||||
if (is_would_block(sent)) {
|
||||
break;
|
||||
} else if (sent == -1) {
|
||||
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
|
||||
state = failed_state;
|
||||
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
|
||||
}
|
||||
// TODO: inefficient if multiple packets in txbuf
|
||||
// replace with deque of buffers
|
||||
tx_buf.erase(tx_buf.begin(), tx_buf.begin() + sent);
|
||||
}
|
||||
}
|
||||
|
||||
if (!tx_buf.empty()) {
|
||||
// tx buf not empty, can't write now because then stream would be inconsistent
|
||||
// Reserve space upfront to avoid multiple reallocations
|
||||
tx_buf.reserve(tx_buf.size() + total_write_len);
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
|
||||
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
|
||||
}
|
||||
return APIError::OK; // Success, data buffered
|
||||
}
|
||||
|
||||
ssize_t sent = socket->writev(iov, iovcnt);
|
||||
if (is_would_block(sent)) {
|
||||
// operation would block, add buffer to tx_buf
|
||||
// Reserve space upfront to avoid multiple reallocations
|
||||
tx_buf.reserve(tx_buf.size() + total_write_len);
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
|
||||
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
|
||||
}
|
||||
return APIError::OK; // Success, data buffered
|
||||
} else if (sent == -1) {
|
||||
// an error occurred
|
||||
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
|
||||
state = failed_state;
|
||||
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
|
||||
} else if ((size_t) sent != total_write_len) {
|
||||
// partially sent, add end to tx_buf
|
||||
size_t remaining = total_write_len - sent;
|
||||
// Reserve space upfront to avoid multiple reallocations
|
||||
tx_buf.reserve(tx_buf.size() + remaining);
|
||||
|
||||
size_t to_consume = sent;
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
if (to_consume >= iov[i].iov_len) {
|
||||
to_consume -= iov[i].iov_len;
|
||||
} else {
|
||||
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
|
||||
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
|
||||
to_consume = 0;
|
||||
}
|
||||
}
|
||||
return APIError::OK; // Success, data buffered
|
||||
}
|
||||
return APIError::OK; // Success, all data sent
|
||||
}
|
||||
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
|
||||
// uncomment to log raw packets
|
||||
//#define HELPER_LOG_PACKETS
|
||||
@@ -546,71 +632,6 @@ APIError APINoiseFrameHelper::try_send_tx_buf_() {
|
||||
|
||||
return APIError::OK;
|
||||
}
|
||||
/** Write the data to the socket, or buffer it a write would block
|
||||
*
|
||||
* @param data The data to write
|
||||
* @param len The length of data
|
||||
*/
|
||||
APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
|
||||
if (iovcnt == 0)
|
||||
return APIError::OK;
|
||||
APIError aerr;
|
||||
|
||||
size_t total_write_len = 0;
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
ESP_LOGVV(TAG, "Sending raw: %s",
|
||||
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
|
||||
#endif
|
||||
total_write_len += iov[i].iov_len;
|
||||
}
|
||||
|
||||
if (!tx_buf_.empty()) {
|
||||
// try to empty tx_buf_ first
|
||||
aerr = try_send_tx_buf_();
|
||||
if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
|
||||
return aerr;
|
||||
}
|
||||
|
||||
if (!tx_buf_.empty()) {
|
||||
// tx buf not empty, can't write now because then stream would be inconsistent
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
|
||||
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
ssize_t sent = socket_->writev(iov, iovcnt);
|
||||
if (is_would_block(sent)) {
|
||||
// operation would block, add buffer to tx_buf
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
|
||||
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
|
||||
}
|
||||
return APIError::OK;
|
||||
} else if (sent == -1) {
|
||||
// an error occurred
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Socket write failed with errno %d", errno);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
} else if ((size_t) sent != total_write_len) {
|
||||
// partially sent, add end to tx_buf
|
||||
size_t to_consume = sent;
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
if (to_consume >= iov[i].iov_len) {
|
||||
to_consume -= iov[i].iov_len;
|
||||
} else {
|
||||
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
|
||||
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
|
||||
to_consume = 0;
|
||||
}
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
// fully sent
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
|
||||
uint8_t header[3];
|
||||
header[0] = 0x01; // indicator
|
||||
@@ -744,6 +765,11 @@ void noise_rand_bytes(void *output, size_t len) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit template instantiation for Noise
|
||||
template APIError APIFrameHelper::write_raw_<APINoiseFrameHelper::State>(
|
||||
const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
|
||||
APINoiseFrameHelper::State &state, APINoiseFrameHelper::State failed_state);
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
@@ -933,6 +959,8 @@ APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *pay
|
||||
}
|
||||
|
||||
std::vector<uint8_t> header;
|
||||
header.reserve(1 + api::ProtoSize::varint(static_cast<uint32_t>(payload_len)) +
|
||||
api::ProtoSize::varint(static_cast<uint32_t>(type)));
|
||||
header.push_back(0x00);
|
||||
ProtoVarInt(payload_len).encode(header);
|
||||
ProtoVarInt(type).encode(header);
|
||||
@@ -966,71 +994,6 @@ APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
|
||||
|
||||
return APIError::OK;
|
||||
}
|
||||
/** Write the data to the socket, or buffer it a write would block
|
||||
*
|
||||
* @param data The data to write
|
||||
* @param len The length of data
|
||||
*/
|
||||
APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
|
||||
if (iovcnt == 0)
|
||||
return APIError::OK;
|
||||
APIError aerr;
|
||||
|
||||
size_t total_write_len = 0;
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
ESP_LOGVV(TAG, "Sending raw: %s",
|
||||
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
|
||||
#endif
|
||||
total_write_len += iov[i].iov_len;
|
||||
}
|
||||
|
||||
if (!tx_buf_.empty()) {
|
||||
// try to empty tx_buf_ first
|
||||
aerr = try_send_tx_buf_();
|
||||
if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
|
||||
return aerr;
|
||||
}
|
||||
|
||||
if (!tx_buf_.empty()) {
|
||||
// tx buf not empty, can't write now because then stream would be inconsistent
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
|
||||
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
ssize_t sent = socket_->writev(iov, iovcnt);
|
||||
if (is_would_block(sent)) {
|
||||
// operation would block, add buffer to tx_buf
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
|
||||
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
|
||||
}
|
||||
return APIError::OK;
|
||||
} else if (sent == -1) {
|
||||
// an error occurred
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Socket write failed with errno %d", errno);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
} else if ((size_t) sent != total_write_len) {
|
||||
// partially sent, add end to tx_buf
|
||||
size_t to_consume = sent;
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
if (to_consume >= iov[i].iov_len) {
|
||||
to_consume -= iov[i].iov_len;
|
||||
} else {
|
||||
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
|
||||
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
|
||||
to_consume = 0;
|
||||
}
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
// fully sent
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
APIError APIPlaintextFrameHelper::close() {
|
||||
state_ = State::CLOSED;
|
||||
@@ -1048,6 +1011,11 @@ APIError APIPlaintextFrameHelper::shutdown(int how) {
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
// Explicit template instantiation for Plaintext
|
||||
template APIError APIFrameHelper::write_raw_<APIPlaintextFrameHelper::State>(
|
||||
const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
|
||||
APIPlaintextFrameHelper::State &state, APIPlaintextFrameHelper::State failed_state);
|
||||
#endif // USE_API_PLAINTEXT
|
||||
|
||||
} // namespace api
|
||||
|
||||
@@ -72,6 +72,12 @@ class APIFrameHelper {
|
||||
virtual APIError shutdown(int how) = 0;
|
||||
// Give this helper a name for logging
|
||||
virtual void set_log_info(std::string info) = 0;
|
||||
|
||||
protected:
|
||||
// Common implementation for writing raw data to socket
|
||||
template<typename StateEnum>
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
|
||||
const std::string &info, StateEnum &state, StateEnum failed_state);
|
||||
};
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -103,7 +109,9 @@ class APINoiseFrameHelper : public APIFrameHelper {
|
||||
APIError try_read_frame_(ParsedFrame *frame);
|
||||
APIError try_send_tx_buf_();
|
||||
APIError write_frame_(const uint8_t *data, size_t len);
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt);
|
||||
inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
|
||||
return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
|
||||
}
|
||||
APIError init_handshake_();
|
||||
APIError check_handshake_finished_();
|
||||
void send_explicit_handshake_reject_(const std::string &reason);
|
||||
@@ -164,7 +172,9 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
|
||||
|
||||
APIError try_read_frame_(ParsedFrame *frame);
|
||||
APIError try_send_tx_buf_();
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt);
|
||||
inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
|
||||
return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
|
||||
}
|
||||
|
||||
std::unique_ptr<socket::Socket> socket_;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
// This file was automatically generated with a tool.
|
||||
// See scripts/api_protobuf/api_protobuf.py
|
||||
// See script/api_protobuf/api_protobuf.py
|
||||
#include "api_pb2_service.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -292,6 +292,24 @@ bool APIServerConnectionBase::send_select_state_response(const SelectStateRespon
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
bool APIServerConnectionBase::send_list_entities_siren_response(const ListEntitiesSirenResponse &msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "send_list_entities_siren_response: %s", msg.dump().c_str());
|
||||
#endif
|
||||
return this->send_message_<ListEntitiesSirenResponse>(msg, 55);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
bool APIServerConnectionBase::send_siren_state_response(const SirenStateResponse &msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "send_siren_state_response: %s", msg.dump().c_str());
|
||||
#endif
|
||||
return this->send_message_<SirenStateResponse>(msg, 56);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool APIServerConnectionBase::send_list_entities_lock_response(const ListEntitiesLockResponse &msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -903,6 +921,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
this->on_select_command_request(msg);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
case 57: {
|
||||
#ifdef USE_SIREN
|
||||
SirenCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_siren_command_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
this->on_siren_command_request(msg);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
@@ -1369,8 +1398,8 @@ void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncrypt
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
|
||||
#ifdef USE_BUTTON
|
||||
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
@@ -1379,46 +1408,7 @@ void APIServerConnection::on_cover_command_request(const CoverCommandRequest &ms
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->cover_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->fan_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->light_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->switch_command(msg);
|
||||
this->button_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
@@ -1447,8 +1437,8 @@ void APIServerConnection::on_climate_command_request(const ClimateCommandRequest
|
||||
this->climate_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) {
|
||||
#ifdef USE_COVER
|
||||
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
@@ -1457,85 +1447,7 @@ void APIServerConnection::on_number_command_request(const NumberCommandRequest &
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->number_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->text_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->select_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->button_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->lock_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->valve_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->media_player_command(msg);
|
||||
this->cover_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
@@ -1551,19 +1463,6 @@ void APIServerConnection::on_date_command_request(const DateCommandRequest &msg)
|
||||
this->date_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->time_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
@@ -1577,6 +1476,136 @@ void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequ
|
||||
this->datetime_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->fan_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->light_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->lock_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->media_player_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->number_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->select_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->siren_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->switch_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->text_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->time_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
@@ -1590,6 +1619,19 @@ void APIServerConnection::on_update_command_request(const UpdateCommandRequest &
|
||||
this->update_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->valve_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
|
||||
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This file was automatically generated with a tool.
|
||||
// See scripts/api_protobuf/api_protobuf.py
|
||||
// See script/api_protobuf/api_protobuf.py
|
||||
#pragma once
|
||||
|
||||
#include "api_pb2.h"
|
||||
@@ -136,6 +136,15 @@ class APIServerConnectionBase : public ProtoService {
|
||||
#ifdef USE_SELECT
|
||||
virtual void on_select_command_request(const SelectCommandRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
bool send_list_entities_siren_response(const ListEntitiesSirenResponse &msg);
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
bool send_siren_state_response(const SirenStateResponse &msg);
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
virtual void on_siren_command_request(const SirenCommandRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool send_list_entities_lock_response(const ListEntitiesLockResponse &msg);
|
||||
#endif
|
||||
@@ -364,17 +373,8 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_API_NOISE
|
||||
virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
virtual void cover_command(const CoverCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
virtual void fan_command(const FanCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
virtual void light_command(const LightCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
virtual void switch_command(const SwitchCommandRequest &msg) = 0;
|
||||
#ifdef USE_BUTTON
|
||||
virtual void button_command(const ButtonCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
virtual void camera_image(const CameraImageRequest &msg) = 0;
|
||||
@@ -382,39 +382,51 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_CLIMATE
|
||||
virtual void climate_command(const ClimateCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
virtual void number_command(const NumberCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
virtual void text_command(const TextCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
virtual void select_command(const SelectCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
virtual void button_command(const ButtonCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
virtual void lock_command(const LockCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
virtual void valve_command(const ValveCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
|
||||
#ifdef USE_COVER
|
||||
virtual void cover_command(const CoverCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
virtual void date_command(const DateCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
virtual void time_command(const TimeCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
virtual void datetime_command(const DateTimeCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
virtual void fan_command(const FanCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
virtual void light_command(const LightCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
virtual void lock_command(const LockCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
virtual void number_command(const NumberCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
virtual void select_command(const SelectCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
virtual void siren_command(const SirenCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
virtual void switch_command(const SwitchCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
virtual void text_command(const TextCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
virtual void time_command(const TimeCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
virtual void update_command(const UpdateCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
virtual void valve_command(const ValveCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
|
||||
#endif
|
||||
@@ -478,17 +490,8 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_API_NOISE
|
||||
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
void on_cover_command_request(const CoverCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
void on_fan_command_request(const FanCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
void on_light_command_request(const LightCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void on_switch_command_request(const SwitchCommandRequest &msg) override;
|
||||
#ifdef USE_BUTTON
|
||||
void on_button_command_request(const ButtonCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
void on_camera_image_request(const CameraImageRequest &msg) override;
|
||||
@@ -496,39 +499,51 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_CLIMATE
|
||||
void on_climate_command_request(const ClimateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
void on_number_command_request(const NumberCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void on_text_command_request(const TextCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void on_select_command_request(const SelectCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
void on_button_command_request(const ButtonCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void on_lock_command_request(const LockCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
void on_valve_command_request(const ValveCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
|
||||
#ifdef USE_COVER
|
||||
void on_cover_command_request(const CoverCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
void on_date_command_request(const DateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void on_time_command_request(const TimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
void on_fan_command_request(const FanCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
void on_light_command_request(const LightCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void on_lock_command_request(const LockCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
void on_number_command_request(const NumberCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void on_select_command_request(const SelectCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
void on_siren_command_request(const SirenCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void on_switch_command_request(const SwitchCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void on_text_command_request(const TextCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void on_time_command_request(const TimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
void on_update_command_request(const UpdateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
void on_valve_command_request(const ValveCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
#endif
|
||||
|
||||
361
esphome/components/api/api_pb2_size.h
Normal file
361
esphome/components/api/api_pb2_size.h
Normal file
@@ -0,0 +1,361 @@
|
||||
#pragma once
|
||||
|
||||
#include "proto.h"
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
|
||||
class ProtoSize {
|
||||
public:
|
||||
/**
|
||||
* @brief ProtoSize class for Protocol Buffer serialization size calculation
|
||||
*
|
||||
* This class provides static methods to calculate the exact byte counts needed
|
||||
* for encoding various Protocol Buffer field types. All methods are designed to be
|
||||
* efficient for the common case where many fields have default values.
|
||||
*
|
||||
* Implements Protocol Buffer encoding size calculation according to:
|
||||
* https://protobuf.dev/programming-guides/encoding/
|
||||
*
|
||||
* Key features:
|
||||
* - Early-return optimization for zero/default values
|
||||
* - Direct total_size updates to avoid unnecessary additions
|
||||
* - Specialized handling for different field types according to protobuf spec
|
||||
* - Templated helpers for repeated fields and messages
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
|
||||
*
|
||||
* @param value The uint32_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(uint32_t value) {
|
||||
// Optimized varint size calculation using leading zeros
|
||||
// Each 7 bits requires one byte in the varint encoding
|
||||
if (value < 128)
|
||||
return 1; // 7 bits, common case for small values
|
||||
|
||||
// For larger values, count bytes needed based on the position of the highest bit set
|
||||
if (value < 16384) {
|
||||
return 2; // 14 bits
|
||||
} else if (value < 2097152) {
|
||||
return 3; // 21 bits
|
||||
} else if (value < 268435456) {
|
||||
return 4; // 28 bits
|
||||
} else {
|
||||
return 5; // 32 bits (maximum for uint32_t)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint64_t value as a varint
|
||||
*
|
||||
* @param value The uint64_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(uint64_t value) {
|
||||
// Handle common case of values fitting in uint32_t (vast majority of use cases)
|
||||
if (value <= UINT32_MAX) {
|
||||
return varint(static_cast<uint32_t>(value));
|
||||
}
|
||||
|
||||
// For larger values, determine size based on highest bit position
|
||||
if (value < (1ULL << 35)) {
|
||||
return 5; // 35 bits
|
||||
} else if (value < (1ULL << 42)) {
|
||||
return 6; // 42 bits
|
||||
} else if (value < (1ULL << 49)) {
|
||||
return 7; // 49 bits
|
||||
} else if (value < (1ULL << 56)) {
|
||||
return 8; // 56 bits
|
||||
} else if (value < (1ULL << 63)) {
|
||||
return 9; // 63 bits
|
||||
} else {
|
||||
return 10; // 64 bits (maximum for uint64_t)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode an int32_t value as a varint
|
||||
*
|
||||
* Special handling is needed for negative values, which are sign-extended to 64 bits
|
||||
* in Protocol Buffers, resulting in a 10-byte varint.
|
||||
*
|
||||
* @param value The int32_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(int32_t value) {
|
||||
// Negative values are sign-extended to 64 bits in protocol buffers,
|
||||
// which always results in a 10-byte varint for negative int32
|
||||
if (value < 0) {
|
||||
return 10; // Negative int32 is always 10 bytes long
|
||||
}
|
||||
// For non-negative values, use the uint32_t implementation
|
||||
return varint(static_cast<uint32_t>(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode an int64_t value as a varint
|
||||
*
|
||||
* @param value The int64_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(int64_t value) {
|
||||
// For int64_t, we convert to uint64_t and calculate the size
|
||||
// This works because the bit pattern determines the encoding size,
|
||||
// and we've handled negative int32 values as a special case above
|
||||
return varint(static_cast<uint64_t>(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a field ID and wire type
|
||||
*
|
||||
* @param field_id The field identifier
|
||||
* @param type The wire type value (from the WireType enum in the protobuf spec)
|
||||
* @return The number of bytes needed to encode the field ID and wire type
|
||||
*/
|
||||
static inline uint32_t field(uint32_t field_id, uint32_t type) {
|
||||
uint32_t tag = (field_id << 3) | (type & 0b111);
|
||||
return varint(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Common parameters for all add_*_field methods
|
||||
*
|
||||
* All add_*_field methods follow these common patterns:
|
||||
*
|
||||
* @param total_size Reference to the total message size to update
|
||||
* @param field_id_size Pre-calculated size of the field ID in bytes
|
||||
* @param value The value to calculate size for (type varies)
|
||||
* @param force Whether to calculate size even if the value is default/zero/empty
|
||||
*
|
||||
* Each method follows this implementation pattern:
|
||||
* 1. Skip calculation if value is default (0, false, empty) and not forced
|
||||
* 2. Calculate the size based on the field's encoding rules
|
||||
* 3. Add the field_id_size + calculated value size to total_size
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int32 field to the total message size
|
||||
*/
|
||||
static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
if (value < 0) {
|
||||
// Negative values are encoded as 10-byte varints in protobuf
|
||||
total_size += field_id_size + 10;
|
||||
} else {
|
||||
// For non-negative values, use the standard varint size
|
||||
total_size += field_id_size + varint(static_cast<uint32_t>(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint32 field to the total message size
|
||||
*/
|
||||
static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value,
|
||||
bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a boolean field to the total message size
|
||||
*/
|
||||
static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value, bool force = false) {
|
||||
// Skip calculation if value is false and not forced
|
||||
if (!value && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Boolean fields always use 1 byte when true
|
||||
total_size += field_id_size + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a fixed field to the total message size
|
||||
*
|
||||
* Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double).
|
||||
*
|
||||
* @tparam NumBytes The number of bytes for this fixed field (4 or 8)
|
||||
* @param is_nonzero Whether the value is non-zero
|
||||
*/
|
||||
template<uint32_t NumBytes>
|
||||
static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero,
|
||||
bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (!is_nonzero && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Fixed fields always take exactly NumBytes
|
||||
total_size += field_id_size + NumBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an enum field to the total message size
|
||||
*
|
||||
* Enum fields are encoded as uint32 varints.
|
||||
*/
|
||||
static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Enums are encoded as uint32
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a sint32 field to the total message size
|
||||
*
|
||||
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
|
||||
*/
|
||||
static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
|
||||
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
|
||||
total_size += field_id_size + varint(zigzag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int64 field to the total message size
|
||||
*/
|
||||
static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint64 field to the total message size
|
||||
*/
|
||||
static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value,
|
||||
bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a sint64 field to the total message size
|
||||
*
|
||||
* Sint64 fields use ZigZag encoding, which is more efficient for negative values.
|
||||
*/
|
||||
static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// ZigZag encoding for sint64: (n << 1) ^ (n >> 63)
|
||||
uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
|
||||
total_size += field_id_size + varint(zigzag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a string/bytes field to the total message size
|
||||
*/
|
||||
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str,
|
||||
bool force = false) {
|
||||
// Skip calculation if string is empty and not forced
|
||||
if (str.empty() && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
const uint32_t str_size = static_cast<uint32_t>(str.size());
|
||||
total_size += field_id_size + varint(str_size) + str_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size
|
||||
*
|
||||
* This helper function directly updates the total_size reference if the nested size
|
||||
* is greater than zero or force is true.
|
||||
*
|
||||
* @param nested_size The pre-calculated size of the nested message
|
||||
*/
|
||||
static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size,
|
||||
bool force = false) {
|
||||
// Skip calculation if nested message is empty and not forced
|
||||
if (nested_size == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
// Field ID + length varint + nested message content
|
||||
total_size += field_id_size + varint(nested_size) + nested_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size
|
||||
*
|
||||
* This templated version directly takes a message object, calculates its size internally,
|
||||
* and updates the total_size reference. This eliminates the need for a temporary variable
|
||||
* at the call site.
|
||||
*
|
||||
* @tparam MessageType The type of the nested message (inferred from parameter)
|
||||
* @param message The nested message object
|
||||
*/
|
||||
template<typename MessageType>
|
||||
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const MessageType &message,
|
||||
bool force = false) {
|
||||
uint32_t nested_size = 0;
|
||||
message.calculate_size(nested_size);
|
||||
|
||||
// Use the base implementation with the calculated nested_size
|
||||
add_message_field(total_size, field_id_size, nested_size, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size
|
||||
*
|
||||
* This helper processes a vector of message objects, calculating the size for each message
|
||||
* and adding it to the total size.
|
||||
*
|
||||
* @tparam MessageType The type of the nested messages in the vector
|
||||
* @param messages Vector of message objects
|
||||
*/
|
||||
template<typename MessageType>
|
||||
static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size,
|
||||
const std::vector<MessageType> &messages) {
|
||||
// Skip if the vector is empty
|
||||
if (messages.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For repeated fields, always use force=true
|
||||
for (const auto &message : messages) {
|
||||
add_message_object(total_size, field_id_size, message, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
@@ -126,19 +126,29 @@ void APIServer::loop() {
|
||||
conn->start();
|
||||
}
|
||||
|
||||
// Partition clients into remove and active
|
||||
auto new_end = std::partition(this->clients_.begin(), this->clients_.end(),
|
||||
[](const std::unique_ptr<APIConnection> &conn) { return !conn->remove_; });
|
||||
// print disconnection messages
|
||||
for (auto it = new_end; it != this->clients_.end(); ++it) {
|
||||
this->client_disconnected_trigger_->trigger((*it)->client_info_, (*it)->client_peername_);
|
||||
ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str());
|
||||
}
|
||||
// resize vector
|
||||
this->clients_.erase(new_end, this->clients_.end());
|
||||
// Process clients and remove disconnected ones in a single pass
|
||||
if (!this->clients_.empty()) {
|
||||
size_t client_index = 0;
|
||||
while (client_index < this->clients_.size()) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
for (auto &client : this->clients_) {
|
||||
client->loop();
|
||||
if (client->remove_) {
|
||||
// Handle disconnection
|
||||
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
|
||||
ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str());
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
} else {
|
||||
// Process active client
|
||||
client->loop();
|
||||
client_index++; // Move to next client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
|
||||
@@ -149,6 +149,18 @@ class ProtoWriteBuffer {
|
||||
void write(uint8_t value) { this->buffer_->push_back(value); }
|
||||
void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
|
||||
void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
|
||||
/**
|
||||
* Encode a field key (tag/wire type combination).
|
||||
*
|
||||
* @param field_id Field number (tag) in the protobuf message
|
||||
* @param type Wire type value:
|
||||
* - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum)
|
||||
* - 1: 64-bit (fixed64, sfixed64, double)
|
||||
* - 2: Length-delimited (string, bytes, embedded messages, packed repeated fields)
|
||||
* - 5: 32-bit (fixed32, sfixed32, float)
|
||||
*
|
||||
* Following https://protobuf.dev/programming-guides/encoding/#structure
|
||||
*/
|
||||
void encode_field_raw(uint32_t field_id, uint32_t type) {
|
||||
uint32_t val = (field_id << 3) | (type & 0b111);
|
||||
this->encode_varint_raw(val);
|
||||
@@ -157,7 +169,7 @@ class ProtoWriteBuffer {
|
||||
if (len == 0 && !force)
|
||||
return;
|
||||
|
||||
this->encode_field_raw(field_id, 2);
|
||||
this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
|
||||
this->encode_varint_raw(len);
|
||||
auto *data = reinterpret_cast<const uint8_t *>(string);
|
||||
this->buffer_->insert(this->buffer_->end(), data, data + len);
|
||||
@@ -171,26 +183,26 @@ class ProtoWriteBuffer {
|
||||
void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 0);
|
||||
this->encode_field_raw(field_id, 0); // type 0: Varint - uint32
|
||||
this->encode_varint_raw(value);
|
||||
}
|
||||
void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 0);
|
||||
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
|
||||
this->encode_varint_raw(ProtoVarInt(value));
|
||||
}
|
||||
void encode_bool(uint32_t field_id, bool value, bool force = false) {
|
||||
if (!value && !force)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 0);
|
||||
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
|
||||
this->write(0x01);
|
||||
}
|
||||
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
|
||||
this->encode_field_raw(field_id, 5);
|
||||
this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32
|
||||
this->write((value >> 0) & 0xFF);
|
||||
this->write((value >> 8) & 0xFF);
|
||||
this->write((value >> 16) & 0xFF);
|
||||
@@ -200,7 +212,7 @@ class ProtoWriteBuffer {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
|
||||
this->encode_field_raw(field_id, 5);
|
||||
this->encode_field_raw(field_id, 1); // type 1: 64-bit fixed64
|
||||
this->write((value >> 0) & 0xFF);
|
||||
this->write((value >> 8) & 0xFF);
|
||||
this->write((value >> 16) & 0xFF);
|
||||
@@ -254,7 +266,7 @@ class ProtoWriteBuffer {
|
||||
this->encode_uint64(field_id, uvalue, force);
|
||||
}
|
||||
template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) {
|
||||
this->encode_field_raw(field_id, 2);
|
||||
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
|
||||
size_t begin = this->buffer_->size();
|
||||
|
||||
value.encode(*this);
|
||||
@@ -276,6 +288,7 @@ class ProtoMessage {
|
||||
virtual ~ProtoMessage() = default;
|
||||
virtual void encode(ProtoWriteBuffer buffer) const = 0;
|
||||
void decode(const uint8_t *buffer, size_t length);
|
||||
virtual void calculate_size(uint32_t &total_size) const = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
std::string dump() const;
|
||||
virtual void dump_to(std::string &out) const = 0;
|
||||
@@ -298,13 +311,29 @@ class ProtoService {
|
||||
virtual void on_fatal_error() = 0;
|
||||
virtual void on_unauthenticated_access() = 0;
|
||||
virtual void on_no_setup_connection() = 0;
|
||||
virtual ProtoWriteBuffer create_buffer() = 0;
|
||||
/**
|
||||
* Create a buffer with a reserved size.
|
||||
* @param reserve_size The number of bytes to pre-allocate in the buffer. This is a hint
|
||||
* to optimize memory usage and avoid reallocations during encoding.
|
||||
* Implementations should aim to allocate at least this size.
|
||||
* @return A ProtoWriteBuffer object with the reserved size.
|
||||
*/
|
||||
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
|
||||
virtual bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) = 0;
|
||||
virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
|
||||
|
||||
// Optimized method that pre-allocates buffer based on message size
|
||||
template<class C> bool send_message_(const C &msg, uint32_t message_type) {
|
||||
auto buffer = this->create_buffer();
|
||||
uint32_t msg_size = 0;
|
||||
msg.calculate_size(msg_size);
|
||||
|
||||
// Create a pre-sized buffer
|
||||
auto buffer = this->create_buffer(msg_size);
|
||||
|
||||
// Encode message into the buffer
|
||||
msg.encode(buffer);
|
||||
|
||||
// Send the buffer
|
||||
return this->send_buffer(buffer, message_type);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,5 +3,6 @@ import esphome.codegen as cg
|
||||
CODEOWNERS = ["@circuitsetup", "@descipher"]
|
||||
|
||||
atm90e32_ns = cg.esphome_ns.namespace("atm90e32")
|
||||
ATM90E32Component = atm90e32_ns.class_("ATM90E32Component", cg.Component)
|
||||
|
||||
CONF_ATM90E32_ID = "atm90e32_id"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "atm90e32.h"
|
||||
#include "atm90e32_reg.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace atm90e32 {
|
||||
@@ -11,115 +11,84 @@ void ATM90E32Component::loop() {
|
||||
if (this->get_publish_interval_flag_()) {
|
||||
this->set_publish_interval_flag_(false);
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].voltage_sensor_ != nullptr) {
|
||||
if (this->phase_[phase].voltage_sensor_ != nullptr)
|
||||
this->phase_[phase].voltage_ = this->get_phase_voltage_(phase);
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].current_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].current_sensor_ != nullptr)
|
||||
this->phase_[phase].current_ = this->get_phase_current_(phase);
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].power_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].power_sensor_ != nullptr)
|
||||
this->phase_[phase].active_power_ = this->get_phase_active_power_(phase);
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].power_factor_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].power_factor_sensor_ != nullptr)
|
||||
this->phase_[phase].power_factor_ = this->get_phase_power_factor_(phase);
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].reactive_power_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].reactive_power_sensor_ != nullptr)
|
||||
this->phase_[phase].reactive_power_ = this->get_phase_reactive_power_(phase);
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].apparent_power_sensor_ != nullptr)
|
||||
this->phase_[phase].apparent_power_ = this->get_phase_apparent_power_(phase);
|
||||
|
||||
if (this->phase_[phase].forward_active_energy_sensor_ != nullptr)
|
||||
this->phase_[phase].forward_active_energy_ = this->get_phase_forward_active_energy_(phase);
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr)
|
||||
this->phase_[phase].reverse_active_energy_ = this->get_phase_reverse_active_energy_(phase);
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].phase_angle_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].phase_angle_sensor_ != nullptr)
|
||||
this->phase_[phase].phase_angle_ = this->get_phase_angle_(phase);
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr)
|
||||
this->phase_[phase].harmonic_active_power_ = this->get_phase_harmonic_active_power_(phase);
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].peak_current_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].peak_current_sensor_ != nullptr)
|
||||
this->phase_[phase].peak_current_ = this->get_phase_peak_current_(phase);
|
||||
}
|
||||
}
|
||||
// After the local store in collected we can publish them trusting they are withing +-1 haardware sampling
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].voltage_sensor_ != nullptr) {
|
||||
|
||||
// After the local store is collected we can publish them trusting they are within +-1 hardware sampling
|
||||
if (this->phase_[phase].voltage_sensor_ != nullptr)
|
||||
this->phase_[phase].voltage_sensor_->publish_state(this->get_local_phase_voltage_(phase));
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].current_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].current_sensor_ != nullptr)
|
||||
this->phase_[phase].current_sensor_->publish_state(this->get_local_phase_current_(phase));
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].power_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].power_sensor_ != nullptr)
|
||||
this->phase_[phase].power_sensor_->publish_state(this->get_local_phase_active_power_(phase));
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].power_factor_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].power_factor_sensor_ != nullptr)
|
||||
this->phase_[phase].power_factor_sensor_->publish_state(this->get_local_phase_power_factor_(phase));
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].reactive_power_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].reactive_power_sensor_ != nullptr)
|
||||
this->phase_[phase].reactive_power_sensor_->publish_state(this->get_local_phase_reactive_power_(phase));
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
|
||||
if (this->phase_[phase].apparent_power_sensor_ != nullptr)
|
||||
this->phase_[phase].apparent_power_sensor_->publish_state(this->get_local_phase_apparent_power_(phase));
|
||||
|
||||
if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) {
|
||||
this->phase_[phase].forward_active_energy_sensor_->publish_state(
|
||||
this->get_local_phase_forward_active_energy_(phase));
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
|
||||
if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) {
|
||||
this->phase_[phase].reverse_active_energy_sensor_->publish_state(
|
||||
this->get_local_phase_reverse_active_energy_(phase));
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].phase_angle_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].phase_angle_sensor_ != nullptr)
|
||||
this->phase_[phase].phase_angle_sensor_->publish_state(this->get_local_phase_angle_(phase));
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
|
||||
if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) {
|
||||
this->phase_[phase].harmonic_active_power_sensor_->publish_state(
|
||||
this->get_local_phase_harmonic_active_power_(phase));
|
||||
}
|
||||
}
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
if (this->phase_[phase].peak_current_sensor_ != nullptr) {
|
||||
|
||||
if (this->phase_[phase].peak_current_sensor_ != nullptr)
|
||||
this->phase_[phase].peak_current_sensor_->publish_state(this->get_local_phase_peak_current_(phase));
|
||||
}
|
||||
}
|
||||
if (this->freq_sensor_ != nullptr) {
|
||||
if (this->freq_sensor_ != nullptr)
|
||||
this->freq_sensor_->publish_state(this->get_frequency_());
|
||||
}
|
||||
if (this->chip_temperature_sensor_ != nullptr) {
|
||||
|
||||
if (this->chip_temperature_sensor_ != nullptr)
|
||||
this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,82 +99,30 @@ void ATM90E32Component::update() {
|
||||
}
|
||||
this->set_publish_interval_flag_(true);
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_calibrations_() {
|
||||
if (enable_offset_calibration_) {
|
||||
this->pref_.load(&this->offset_phase_);
|
||||
}
|
||||
};
|
||||
|
||||
void ATM90E32Component::run_offset_calibrations() {
|
||||
// Run the calibrations and
|
||||
// Setup voltage and current calibration offsets for PHASE A
|
||||
this->offset_phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA);
|
||||
this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_;
|
||||
this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset
|
||||
this->offset_phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA);
|
||||
this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_;
|
||||
this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset
|
||||
// Setup voltage and current calibration offsets for PHASE B
|
||||
this->offset_phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB);
|
||||
this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_;
|
||||
this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset
|
||||
this->offset_phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB);
|
||||
this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_;
|
||||
this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset
|
||||
// Setup voltage and current calibration offsets for PHASE C
|
||||
this->offset_phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC);
|
||||
this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_;
|
||||
this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset
|
||||
this->offset_phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC);
|
||||
this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_;
|
||||
this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset
|
||||
this->pref_.save(&this->offset_phase_);
|
||||
ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_,
|
||||
this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_);
|
||||
ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_,
|
||||
this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_);
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_offset_calibrations() {
|
||||
// Clear the calibrations and
|
||||
this->offset_phase_[PHASEA].voltage_offset_ = 0;
|
||||
this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_;
|
||||
this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset
|
||||
this->offset_phase_[PHASEA].current_offset_ = 0;
|
||||
this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_;
|
||||
this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset
|
||||
this->offset_phase_[PHASEB].voltage_offset_ = 0;
|
||||
this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_;
|
||||
this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset
|
||||
this->offset_phase_[PHASEB].current_offset_ = 0;
|
||||
this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_;
|
||||
this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset
|
||||
this->offset_phase_[PHASEC].voltage_offset_ = 0;
|
||||
this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_;
|
||||
this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset
|
||||
this->offset_phase_[PHASEC].current_offset_ = 0;
|
||||
this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_;
|
||||
this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset
|
||||
this->pref_.save(&this->offset_phase_);
|
||||
ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_,
|
||||
this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_);
|
||||
ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_,
|
||||
this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_);
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
this->check_phase_status();
|
||||
this->check_over_current();
|
||||
this->check_freq_status();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ATM90E32Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component...");
|
||||
this->spi_setup();
|
||||
if (this->enable_offset_calibration_) {
|
||||
uint32_t hash = fnv1_hash(App.get_friendly_name());
|
||||
this->pref_ = global_preferences->make_preference<Calibration[3]>(hash, true);
|
||||
this->restore_calibrations_();
|
||||
}
|
||||
|
||||
uint16_t mmode0 = 0x87; // 3P4W 50Hz
|
||||
uint16_t high_thresh = 0;
|
||||
uint16_t low_thresh = 0;
|
||||
|
||||
if (line_freq_ == 60) {
|
||||
mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz
|
||||
// for freq threshold registers
|
||||
high_thresh = 6300; // 63.00 Hz
|
||||
low_thresh = 5700; // 57.00 Hz
|
||||
} else {
|
||||
high_thresh = 5300; // 53.00 Hz
|
||||
low_thresh = 4700; // 47.00 Hz
|
||||
}
|
||||
|
||||
if (current_phases_ == 2) {
|
||||
@@ -216,34 +133,84 @@ void ATM90E32Component::setup() {
|
||||
this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset
|
||||
delay(6); // Wait for the minimum 5ms + 1ms
|
||||
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access
|
||||
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != 0x55AA) {
|
||||
if (!this->validate_spi_read_(0x55AA, "setup()")) {
|
||||
ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->write16_(ATM90E32_REGISTER_METEREN, 0x0001); // Enable Metering
|
||||
this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F); // Peak Detector time ms (15:8), Sag Period ms (7:0)
|
||||
this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F); // Peak Detector time (15:8) 255ms, Sag Period (7:0) 63ms
|
||||
this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861); // PL Constant MSB (default) = 140625000
|
||||
this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468); // PL Constant LSB (default)
|
||||
this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // ZX2, ZX1, ZX0 pin config
|
||||
this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // Zero crossing (ZX2, ZX1, ZX0) pin config
|
||||
this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program)
|
||||
this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels
|
||||
this->write16_(ATM90E32_REGISTER_FREQHITH, high_thresh); // Frequency high threshold
|
||||
this->write16_(ATM90E32_REGISTER_FREQLOTH, low_thresh); // Frequency low threshold
|
||||
this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500
|
||||
this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50%
|
||||
this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50%
|
||||
this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750
|
||||
this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10%
|
||||
// Setup voltage and current gain for PHASE A
|
||||
this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[PHASEA].voltage_gain_); // A Voltage rms gain
|
||||
this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[PHASEA].ct_gain_); // A line current gain
|
||||
// Setup voltage and current gain for PHASE B
|
||||
this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[PHASEB].voltage_gain_); // B Voltage rms gain
|
||||
this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[PHASEB].ct_gain_); // B line current gain
|
||||
// Setup voltage and current gain for PHASE C
|
||||
this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[PHASEC].voltage_gain_); // C Voltage rms gain
|
||||
this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[PHASEC].ct_gain_); // C line current gain
|
||||
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration
|
||||
|
||||
if (this->enable_offset_calibration_) {
|
||||
// Initialize flash storage for offset calibrations
|
||||
uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_->dump_summary());
|
||||
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
|
||||
this->restore_offset_calibrations_();
|
||||
|
||||
// Initialize flash storage for power offset calibrations
|
||||
uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_->dump_summary());
|
||||
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
|
||||
this->restore_power_offset_calibrations_();
|
||||
} else {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Power & Voltage/Current offset calibration is disabled. Using config file values.");
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
this->write16_(this->voltage_offset_registers[phase],
|
||||
static_cast<uint16_t>(this->offset_phase_[phase].voltage_offset_));
|
||||
this->write16_(this->current_offset_registers[phase],
|
||||
static_cast<uint16_t>(this->offset_phase_[phase].current_offset_));
|
||||
this->write16_(this->power_offset_registers[phase],
|
||||
static_cast<uint16_t>(this->power_offset_phase_[phase].active_power_offset));
|
||||
this->write16_(this->reactive_power_offset_registers[phase],
|
||||
static_cast<uint16_t>(this->power_offset_phase_[phase].reactive_power_offset));
|
||||
}
|
||||
}
|
||||
|
||||
if (this->enable_gain_calibration_) {
|
||||
// Initialize flash storage for gain calibration
|
||||
uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_->dump_summary());
|
||||
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
|
||||
this->restore_gain_calibrations_();
|
||||
|
||||
if (this->using_saved_calibrations_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored gain calibration from memory.");
|
||||
} else {
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
|
||||
this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration is disabled. Using config file values.");
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
|
||||
this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
|
||||
}
|
||||
}
|
||||
|
||||
// Sag threshold (78%)
|
||||
uint16_t sagth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 0.78f);
|
||||
// Overvoltage threshold (122%)
|
||||
uint16_t ovth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 1.22f);
|
||||
|
||||
// Write to registers
|
||||
this->write16_(ATM90E32_REGISTER_SAGTH, sagth);
|
||||
this->write16_(ATM90E32_REGISTER_OVTH, ovth);
|
||||
|
||||
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration
|
||||
}
|
||||
|
||||
void ATM90E32Component::dump_config() {
|
||||
@@ -257,6 +224,7 @@ void ATM90E32Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Current A", this->phase_[PHASEA].current_sensor_);
|
||||
LOG_SENSOR(" ", "Power A", this->phase_[PHASEA].power_sensor_);
|
||||
LOG_SENSOR(" ", "Reactive Power A", this->phase_[PHASEA].reactive_power_sensor_);
|
||||
LOG_SENSOR(" ", "Apparent Power A", this->phase_[PHASEA].apparent_power_sensor_);
|
||||
LOG_SENSOR(" ", "PF A", this->phase_[PHASEA].power_factor_sensor_);
|
||||
LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[PHASEA].forward_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[PHASEA].reverse_active_energy_sensor_);
|
||||
@@ -267,22 +235,24 @@ void ATM90E32Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Current B", this->phase_[PHASEB].current_sensor_);
|
||||
LOG_SENSOR(" ", "Power B", this->phase_[PHASEB].power_sensor_);
|
||||
LOG_SENSOR(" ", "Reactive Power B", this->phase_[PHASEB].reactive_power_sensor_);
|
||||
LOG_SENSOR(" ", "Apparent Power B", this->phase_[PHASEB].apparent_power_sensor_);
|
||||
LOG_SENSOR(" ", "PF B", this->phase_[PHASEB].power_factor_sensor_);
|
||||
LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[PHASEB].forward_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[PHASEB].reverse_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEB].harmonic_active_power_sensor_);
|
||||
LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEB].phase_angle_sensor_);
|
||||
LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEB].peak_current_sensor_);
|
||||
LOG_SENSOR(" ", "Harmonic Power B", this->phase_[PHASEB].harmonic_active_power_sensor_);
|
||||
LOG_SENSOR(" ", "Phase Angle B", this->phase_[PHASEB].phase_angle_sensor_);
|
||||
LOG_SENSOR(" ", "Peak Current B", this->phase_[PHASEB].peak_current_sensor_);
|
||||
LOG_SENSOR(" ", "Voltage C", this->phase_[PHASEC].voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current C", this->phase_[PHASEC].current_sensor_);
|
||||
LOG_SENSOR(" ", "Power C", this->phase_[PHASEC].power_sensor_);
|
||||
LOG_SENSOR(" ", "Reactive Power C", this->phase_[PHASEC].reactive_power_sensor_);
|
||||
LOG_SENSOR(" ", "Apparent Power C", this->phase_[PHASEC].apparent_power_sensor_);
|
||||
LOG_SENSOR(" ", "PF C", this->phase_[PHASEC].power_factor_sensor_);
|
||||
LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[PHASEC].forward_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[PHASEC].reverse_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEC].harmonic_active_power_sensor_);
|
||||
LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEC].phase_angle_sensor_);
|
||||
LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEC].peak_current_sensor_);
|
||||
LOG_SENSOR(" ", "Harmonic Power C", this->phase_[PHASEC].harmonic_active_power_sensor_);
|
||||
LOG_SENSOR(" ", "Phase Angle C", this->phase_[PHASEC].phase_angle_sensor_);
|
||||
LOG_SENSOR(" ", "Peak Current C", this->phase_[PHASEC].peak_current_sensor_);
|
||||
LOG_SENSOR(" ", "Frequency", this->freq_sensor_);
|
||||
LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_);
|
||||
}
|
||||
@@ -298,7 +268,7 @@ uint16_t ATM90E32Component::read16_(uint16_t a_register) {
|
||||
uint8_t data[2];
|
||||
uint16_t output;
|
||||
this->enable();
|
||||
delay_microseconds_safe(10);
|
||||
delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1ms is plenty
|
||||
this->write_byte(addrh);
|
||||
this->write_byte(addrl);
|
||||
this->read_array(data, 2);
|
||||
@@ -328,8 +298,7 @@ void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) {
|
||||
this->write_byte16(a_register);
|
||||
this->write_byte16(val);
|
||||
this->disable();
|
||||
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != val)
|
||||
ESP_LOGW(TAG, "SPI write error 0x%04X val 0x%04X", a_register, val);
|
||||
this->validate_spi_read_(val, "write16()");
|
||||
}
|
||||
|
||||
float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; }
|
||||
@@ -340,6 +309,8 @@ float ATM90E32Component::get_local_phase_active_power_(uint8_t phase) { return t
|
||||
|
||||
float ATM90E32Component::get_local_phase_reactive_power_(uint8_t phase) { return this->phase_[phase].reactive_power_; }
|
||||
|
||||
float ATM90E32Component::get_local_phase_apparent_power_(uint8_t phase) { return this->phase_[phase].apparent_power_; }
|
||||
|
||||
float ATM90E32Component::get_local_phase_power_factor_(uint8_t phase) { return this->phase_[phase].power_factor_; }
|
||||
|
||||
float ATM90E32Component::get_local_phase_forward_active_energy_(uint8_t phase) {
|
||||
@@ -360,8 +331,7 @@ float ATM90E32Component::get_local_phase_peak_current_(uint8_t phase) { return t
|
||||
|
||||
float ATM90E32Component::get_phase_voltage_(uint8_t phase) {
|
||||
const uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMS + phase);
|
||||
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage)
|
||||
ESP_LOGW(TAG, "SPI URMS voltage register read error.");
|
||||
this->validate_spi_read_(voltage, "get_phase_voltage()");
|
||||
return (float) voltage / 100;
|
||||
}
|
||||
|
||||
@@ -371,8 +341,7 @@ float ATM90E32Component::get_phase_voltage_avg_(uint8_t phase) {
|
||||
uint16_t voltage = 0;
|
||||
for (uint8_t i = 0; i < reads; i++) {
|
||||
voltage = this->read16_(ATM90E32_REGISTER_URMS + phase);
|
||||
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage)
|
||||
ESP_LOGW(TAG, "SPI URMS voltage register read error.");
|
||||
this->validate_spi_read_(voltage, "get_phase_voltage_avg_()");
|
||||
accumulation += voltage;
|
||||
}
|
||||
voltage = accumulation / reads;
|
||||
@@ -386,8 +355,7 @@ float ATM90E32Component::get_phase_current_avg_(uint8_t phase) {
|
||||
uint16_t current = 0;
|
||||
for (uint8_t i = 0; i < reads; i++) {
|
||||
current = this->read16_(ATM90E32_REGISTER_IRMS + phase);
|
||||
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current)
|
||||
ESP_LOGW(TAG, "SPI IRMS current register read error.");
|
||||
this->validate_spi_read_(current, "get_phase_current_avg_()");
|
||||
accumulation += current;
|
||||
}
|
||||
current = accumulation / reads;
|
||||
@@ -397,8 +365,7 @@ float ATM90E32Component::get_phase_current_avg_(uint8_t phase) {
|
||||
|
||||
float ATM90E32Component::get_phase_current_(uint8_t phase) {
|
||||
const uint16_t current = this->read16_(ATM90E32_REGISTER_IRMS + phase);
|
||||
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current)
|
||||
ESP_LOGW(TAG, "SPI IRMS current register read error.");
|
||||
this->validate_spi_read_(current, "get_phase_current_()");
|
||||
return (float) current / 1000;
|
||||
}
|
||||
|
||||
@@ -412,11 +379,15 @@ float ATM90E32Component::get_phase_reactive_power_(uint8_t phase) {
|
||||
return val * 0.00032f;
|
||||
}
|
||||
|
||||
float ATM90E32Component::get_phase_apparent_power_(uint8_t phase) {
|
||||
const int val = this->read32_(ATM90E32_REGISTER_SMEAN + phase, ATM90E32_REGISTER_SMEANLSB + phase);
|
||||
return val * 0.00032f;
|
||||
}
|
||||
|
||||
float ATM90E32Component::get_phase_power_factor_(uint8_t phase) {
|
||||
const int16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase);
|
||||
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != powerfactor)
|
||||
ESP_LOGW(TAG, "SPI power factor read error.");
|
||||
return (float) powerfactor / 1000;
|
||||
uint16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase); // unsigned to compare to lastspidata
|
||||
this->validate_spi_read_(powerfactor, "get_phase_power_factor_()");
|
||||
return (float) ((int16_t) powerfactor) / 1000; // make it signed again
|
||||
}
|
||||
|
||||
float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) {
|
||||
@@ -426,17 +397,19 @@ float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) {
|
||||
} else {
|
||||
this->phase_[phase].cumulative_forward_active_energy_ = val;
|
||||
}
|
||||
return ((float) this->phase_[phase].cumulative_forward_active_energy_ * 10 / 3200);
|
||||
// 0.01CF resolution = 0.003125 Wh per count
|
||||
return ((float) this->phase_[phase].cumulative_forward_active_energy_ * (10.0f / 3200.0f));
|
||||
}
|
||||
|
||||
float ATM90E32Component::get_phase_reverse_active_energy_(uint8_t phase) {
|
||||
const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY);
|
||||
const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY + phase);
|
||||
if (UINT32_MAX - this->phase_[phase].cumulative_reverse_active_energy_ > val) {
|
||||
this->phase_[phase].cumulative_reverse_active_energy_ += val;
|
||||
} else {
|
||||
this->phase_[phase].cumulative_reverse_active_energy_ = val;
|
||||
}
|
||||
return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * 10 / 3200);
|
||||
// 0.01CF resolution = 0.003125 Wh per count
|
||||
return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * (10.0f / 3200.0f));
|
||||
}
|
||||
|
||||
float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) {
|
||||
@@ -446,15 +419,15 @@ float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) {
|
||||
|
||||
float ATM90E32Component::get_phase_angle_(uint8_t phase) {
|
||||
uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0;
|
||||
return (float) (val > 180) ? val - 360.0 : val;
|
||||
return (val > 180) ? (float) (val - 360.0f) : (float) val;
|
||||
}
|
||||
|
||||
float ATM90E32Component::get_phase_peak_current_(uint8_t phase) {
|
||||
int16_t val = (float) this->read16_(ATM90E32_REGISTER_IPEAK + phase);
|
||||
if (!this->peak_current_signed_)
|
||||
val = abs(val);
|
||||
val = std::abs(val);
|
||||
// phase register * phase current gain value / 1000 * 2^13
|
||||
return (float) (val * this->phase_[phase].ct_gain_ / 8192000.0);
|
||||
return (val * this->phase_[phase].ct_gain_ / 8192000.0);
|
||||
}
|
||||
|
||||
float ATM90E32Component::get_frequency_() {
|
||||
@@ -467,29 +440,433 @@ float ATM90E32Component::get_chip_temperature_() {
|
||||
return (float) ctemp;
|
||||
}
|
||||
|
||||
uint16_t ATM90E32Component::calibrate_voltage_offset_phase(uint8_t phase) {
|
||||
const uint8_t num_reads = 5;
|
||||
uint64_t total_value = 0;
|
||||
for (int i = 0; i < num_reads; ++i) {
|
||||
const uint32_t measurement_value = read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase);
|
||||
total_value += measurement_value;
|
||||
void ATM90E32Component::run_gain_calibrations() {
|
||||
if (!this->enable_gain_calibration_) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Gain calibration is disabled! Enable it first with enable_gain_calibration: true");
|
||||
return;
|
||||
}
|
||||
const uint32_t average_value = total_value / num_reads;
|
||||
const uint32_t shifted_value = average_value >> 7;
|
||||
const uint32_t voltage_offset = ~shifted_value + 1;
|
||||
return voltage_offset & 0xFFFF; // Take the lower 16 bits
|
||||
|
||||
float ref_voltages[3] = {
|
||||
this->get_reference_voltage(0),
|
||||
this->get_reference_voltage(1),
|
||||
this->get_reference_voltage(2),
|
||||
};
|
||||
float ref_currents[3] = {this->get_reference_current(0), this->get_reference_current(1),
|
||||
this->get_reference_current(2)};
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION] ");
|
||||
ESP_LOGI(TAG, "[CALIBRATION] ========================= Gain Calibration =========================");
|
||||
ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------");
|
||||
ESP_LOGI(TAG,
|
||||
"[CALIBRATION] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |");
|
||||
ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------");
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
float measured_voltage = this->get_phase_voltage_avg_(phase);
|
||||
float measured_current = this->get_phase_current_avg_(phase);
|
||||
|
||||
float ref_voltage = ref_voltages[phase];
|
||||
float ref_current = ref_currents[phase];
|
||||
|
||||
uint16_t current_voltage_gain = this->read16_(voltage_gain_registers[phase]);
|
||||
uint16_t current_current_gain = this->read16_(current_gain_registers[phase]);
|
||||
|
||||
bool did_voltage = false;
|
||||
bool did_current = false;
|
||||
|
||||
// Voltage calibration
|
||||
if (ref_voltage <= 0.0f) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: reference voltage is 0.",
|
||||
phase_labels[phase]);
|
||||
} else if (measured_voltage == 0.0f) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: measured voltage is 0.",
|
||||
phase_labels[phase]);
|
||||
} else {
|
||||
uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain);
|
||||
if (new_voltage_gain == 0) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Voltage gain would be 0. Check reference and measured voltage.",
|
||||
phase_labels[phase]);
|
||||
} else {
|
||||
if (new_voltage_gain >= 65535) {
|
||||
ESP_LOGW(
|
||||
TAG,
|
||||
"[CALIBRATION] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage transformer.",
|
||||
phase_labels[phase]);
|
||||
new_voltage_gain = 65535;
|
||||
}
|
||||
this->gain_phase_[phase].voltage_gain = static_cast<uint16_t>(new_voltage_gain);
|
||||
did_voltage = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Current calibration
|
||||
if (ref_current == 0.0f) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: reference current is 0.",
|
||||
phase_labels[phase]);
|
||||
} else if (measured_current == 0.0f) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: measured current is 0.",
|
||||
phase_labels[phase]);
|
||||
} else {
|
||||
uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain);
|
||||
if (new_current_gain == 0) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain would be 0. Check reference and measured current.",
|
||||
phase_labels[phase]);
|
||||
} else {
|
||||
if (new_current_gain >= 65535) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.",
|
||||
phase_labels[phase]);
|
||||
new_current_gain = 65535;
|
||||
}
|
||||
this->gain_phase_[phase].current_gain = static_cast<uint16_t>(new_current_gain);
|
||||
did_current = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Final row output
|
||||
ESP_LOGI(TAG, "[CALIBRATION] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |",
|
||||
'A' + phase, measured_voltage, measured_current, ref_voltage, ref_current, current_voltage_gain,
|
||||
did_voltage ? this->gain_phase_[phase].voltage_gain : current_voltage_gain, current_current_gain,
|
||||
did_current ? this->gain_phase_[phase].current_gain : current_current_gain);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION] =====================================================================\n");
|
||||
|
||||
this->save_gain_calibration_to_memory_();
|
||||
this->write_gains_to_registers_();
|
||||
this->verify_gain_writes_();
|
||||
}
|
||||
|
||||
uint16_t ATM90E32Component::calibrate_current_offset_phase(uint8_t phase) {
|
||||
void ATM90E32Component::save_gain_calibration_to_memory_() {
|
||||
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
|
||||
if (success) {
|
||||
this->using_saved_calibrations_ = true;
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration saved to memory.");
|
||||
} else {
|
||||
this->using_saved_calibrations_ = false;
|
||||
ESP_LOGE(TAG, "[CALIBRATION] Failed to save gain calibration to memory!");
|
||||
}
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_offset_calibrations() {
|
||||
if (!this->enable_offset_calibration_) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Offset calibration is disabled! Enable it first with enable_offset_calibration: true");
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
int16_t voltage_offset = calibrate_offset(phase, true);
|
||||
int16_t current_offset = calibrate_offset(phase, false);
|
||||
|
||||
this->write_offsets_to_registers_(phase, voltage_offset, current_offset);
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage: %d, offset_current: %d", 'A' + phase, voltage_offset,
|
||||
current_offset);
|
||||
}
|
||||
|
||||
this->offset_pref_.save(&this->offset_phase_); // Save to flash
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_power_offset_calibrations() {
|
||||
if (!this->enable_offset_calibration_) {
|
||||
ESP_LOGW(
|
||||
TAG,
|
||||
"[CALIBRATION] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true");
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
int16_t active_offset = calibrate_power_offset(phase, false);
|
||||
int16_t reactive_offset = calibrate_power_offset(phase, true);
|
||||
|
||||
this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset);
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase,
|
||||
active_offset, reactive_offset);
|
||||
}
|
||||
|
||||
this->power_offset_pref_.save(&this->power_offset_phase_); // Save to flash
|
||||
}
|
||||
|
||||
void ATM90E32Component::write_gains_to_registers_() {
|
||||
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
|
||||
|
||||
for (int phase = 0; phase < 3; phase++) {
|
||||
this->write16_(voltage_gain_registers[phase], this->gain_phase_[phase].voltage_gain);
|
||||
this->write16_(current_gain_registers[phase], this->gain_phase_[phase].current_gain);
|
||||
}
|
||||
|
||||
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
|
||||
}
|
||||
|
||||
void ATM90E32Component::write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset) {
|
||||
// Save to runtime
|
||||
this->offset_phase_[phase].voltage_offset_ = voltage_offset;
|
||||
this->phase_[phase].voltage_offset_ = voltage_offset;
|
||||
|
||||
// Save to flash-storable struct
|
||||
this->offset_phase_[phase].current_offset_ = current_offset;
|
||||
this->phase_[phase].current_offset_ = current_offset;
|
||||
|
||||
// Write to registers
|
||||
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
|
||||
this->write16_(voltage_offset_registers[phase], static_cast<uint16_t>(voltage_offset));
|
||||
this->write16_(current_offset_registers[phase], static_cast<uint16_t>(current_offset));
|
||||
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
|
||||
}
|
||||
|
||||
void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset) {
|
||||
// Save to runtime
|
||||
this->phase_[phase].active_power_offset_ = p_offset;
|
||||
this->phase_[phase].reactive_power_offset_ = q_offset;
|
||||
|
||||
// Save to flash-storable struct
|
||||
this->power_offset_phase_[phase].active_power_offset = p_offset;
|
||||
this->power_offset_phase_[phase].reactive_power_offset = q_offset;
|
||||
|
||||
// Write to registers
|
||||
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
|
||||
this->write16_(this->power_offset_registers[phase], static_cast<uint16_t>(p_offset));
|
||||
this->write16_(this->reactive_power_offset_registers[phase], static_cast<uint16_t>(q_offset));
|
||||
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_gain_calibrations_() {
|
||||
if (this->gain_calibration_pref_.load(&this->gain_phase_)) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Restoring saved gain calibrations to registers:");
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
uint16_t v_gain = this->gain_phase_[phase].voltage_gain;
|
||||
uint16_t i_gain = this->gain_phase_[phase].current_gain;
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, v_gain, i_gain);
|
||||
}
|
||||
|
||||
this->write_gains_to_registers_();
|
||||
|
||||
if (this->verify_gain_writes_()) {
|
||||
this->using_saved_calibrations_ = true;
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration loaded and verified successfully.");
|
||||
} else {
|
||||
this->using_saved_calibrations_ = false;
|
||||
ESP_LOGE(TAG, "[CALIBRATION] Gain verification failed! Calibration may not be applied correctly.");
|
||||
}
|
||||
} else {
|
||||
this->using_saved_calibrations_ = false;
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No stored gain calibrations found. Using config file values.");
|
||||
}
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_offset_calibrations_() {
|
||||
if (this->offset_pref_.load(&this->offset_phase_)) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored offset calibration from memory.");
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
auto &offset = this->offset_phase_[phase];
|
||||
write_offsets_to_registers_(phase, offset.voltage_offset_, offset.current_offset_);
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage:: %d, offset_current: %d", 'A' + phase,
|
||||
offset.voltage_offset_, offset.current_offset_);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No stored offset calibrations found. Using default values.");
|
||||
}
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_power_offset_calibrations_() {
|
||||
if (this->power_offset_pref_.load(&this->power_offset_phase_)) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored power offset calibration from memory.");
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
auto &offset = this->power_offset_phase_[phase];
|
||||
write_power_offsets_to_registers_(phase, offset.active_power_offset, offset.reactive_power_offset);
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase,
|
||||
offset.active_power_offset, offset.reactive_power_offset);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No stored power offsets found. Using default values.");
|
||||
}
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_gain_calibrations() {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Clearing stored gain calibrations and restoring config-defined values...");
|
||||
|
||||
for (int phase = 0; phase < 3; phase++) {
|
||||
gain_phase_[phase].voltage_gain = this->phase_[phase].voltage_gain_;
|
||||
gain_phase_[phase].current_gain = this->phase_[phase].ct_gain_;
|
||||
}
|
||||
|
||||
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
|
||||
this->using_saved_calibrations_ = false;
|
||||
|
||||
if (success) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Gain calibrations cleared. Config values restored:");
|
||||
for (int phase = 0; phase < 3; phase++) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase,
|
||||
gain_phase_[phase].voltage_gain, gain_phase_[phase].current_gain);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "[CALIBRATION] Failed to clear gain calibrations!");
|
||||
}
|
||||
|
||||
this->write_gains_to_registers_(); // Apply them to the chip immediately
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_offset_calibrations() {
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
this->write_offsets_to_registers_(phase, 0, 0);
|
||||
}
|
||||
|
||||
this->offset_pref_.save(&this->offset_phase_); // Save cleared values to flash memory
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Offsets cleared.");
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_power_offset_calibrations() {
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
this->write_power_offsets_to_registers_(phase, 0, 0);
|
||||
}
|
||||
|
||||
this->power_offset_pref_.save(&this->power_offset_phase_);
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Power offsets cleared.");
|
||||
}
|
||||
|
||||
int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) {
|
||||
const uint8_t num_reads = 5;
|
||||
uint64_t total_value = 0;
|
||||
for (int i = 0; i < num_reads; ++i) {
|
||||
const uint32_t measurement_value = read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase);
|
||||
total_value += measurement_value;
|
||||
|
||||
for (uint8_t i = 0; i < num_reads; ++i) {
|
||||
uint32_t reading = voltage ? this->read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase)
|
||||
: this->read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase);
|
||||
total_value += reading;
|
||||
}
|
||||
|
||||
const uint32_t average_value = total_value / num_reads;
|
||||
const uint32_t current_offset = ~average_value + 1;
|
||||
return current_offset & 0xFFFF; // Take the lower 16 bits
|
||||
const uint32_t shifted = average_value >> 7;
|
||||
const uint32_t offset = ~shifted + 1;
|
||||
return static_cast<int16_t>(offset); // Takes lower 16 bits
|
||||
}
|
||||
|
||||
int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) {
|
||||
const uint8_t num_reads = 5;
|
||||
uint64_t total_value = 0;
|
||||
|
||||
for (uint8_t i = 0; i < num_reads; ++i) {
|
||||
uint32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase)
|
||||
: this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase);
|
||||
total_value += reading;
|
||||
}
|
||||
|
||||
const uint32_t average_value = total_value / num_reads;
|
||||
const uint32_t power_offset = ~average_value + 1;
|
||||
return static_cast<int16_t>(power_offset); // Takes the lower 16 bits
|
||||
}
|
||||
|
||||
bool ATM90E32Component::verify_gain_writes_() {
|
||||
bool success = true;
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);
|
||||
uint16_t read_current = this->read16_(current_gain_registers[phase]);
|
||||
|
||||
if (read_voltage != this->gain_phase_[phase].voltage_gain ||
|
||||
read_current != this->gain_phase_[phase].current_gain) {
|
||||
ESP_LOGE(TAG, "[CALIBRATION] Mismatch detected for Phase %s!", phase_labels[phase]);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
return success; // Return true if all writes were successful, false otherwise
|
||||
}
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void ATM90E32Component::check_phase_status() {
|
||||
uint16_t state0 = this->read16_(ATM90E32_REGISTER_EMMSTATE0);
|
||||
uint16_t state1 = this->read16_(ATM90E32_REGISTER_EMMSTATE1);
|
||||
|
||||
for (int phase = 0; phase < 3; phase++) {
|
||||
std::string status;
|
||||
|
||||
if (state0 & over_voltage_flags[phase])
|
||||
status += "Over Voltage; ";
|
||||
if (state1 & voltage_sag_flags[phase])
|
||||
status += "Voltage Sag; ";
|
||||
if (state1 & phase_loss_flags[phase])
|
||||
status += "Phase Loss; ";
|
||||
|
||||
auto *sensor = this->phase_status_text_sensor_[phase];
|
||||
const char *phase_name = sensor ? sensor->get_name().c_str() : "Unknown Phase";
|
||||
if (!status.empty()) {
|
||||
status.pop_back(); // remove space
|
||||
status.pop_back(); // remove semicolon
|
||||
ESP_LOGW(TAG, "%s: %s", phase_name, status.c_str());
|
||||
if (sensor != nullptr)
|
||||
sensor->publish_state(status);
|
||||
} else {
|
||||
if (sensor != nullptr)
|
||||
sensor->publish_state("Okay");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ATM90E32Component::check_freq_status() {
|
||||
uint16_t state1 = this->read16_(ATM90E32_REGISTER_EMMSTATE1);
|
||||
|
||||
std::string freq_status;
|
||||
|
||||
if (state1 & ATM90E32_STATUS_S1_FREQHIST) {
|
||||
freq_status = "HIGH";
|
||||
} else if (state1 & ATM90E32_STATUS_S1_FREQLOST) {
|
||||
freq_status = "LOW";
|
||||
} else {
|
||||
freq_status = "Normal";
|
||||
}
|
||||
ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str());
|
||||
|
||||
if (this->freq_status_text_sensor_ != nullptr) {
|
||||
this->freq_status_text_sensor_->publish_state(freq_status);
|
||||
}
|
||||
}
|
||||
|
||||
void ATM90E32Component::check_over_current() {
|
||||
constexpr float max_current_threshold = 65.53f;
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
float current_val =
|
||||
this->phase_[phase].current_sensor_ != nullptr ? this->phase_[phase].current_sensor_->state : 0.0f;
|
||||
|
||||
if (current_val > max_current_threshold) {
|
||||
ESP_LOGW(TAG, "Over current detected on Phase %c: %.2f A", 'A' + phase, current_val);
|
||||
ESP_LOGW(TAG, "You may need to half your gain_ct: value & multiply the current and power values by 2");
|
||||
if (this->phase_status_text_sensor_[phase] != nullptr) {
|
||||
this->phase_status_text_sensor_[phase]->publish_state("Over Current; ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
uint16_t ATM90E32Component::calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier) {
|
||||
// this assumes that 60Hz electrical systems use 120V mains,
|
||||
// which is usually, but not always the case
|
||||
float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f;
|
||||
float target_voltage = nominal_voltage * multiplier;
|
||||
|
||||
float peak_01v = target_voltage * 100.0f * std::sqrt(2.0f); // convert RMS → peak, scale to 0.01V
|
||||
float divider = (2.0f * ugain) / 32768.0f;
|
||||
|
||||
float threshold = peak_01v / divider;
|
||||
|
||||
return static_cast<uint16_t>(threshold);
|
||||
}
|
||||
|
||||
bool ATM90E32Component::validate_spi_read_(uint16_t expected, const char *context) {
|
||||
uint16_t last = this->read16_(ATM90E32_REGISTER_LASTSPIDATA);
|
||||
if (last != expected) {
|
||||
if (context != nullptr) {
|
||||
ESP_LOGW(TAG, "[%s] SPI read mismatch: expected 0x%04X, got 0x%04X", context, expected, last);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "SPI read mismatch: expected 0x%04X, got 0x%04X", expected, last);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace atm90e32
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <unordered_map>
|
||||
#include "atm90e32_reg.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
@@ -18,6 +19,26 @@ class ATM90E32Component : public PollingComponent,
|
||||
static const uint8_t PHASEA = 0;
|
||||
static const uint8_t PHASEB = 1;
|
||||
static const uint8_t PHASEC = 2;
|
||||
const char *phase_labels[3] = {"A", "B", "C"};
|
||||
// these registers are not sucessive, so we can't just do 'base + phase'
|
||||
const uint16_t voltage_gain_registers[3] = {ATM90E32_REGISTER_UGAINA, ATM90E32_REGISTER_UGAINB,
|
||||
ATM90E32_REGISTER_UGAINC};
|
||||
const uint16_t current_gain_registers[3] = {ATM90E32_REGISTER_IGAINA, ATM90E32_REGISTER_IGAINB,
|
||||
ATM90E32_REGISTER_IGAINC};
|
||||
const uint16_t voltage_offset_registers[3] = {ATM90E32_REGISTER_UOFFSETA, ATM90E32_REGISTER_UOFFSETB,
|
||||
ATM90E32_REGISTER_UOFFSETC};
|
||||
const uint16_t current_offset_registers[3] = {ATM90E32_REGISTER_IOFFSETA, ATM90E32_REGISTER_IOFFSETB,
|
||||
ATM90E32_REGISTER_IOFFSETC};
|
||||
const uint16_t power_offset_registers[3] = {ATM90E32_REGISTER_POFFSETA, ATM90E32_REGISTER_POFFSETB,
|
||||
ATM90E32_REGISTER_POFFSETC};
|
||||
const uint16_t reactive_power_offset_registers[3] = {ATM90E32_REGISTER_QOFFSETA, ATM90E32_REGISTER_QOFFSETB,
|
||||
ATM90E32_REGISTER_QOFFSETC};
|
||||
const uint16_t over_voltage_flags[3] = {ATM90E32_STATUS_S0_OVPHASEAST, ATM90E32_STATUS_S0_OVPHASEBST,
|
||||
ATM90E32_STATUS_S0_OVPHASECST};
|
||||
const uint16_t voltage_sag_flags[3] = {ATM90E32_STATUS_S1_SAGPHASEAST, ATM90E32_STATUS_S1_SAGPHASEBST,
|
||||
ATM90E32_STATUS_S1_SAGPHASECST};
|
||||
const uint16_t phase_loss_flags[3] = {ATM90E32_STATUS_S1_PHASELOSSAST, ATM90E32_STATUS_S1_PHASELOSSBST,
|
||||
ATM90E32_STATUS_S1_PHASELOSSCST};
|
||||
void loop() override;
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -42,6 +63,14 @@ class ATM90E32Component : public PollingComponent,
|
||||
void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; }
|
||||
void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; }
|
||||
void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; }
|
||||
void set_voltage_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].voltage_offset_ = offset; }
|
||||
void set_current_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].current_offset_ = offset; }
|
||||
void set_active_power_offset(uint8_t phase, int16_t offset) {
|
||||
this->power_offset_phase_[phase].active_power_offset = offset;
|
||||
}
|
||||
void set_reactive_power_offset(uint8_t phase, int16_t offset) {
|
||||
this->power_offset_phase_[phase].reactive_power_offset = offset;
|
||||
}
|
||||
void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
|
||||
void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; }
|
||||
void set_chip_temperature_sensor(sensor::Sensor *chip_temperature_sensor) {
|
||||
@@ -51,53 +80,104 @@ class ATM90E32Component : public PollingComponent,
|
||||
void set_current_phases(int phases) { current_phases_ = phases; }
|
||||
void set_pga_gain(uint16_t gain) { pga_gain_ = gain; }
|
||||
void run_offset_calibrations();
|
||||
void run_power_offset_calibrations();
|
||||
void clear_offset_calibrations();
|
||||
void clear_power_offset_calibrations();
|
||||
void clear_gain_calibrations();
|
||||
void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; }
|
||||
uint16_t calibrate_voltage_offset_phase(uint8_t /*phase*/);
|
||||
uint16_t calibrate_current_offset_phase(uint8_t /*phase*/);
|
||||
void set_enable_gain_calibration(bool flag) { enable_gain_calibration_ = flag; }
|
||||
int16_t calibrate_offset(uint8_t phase, bool voltage);
|
||||
int16_t calibrate_power_offset(uint8_t phase, bool reactive);
|
||||
void run_gain_calibrations();
|
||||
#ifdef USE_NUMBER
|
||||
void set_reference_voltage(uint8_t phase, number::Number *ref_voltage) { ref_voltages_[phase] = ref_voltage; }
|
||||
void set_reference_current(uint8_t phase, number::Number *ref_current) { ref_currents_[phase] = ref_current; }
|
||||
#endif
|
||||
float get_reference_voltage(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
|
||||
#else
|
||||
return 120.0; // Default voltage
|
||||
#endif
|
||||
}
|
||||
float get_reference_current(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
|
||||
#else
|
||||
return 5.0f; // Default current
|
||||
#endif
|
||||
}
|
||||
bool using_saved_calibrations_ = false; // Track if stored calibrations are being used
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void check_phase_status();
|
||||
void check_freq_status();
|
||||
void check_over_current();
|
||||
void set_phase_status_text_sensor(uint8_t phase, text_sensor::TextSensor *sensor) {
|
||||
this->phase_status_text_sensor_[phase] = sensor;
|
||||
}
|
||||
void set_freq_status_text_sensor(text_sensor::TextSensor *sensor) { this->freq_status_text_sensor_ = sensor; }
|
||||
#endif
|
||||
uint16_t calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier);
|
||||
int32_t last_periodic_millis = millis();
|
||||
|
||||
protected:
|
||||
#ifdef USE_NUMBER
|
||||
number::Number *ref_voltages_[3]{nullptr, nullptr, nullptr};
|
||||
number::Number *ref_currents_[3]{nullptr, nullptr, nullptr};
|
||||
#endif
|
||||
uint16_t read16_(uint16_t a_register);
|
||||
int read32_(uint16_t addr_h, uint16_t addr_l);
|
||||
void write16_(uint16_t a_register, uint16_t val);
|
||||
float get_local_phase_voltage_(uint8_t /*phase*/);
|
||||
float get_local_phase_current_(uint8_t /*phase*/);
|
||||
float get_local_phase_active_power_(uint8_t /*phase*/);
|
||||
float get_local_phase_reactive_power_(uint8_t /*phase*/);
|
||||
float get_local_phase_power_factor_(uint8_t /*phase*/);
|
||||
float get_local_phase_forward_active_energy_(uint8_t /*phase*/);
|
||||
float get_local_phase_reverse_active_energy_(uint8_t /*phase*/);
|
||||
float get_local_phase_angle_(uint8_t /*phase*/);
|
||||
float get_local_phase_harmonic_active_power_(uint8_t /*phase*/);
|
||||
float get_local_phase_peak_current_(uint8_t /*phase*/);
|
||||
float get_phase_voltage_(uint8_t /*phase*/);
|
||||
float get_phase_voltage_avg_(uint8_t /*phase*/);
|
||||
float get_phase_current_(uint8_t /*phase*/);
|
||||
float get_phase_current_avg_(uint8_t /*phase*/);
|
||||
float get_phase_active_power_(uint8_t /*phase*/);
|
||||
float get_phase_reactive_power_(uint8_t /*phase*/);
|
||||
float get_phase_power_factor_(uint8_t /*phase*/);
|
||||
float get_phase_forward_active_energy_(uint8_t /*phase*/);
|
||||
float get_phase_reverse_active_energy_(uint8_t /*phase*/);
|
||||
float get_phase_angle_(uint8_t /*phase*/);
|
||||
float get_phase_harmonic_active_power_(uint8_t /*phase*/);
|
||||
float get_phase_peak_current_(uint8_t /*phase*/);
|
||||
float get_local_phase_voltage_(uint8_t phase);
|
||||
float get_local_phase_current_(uint8_t phase);
|
||||
float get_local_phase_active_power_(uint8_t phase);
|
||||
float get_local_phase_reactive_power_(uint8_t phase);
|
||||
float get_local_phase_apparent_power_(uint8_t phase);
|
||||
float get_local_phase_power_factor_(uint8_t phase);
|
||||
float get_local_phase_forward_active_energy_(uint8_t phase);
|
||||
float get_local_phase_reverse_active_energy_(uint8_t phase);
|
||||
float get_local_phase_angle_(uint8_t phase);
|
||||
float get_local_phase_harmonic_active_power_(uint8_t phase);
|
||||
float get_local_phase_peak_current_(uint8_t phase);
|
||||
float get_phase_voltage_(uint8_t phase);
|
||||
float get_phase_voltage_avg_(uint8_t phase);
|
||||
float get_phase_current_(uint8_t phase);
|
||||
float get_phase_current_avg_(uint8_t phase);
|
||||
float get_phase_active_power_(uint8_t phase);
|
||||
float get_phase_reactive_power_(uint8_t phase);
|
||||
float get_phase_apparent_power_(uint8_t phase);
|
||||
float get_phase_power_factor_(uint8_t phase);
|
||||
float get_phase_forward_active_energy_(uint8_t phase);
|
||||
float get_phase_reverse_active_energy_(uint8_t phase);
|
||||
float get_phase_angle_(uint8_t phase);
|
||||
float get_phase_harmonic_active_power_(uint8_t phase);
|
||||
float get_phase_peak_current_(uint8_t phase);
|
||||
float get_frequency_();
|
||||
float get_chip_temperature_();
|
||||
bool get_publish_interval_flag_() { return publish_interval_flag_; };
|
||||
void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; };
|
||||
void restore_calibrations_();
|
||||
void restore_offset_calibrations_();
|
||||
void restore_power_offset_calibrations_();
|
||||
void restore_gain_calibrations_();
|
||||
void save_gain_calibration_to_memory_();
|
||||
void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset);
|
||||
void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset);
|
||||
void write_gains_to_registers_();
|
||||
bool verify_gain_writes_();
|
||||
bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
|
||||
|
||||
struct ATM90E32Phase {
|
||||
uint16_t voltage_gain_{0};
|
||||
uint16_t ct_gain_{0};
|
||||
uint16_t voltage_offset_{0};
|
||||
uint16_t current_offset_{0};
|
||||
int16_t voltage_offset_{0};
|
||||
int16_t current_offset_{0};
|
||||
int16_t active_power_offset_{0};
|
||||
int16_t reactive_power_offset_{0};
|
||||
float voltage_{0};
|
||||
float current_{0};
|
||||
float active_power_{0};
|
||||
float reactive_power_{0};
|
||||
float apparent_power_{0};
|
||||
float power_factor_{0};
|
||||
float forward_active_energy_{0};
|
||||
float reverse_active_energy_{0};
|
||||
@@ -119,14 +199,30 @@ class ATM90E32Component : public PollingComponent,
|
||||
uint32_t cumulative_reverse_active_energy_{0};
|
||||
} phase_[3];
|
||||
|
||||
struct Calibration {
|
||||
uint16_t voltage_offset_{0};
|
||||
uint16_t current_offset_{0};
|
||||
struct OffsetCalibration {
|
||||
int16_t voltage_offset_{0};
|
||||
int16_t current_offset_{0};
|
||||
} offset_phase_[3];
|
||||
|
||||
ESPPreferenceObject pref_;
|
||||
struct PowerOffsetCalibration {
|
||||
int16_t active_power_offset{0};
|
||||
int16_t reactive_power_offset{0};
|
||||
} power_offset_phase_[3];
|
||||
|
||||
struct GainCalibration {
|
||||
uint16_t voltage_gain{1};
|
||||
uint16_t current_gain{1};
|
||||
} gain_phase_[3];
|
||||
|
||||
ESPPreferenceObject offset_pref_;
|
||||
ESPPreferenceObject power_offset_pref_;
|
||||
ESPPreferenceObject gain_calibration_pref_;
|
||||
|
||||
sensor::Sensor *freq_sensor_{nullptr};
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
text_sensor::TextSensor *phase_status_text_sensor_[3]{nullptr};
|
||||
text_sensor::TextSensor *freq_status_text_sensor_{nullptr};
|
||||
#endif
|
||||
sensor::Sensor *chip_temperature_sensor_{nullptr};
|
||||
uint16_t pga_gain_{0x15};
|
||||
int line_freq_{60};
|
||||
@@ -134,6 +230,7 @@ class ATM90E32Component : public PollingComponent,
|
||||
bool publish_interval_flag_{false};
|
||||
bool peak_current_signed_{false};
|
||||
bool enable_offset_calibration_{false};
|
||||
bool enable_gain_calibration_{false};
|
||||
};
|
||||
|
||||
} // namespace atm90e32
|
||||
|
||||
@@ -176,16 +176,17 @@ static const uint16_t ATM90E32_REGISTER_ANENERGYCH = 0xAF; // C Reverse Harm. E
|
||||
|
||||
/* POWER & P.F. REGISTERS */
|
||||
static const uint16_t ATM90E32_REGISTER_PMEANT = 0xB0; // Total Mean Power (P)
|
||||
static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1; // Mean Power Reg Base (P)
|
||||
static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1; // Active Power Reg Base (P)
|
||||
static const uint16_t ATM90E32_REGISTER_PMEANA = 0xB1; // A Mean Power (P)
|
||||
static const uint16_t ATM90E32_REGISTER_PMEANB = 0xB2; // B Mean Power (P)
|
||||
static const uint16_t ATM90E32_REGISTER_PMEANC = 0xB3; // C Mean Power (P)
|
||||
static const uint16_t ATM90E32_REGISTER_QMEANT = 0xB4; // Total Mean Power (Q)
|
||||
static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5; // Mean Power Reg Base (Q)
|
||||
static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5; // Reactive Power Reg Base (Q)
|
||||
static const uint16_t ATM90E32_REGISTER_QMEANA = 0xB5; // A Mean Power (Q)
|
||||
static const uint16_t ATM90E32_REGISTER_QMEANB = 0xB6; // B Mean Power (Q)
|
||||
static const uint16_t ATM90E32_REGISTER_QMEANC = 0xB7; // C Mean Power (Q)
|
||||
static const uint16_t ATM90E32_REGISTER_SMEANT = 0xB8; // Total Mean Power (S)
|
||||
static const uint16_t ATM90E32_REGISTER_SMEAN = 0xB9; // Apparent Mean Power Base (S)
|
||||
static const uint16_t ATM90E32_REGISTER_SMEANA = 0xB9; // A Mean Power (S)
|
||||
static const uint16_t ATM90E32_REGISTER_SMEANB = 0xBA; // B Mean Power (S)
|
||||
static const uint16_t ATM90E32_REGISTER_SMEANC = 0xBB; // C Mean Power (S)
|
||||
@@ -206,6 +207,7 @@ static const uint16_t ATM90E32_REGISTER_QMEANALSB = 0xC5; // Lower Word (A Rea
|
||||
static const uint16_t ATM90E32_REGISTER_QMEANBLSB = 0xC6; // Lower Word (B React. Power)
|
||||
static const uint16_t ATM90E32_REGISTER_QMEANCLSB = 0xC7; // Lower Word (C React. Power)
|
||||
static const uint16_t ATM90E32_REGISTER_SAMEANTLSB = 0xC8; // Lower Word (Tot. App. Power)
|
||||
static const uint16_t ATM90E32_REGISTER_SMEANLSB = 0xC9; // Lower Word Reg Base (Apparent Power)
|
||||
static const uint16_t ATM90E32_REGISTER_SMEANALSB = 0xC9; // Lower Word (A App. Power)
|
||||
static const uint16_t ATM90E32_REGISTER_SMEANBLSB = 0xCA; // Lower Word (B App. Power)
|
||||
static const uint16_t ATM90E32_REGISTER_SMEANCLSB = 0xCB; // Lower Word (C App. Power)
|
||||
|
||||
@@ -1,43 +1,95 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_CHIP, ICON_SCALE
|
||||
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE
|
||||
|
||||
from .. import atm90e32_ns
|
||||
from ..sensor import ATM90E32Component
|
||||
|
||||
CONF_RUN_GAIN_CALIBRATION = "run_gain_calibration"
|
||||
CONF_CLEAR_GAIN_CALIBRATION = "clear_gain_calibration"
|
||||
CONF_RUN_OFFSET_CALIBRATION = "run_offset_calibration"
|
||||
CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration"
|
||||
CONF_RUN_POWER_OFFSET_CALIBRATION = "run_power_offset_calibration"
|
||||
CONF_CLEAR_POWER_OFFSET_CALIBRATION = "clear_power_offset_calibration"
|
||||
|
||||
ATM90E32CalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32CalibrationButton",
|
||||
button.Button,
|
||||
ATM90E32GainCalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32GainCalibrationButton", button.Button
|
||||
)
|
||||
ATM90E32ClearCalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32ClearCalibrationButton",
|
||||
button.Button,
|
||||
ATM90E32ClearGainCalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32ClearGainCalibrationButton", button.Button
|
||||
)
|
||||
ATM90E32OffsetCalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32OffsetCalibrationButton", button.Button
|
||||
)
|
||||
ATM90E32ClearOffsetCalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32ClearOffsetCalibrationButton", button.Button
|
||||
)
|
||||
ATM90E32PowerOffsetCalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32PowerOffsetCalibrationButton", button.Button
|
||||
)
|
||||
ATM90E32ClearPowerOffsetCalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32ClearPowerOffsetCalibrationButton", button.Button
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component),
|
||||
cv.Optional(CONF_RUN_GAIN_CALIBRATION): button.button_schema(
|
||||
ATM90E32GainCalibrationButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon="mdi:scale-balance",
|
||||
),
|
||||
cv.Optional(CONF_CLEAR_GAIN_CALIBRATION): button.button_schema(
|
||||
ATM90E32ClearGainCalibrationButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon="mdi:delete",
|
||||
),
|
||||
cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema(
|
||||
ATM90E32CalibrationButton,
|
||||
ATM90E32OffsetCalibrationButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_SCALE,
|
||||
),
|
||||
cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema(
|
||||
ATM90E32ClearCalibrationButton,
|
||||
ATM90E32ClearOffsetCalibrationButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_CHIP,
|
||||
icon="mdi:delete",
|
||||
),
|
||||
cv.Optional(CONF_RUN_POWER_OFFSET_CALIBRATION): button.button_schema(
|
||||
ATM90E32PowerOffsetCalibrationButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_SCALE,
|
||||
),
|
||||
cv.Optional(CONF_CLEAR_POWER_OFFSET_CALIBRATION): button.button_schema(
|
||||
ATM90E32ClearPowerOffsetCalibrationButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon="mdi:delete",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
if run_gain := config.get(CONF_RUN_GAIN_CALIBRATION):
|
||||
b = await button.new_button(run_gain)
|
||||
await cg.register_parented(b, parent)
|
||||
|
||||
if clear_gain := config.get(CONF_CLEAR_GAIN_CALIBRATION):
|
||||
b = await button.new_button(clear_gain)
|
||||
await cg.register_parented(b, parent)
|
||||
|
||||
if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION):
|
||||
b = await button.new_button(run_offset)
|
||||
await cg.register_parented(b, parent)
|
||||
|
||||
if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION):
|
||||
b = await button.new_button(clear_offset)
|
||||
await cg.register_parented(b, parent)
|
||||
|
||||
if run_power := config.get(CONF_RUN_POWER_OFFSET_CALIBRATION):
|
||||
b = await button.new_button(run_power)
|
||||
await cg.register_parented(b, parent)
|
||||
|
||||
if clear_power := config.get(CONF_CLEAR_POWER_OFFSET_CALIBRATION):
|
||||
b = await button.new_button(clear_power)
|
||||
await cg.register_parented(b, parent)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "atm90e32_button.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -6,15 +7,73 @@ namespace atm90e32 {
|
||||
|
||||
static const char *const TAG = "atm90e32.button";
|
||||
|
||||
void ATM90E32CalibrationButton::press_action() {
|
||||
ESP_LOGI(TAG, "Running offset calibrations, Note: CTs and ACVs must be 0 during this process...");
|
||||
void ATM90E32GainCalibrationButton::press_action() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Gain Calibration button [%s]", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "%s", this->get_name().c_str());
|
||||
ESP_LOGI(TAG,
|
||||
"[CALIBRATION] Use gain_ct: & gain_voltage: under each phase_x: in your config file to save these values");
|
||||
this->parent_->run_gain_calibrations();
|
||||
}
|
||||
|
||||
void ATM90E32ClearGainCalibrationButton::press_action() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Gain button [%s]", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "%s", this->get_name().c_str());
|
||||
this->parent_->clear_gain_calibrations();
|
||||
}
|
||||
|
||||
void ATM90E32OffsetCalibrationButton::press_action() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Offset Calibration button [%s]", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "%s", this->get_name().c_str());
|
||||
ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs and ACVs must be 0 during this process. USB power only**");
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Use offset_voltage: & offset_current: under each phase_x: in your config file to save "
|
||||
"these values");
|
||||
this->parent_->run_offset_calibrations();
|
||||
}
|
||||
|
||||
void ATM90E32ClearCalibrationButton::press_action() {
|
||||
ESP_LOGI(TAG, "Offset calibrations cleared.");
|
||||
void ATM90E32ClearOffsetCalibrationButton::press_action() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Offset button [%s]", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "%s", this->get_name().c_str());
|
||||
this->parent_->clear_offset_calibrations();
|
||||
}
|
||||
|
||||
void ATM90E32PowerOffsetCalibrationButton::press_action() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Power Calibration button [%s]", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "%s", this->get_name().c_str());
|
||||
ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs must be 0 during this process. Voltage reference should be present**");
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Use offset_active_power: & offset_reactive_power: under each phase_x: in your config "
|
||||
"file to save these values");
|
||||
this->parent_->run_power_offset_calibrations();
|
||||
}
|
||||
|
||||
void ATM90E32ClearPowerOffsetCalibrationButton::press_action() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Power button [%s]", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "%s", this->get_name().c_str());
|
||||
this->parent_->clear_power_offset_calibrations();
|
||||
}
|
||||
|
||||
} // namespace atm90e32
|
||||
} // namespace esphome
|
||||
|
||||
@@ -7,17 +7,49 @@
|
||||
namespace esphome {
|
||||
namespace atm90e32 {
|
||||
|
||||
class ATM90E32CalibrationButton : public button::Button, public Parented<ATM90E32Component> {
|
||||
class ATM90E32GainCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
|
||||
public:
|
||||
ATM90E32CalibrationButton() = default;
|
||||
ATM90E32GainCalibrationButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
class ATM90E32ClearCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
|
||||
class ATM90E32ClearGainCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
|
||||
public:
|
||||
ATM90E32ClearCalibrationButton() = default;
|
||||
ATM90E32ClearGainCalibrationButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
class ATM90E32OffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
|
||||
public:
|
||||
ATM90E32OffsetCalibrationButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
class ATM90E32ClearOffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
|
||||
public:
|
||||
ATM90E32ClearOffsetCalibrationButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
class ATM90E32PowerOffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
|
||||
public:
|
||||
ATM90E32PowerOffsetCalibrationButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
class ATM90E32ClearPowerOffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
|
||||
public:
|
||||
ATM90E32ClearPowerOffsetCalibrationButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
|
||||
130
esphome/components/atm90e32/number/__init__.py
Normal file
130
esphome/components/atm90e32/number/__init__.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MAX_VALUE,
|
||||
CONF_MIN_VALUE,
|
||||
CONF_MODE,
|
||||
CONF_PHASE_A,
|
||||
CONF_PHASE_B,
|
||||
CONF_PHASE_C,
|
||||
CONF_REFERENCE_VOLTAGE,
|
||||
CONF_STEP,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
UNIT_AMPERE,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
|
||||
from .. import atm90e32_ns
|
||||
from ..sensor import ATM90E32Component
|
||||
|
||||
ATM90E32Number = atm90e32_ns.class_(
|
||||
"ATM90E32Number", number.Number, cg.Parented.template(ATM90E32Component)
|
||||
)
|
||||
|
||||
CONF_REFERENCE_CURRENT = "reference_current"
|
||||
PHASE_KEYS = [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]
|
||||
|
||||
|
||||
REFERENCE_VOLTAGE_PHASE_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MODE, default="box"): cv.string,
|
||||
cv.Optional(CONF_MIN_VALUE, default=100.0): cv.float_,
|
||||
cv.Optional(CONF_MAX_VALUE, default=260.0): cv.float_,
|
||||
cv.Optional(CONF_STEP, default=0.1): cv.float_,
|
||||
}
|
||||
).extend(
|
||||
number.number_schema(
|
||||
class_=ATM90E32Number,
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon="mdi:power-plug",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
REFERENCE_CURRENT_PHASE_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MODE, default="box"): cv.string,
|
||||
cv.Optional(CONF_MIN_VALUE, default=1.0): cv.float_,
|
||||
cv.Optional(CONF_MAX_VALUE, default=200.0): cv.float_,
|
||||
cv.Optional(CONF_STEP, default=0.1): cv.float_,
|
||||
}
|
||||
).extend(
|
||||
number.number_schema(
|
||||
class_=ATM90E32Number,
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon="mdi:home-lightning-bolt",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
REFERENCE_VOLTAGE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_PHASE_A): REFERENCE_VOLTAGE_PHASE_SCHEMA,
|
||||
cv.Optional(CONF_PHASE_B): REFERENCE_VOLTAGE_PHASE_SCHEMA,
|
||||
cv.Optional(CONF_PHASE_C): REFERENCE_VOLTAGE_PHASE_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
REFERENCE_CURRENT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_PHASE_A): REFERENCE_CURRENT_PHASE_SCHEMA,
|
||||
cv.Optional(CONF_PHASE_B): REFERENCE_CURRENT_PHASE_SCHEMA,
|
||||
cv.Optional(CONF_PHASE_C): REFERENCE_CURRENT_PHASE_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component),
|
||||
cv.Optional(CONF_REFERENCE_VOLTAGE): REFERENCE_VOLTAGE_SCHEMA,
|
||||
cv.Optional(CONF_REFERENCE_CURRENT): REFERENCE_CURRENT_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
if voltage_cfg := config.get(CONF_REFERENCE_VOLTAGE):
|
||||
voltage_objs = [None, None, None]
|
||||
|
||||
for i, key in enumerate(PHASE_KEYS):
|
||||
if validated := voltage_cfg.get(key):
|
||||
obj = await number.new_number(
|
||||
validated,
|
||||
min_value=validated["min_value"],
|
||||
max_value=validated["max_value"],
|
||||
step=validated["step"],
|
||||
)
|
||||
await cg.register_parented(obj, parent)
|
||||
voltage_objs[i] = obj
|
||||
|
||||
# Inherit from A → B/C if only A defined
|
||||
if voltage_objs[0] is not None:
|
||||
for i in range(3):
|
||||
if voltage_objs[i] is None:
|
||||
voltage_objs[i] = voltage_objs[0]
|
||||
|
||||
for i, obj in enumerate(voltage_objs):
|
||||
if obj is not None:
|
||||
cg.add(parent.set_reference_voltage(i, obj))
|
||||
|
||||
if current_cfg := config.get(CONF_REFERENCE_CURRENT):
|
||||
for i, key in enumerate(PHASE_KEYS):
|
||||
if validated := current_cfg.get(key):
|
||||
obj = await number.new_number(
|
||||
validated,
|
||||
min_value=validated["min_value"],
|
||||
max_value=validated["max_value"],
|
||||
step=validated["step"],
|
||||
)
|
||||
await cg.register_parented(obj, parent)
|
||||
cg.add(parent.set_reference_current(i, obj))
|
||||
16
esphome/components/atm90e32/number/atm90e32_number.h
Normal file
16
esphome/components/atm90e32/number/atm90e32_number.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/atm90e32/atm90e32.h"
|
||||
#include "esphome/components/number/number.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace atm90e32 {
|
||||
|
||||
class ATM90E32Number : public number::Number, public Parented<ATM90E32Component> {
|
||||
public:
|
||||
void control(float value) override { this->publish_state(value); }
|
||||
};
|
||||
|
||||
} // namespace atm90e32
|
||||
} // namespace esphome
|
||||
@@ -33,6 +33,7 @@ from esphome.const import (
|
||||
UNIT_DEGREES,
|
||||
UNIT_HERTZ,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS,
|
||||
UNIT_VOLT_AMPS_REACTIVE,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
@@ -45,10 +46,17 @@ CONF_GAIN_PGA = "gain_pga"
|
||||
CONF_CURRENT_PHASES = "current_phases"
|
||||
CONF_GAIN_VOLTAGE = "gain_voltage"
|
||||
CONF_GAIN_CT = "gain_ct"
|
||||
CONF_OFFSET_VOLTAGE = "offset_voltage"
|
||||
CONF_OFFSET_CURRENT = "offset_current"
|
||||
CONF_OFFSET_ACTIVE_POWER = "offset_active_power"
|
||||
CONF_OFFSET_REACTIVE_POWER = "offset_reactive_power"
|
||||
CONF_HARMONIC_POWER = "harmonic_power"
|
||||
CONF_PEAK_CURRENT = "peak_current"
|
||||
CONF_PEAK_CURRENT_SIGNED = "peak_current_signed"
|
||||
CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration"
|
||||
CONF_ENABLE_GAIN_CALIBRATION = "enable_gain_calibration"
|
||||
CONF_PHASE_STATUS = "phase_status"
|
||||
CONF_FREQUENCY_STATUS = "frequency_status"
|
||||
UNIT_DEG = "degrees"
|
||||
LINE_FREQS = {
|
||||
"50HZ": 50,
|
||||
@@ -92,10 +100,11 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
|
||||
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
|
||||
icon=ICON_LIGHTBULB,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
unit_of_measurement=UNIT_VOLT_AMPS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
@@ -137,6 +146,10 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
|
||||
),
|
||||
cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t,
|
||||
cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t,
|
||||
cv.Optional(CONF_OFFSET_VOLTAGE, default=0): cv.int_,
|
||||
cv.Optional(CONF_OFFSET_CURRENT, default=0): cv.int_,
|
||||
cv.Optional(CONF_OFFSET_ACTIVE_POWER, default=0): cv.int_,
|
||||
cv.Optional(CONF_OFFSET_REACTIVE_POWER, default=0): cv.int_,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -164,9 +177,10 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum(
|
||||
CURRENT_PHASES, upper=True
|
||||
),
|
||||
cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True),
|
||||
cv.Optional(CONF_GAIN_PGA, default="1X"): cv.enum(PGA_GAINS, upper=True),
|
||||
cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ENABLE_GAIN_CALIBRATION, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
@@ -185,6 +199,10 @@ async def to_code(config):
|
||||
conf = config[phase]
|
||||
cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE]))
|
||||
cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT]))
|
||||
cg.add(var.set_voltage_offset(i, conf[CONF_OFFSET_VOLTAGE]))
|
||||
cg.add(var.set_current_offset(i, conf[CONF_OFFSET_CURRENT]))
|
||||
cg.add(var.set_active_power_offset(i, conf[CONF_OFFSET_ACTIVE_POWER]))
|
||||
cg.add(var.set_reactive_power_offset(i, conf[CONF_OFFSET_REACTIVE_POWER]))
|
||||
if voltage_config := conf.get(CONF_VOLTAGE):
|
||||
sens = await sensor.new_sensor(voltage_config)
|
||||
cg.add(var.set_voltage_sensor(i, sens))
|
||||
@@ -218,16 +236,15 @@ async def to_code(config):
|
||||
if peak_current_config := conf.get(CONF_PEAK_CURRENT):
|
||||
sens = await sensor.new_sensor(peak_current_config)
|
||||
cg.add(var.set_peak_current_sensor(i, sens))
|
||||
|
||||
if frequency_config := config.get(CONF_FREQUENCY):
|
||||
sens = await sensor.new_sensor(frequency_config)
|
||||
cg.add(var.set_freq_sensor(sens))
|
||||
if chip_temperature_config := config.get(CONF_CHIP_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(chip_temperature_config)
|
||||
cg.add(var.set_chip_temperature_sensor(sens))
|
||||
|
||||
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
|
||||
cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES]))
|
||||
cg.add(var.set_pga_gain(config[CONF_GAIN_PGA]))
|
||||
cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED]))
|
||||
cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION]))
|
||||
cg.add(var.set_enable_gain_calibration(config[CONF_ENABLE_GAIN_CALIBRATION]))
|
||||
|
||||
48
esphome/components/atm90e32/text_sensor/__init__.py
Normal file
48
esphome/components/atm90e32/text_sensor/__init__.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C
|
||||
|
||||
from ..sensor import ATM90E32Component
|
||||
|
||||
CONF_PHASE_STATUS = "phase_status"
|
||||
CONF_FREQUENCY_STATUS = "frequency_status"
|
||||
PHASE_KEYS = [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]
|
||||
|
||||
PHASE_STATUS_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_PHASE_A): text_sensor.text_sensor_schema(
|
||||
icon="mdi:flash-alert"
|
||||
),
|
||||
cv.Optional(CONF_PHASE_B): text_sensor.text_sensor_schema(
|
||||
icon="mdi:flash-alert"
|
||||
),
|
||||
cv.Optional(CONF_PHASE_C): text_sensor.text_sensor_schema(
|
||||
icon="mdi:flash-alert"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(ATM90E32Component),
|
||||
cv.Optional(CONF_PHASE_STATUS): PHASE_STATUS_SCHEMA,
|
||||
cv.Optional(CONF_FREQUENCY_STATUS): text_sensor.text_sensor_schema(
|
||||
icon="mdi:lightbulb-alert"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
if phase_cfg := config.get(CONF_PHASE_STATUS):
|
||||
for i, key in enumerate(PHASE_KEYS):
|
||||
if sub_phase_cfg := phase_cfg.get(key):
|
||||
sens = await text_sensor.new_text_sensor(sub_phase_cfg)
|
||||
cg.add(parent.set_phase_status_text_sensor(i, sens))
|
||||
|
||||
if freq_status_config := config.get(CONF_FREQUENCY_STATUS):
|
||||
sens = await text_sensor.new_text_sensor(freq_status_config)
|
||||
cg.add(parent.set_freq_status_text_sensor(sens))
|
||||
@@ -37,16 +37,13 @@ AUDIO_COMPONENT_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
_UNDEF = object()
|
||||
|
||||
|
||||
def set_stream_limits(
|
||||
min_bits_per_sample: int = _UNDEF,
|
||||
max_bits_per_sample: int = _UNDEF,
|
||||
min_channels: int = _UNDEF,
|
||||
max_channels: int = _UNDEF,
|
||||
min_sample_rate: int = _UNDEF,
|
||||
max_sample_rate: int = _UNDEF,
|
||||
min_bits_per_sample: int = cv.UNDEFINED,
|
||||
max_bits_per_sample: int = cv.UNDEFINED,
|
||||
min_channels: int = cv.UNDEFINED,
|
||||
max_channels: int = cv.UNDEFINED,
|
||||
min_sample_rate: int = cv.UNDEFINED,
|
||||
max_sample_rate: int = cv.UNDEFINED,
|
||||
):
|
||||
"""Sets the limits for the audio stream that audio component can handle
|
||||
|
||||
@@ -55,17 +52,17 @@ def set_stream_limits(
|
||||
"""
|
||||
|
||||
def set_limits_in_config(config):
|
||||
if min_bits_per_sample is not _UNDEF:
|
||||
if min_bits_per_sample is not cv.UNDEFINED:
|
||||
config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample
|
||||
if max_bits_per_sample is not _UNDEF:
|
||||
if max_bits_per_sample is not cv.UNDEFINED:
|
||||
config[CONF_MAX_BITS_PER_SAMPLE] = max_bits_per_sample
|
||||
if min_channels is not _UNDEF:
|
||||
if min_channels is not cv.UNDEFINED:
|
||||
config[CONF_MIN_CHANNELS] = min_channels
|
||||
if max_channels is not _UNDEF:
|
||||
if max_channels is not cv.UNDEFINED:
|
||||
config[CONF_MAX_CHANNELS] = max_channels
|
||||
if min_sample_rate is not _UNDEF:
|
||||
if min_sample_rate is not cv.UNDEFINED:
|
||||
config[CONF_MIN_SAMPLE_RATE] = min_sample_rate
|
||||
if max_sample_rate is not _UNDEF:
|
||||
if max_sample_rate is not cv.UNDEFINED:
|
||||
config[CONF_MAX_SAMPLE_RATE] = max_sample_rate
|
||||
|
||||
return set_limits_in_config
|
||||
@@ -75,10 +72,10 @@ def final_validate_audio_schema(
|
||||
name: str,
|
||||
*,
|
||||
audio_device: str,
|
||||
bits_per_sample: int = _UNDEF,
|
||||
channels: int = _UNDEF,
|
||||
sample_rate: int = _UNDEF,
|
||||
enabled_channels: list[int] = _UNDEF,
|
||||
bits_per_sample: int = cv.UNDEFINED,
|
||||
channels: int = cv.UNDEFINED,
|
||||
sample_rate: int = cv.UNDEFINED,
|
||||
enabled_channels: list[int] = cv.UNDEFINED,
|
||||
audio_device_issue: bool = False,
|
||||
):
|
||||
"""Validates audio compatibility when passed between different components.
|
||||
@@ -101,7 +98,7 @@ def final_validate_audio_schema(
|
||||
def validate_audio_compatiblity(audio_config):
|
||||
audio_schema = {}
|
||||
|
||||
if bits_per_sample is not _UNDEF:
|
||||
if bits_per_sample is not cv.UNDEFINED:
|
||||
try:
|
||||
cv.int_range(
|
||||
min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE),
|
||||
@@ -114,7 +111,7 @@ def final_validate_audio_schema(
|
||||
error_string = f"Invalid configuration for the {name} component. The {CONF_BITS_PER_SAMPLE} {str(exc)}"
|
||||
raise cv.Invalid(error_string) from exc
|
||||
|
||||
if channels is not _UNDEF:
|
||||
if channels is not cv.UNDEFINED:
|
||||
try:
|
||||
cv.int_range(
|
||||
min=audio_config.get(CONF_MIN_CHANNELS),
|
||||
@@ -127,7 +124,7 @@ def final_validate_audio_schema(
|
||||
error_string = f"Invalid configuration for the {name} component. The {CONF_NUM_CHANNELS} {str(exc)}"
|
||||
raise cv.Invalid(error_string) from exc
|
||||
|
||||
if sample_rate is not _UNDEF:
|
||||
if sample_rate is not cv.UNDEFINED:
|
||||
try:
|
||||
cv.int_range(
|
||||
min=audio_config.get(CONF_MIN_SAMPLE_RATE),
|
||||
@@ -140,7 +137,7 @@ def final_validate_audio_schema(
|
||||
error_string = f"Invalid configuration for the {name} component. The {CONF_SAMPLE_RATE} {str(exc)}"
|
||||
raise cv.Invalid(error_string) from exc
|
||||
|
||||
if enabled_channels is not _UNDEF:
|
||||
if enabled_channels is not cv.UNDEFINED:
|
||||
for channel in enabled_channels:
|
||||
try:
|
||||
# Channels are 0-indexed
|
||||
@@ -168,4 +165,4 @@ def final_validate_audio_schema(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
cg.add_library("esphome/esp-audio-libs", "1.1.3")
|
||||
cg.add_library("esphome/esp-audio-libs", "1.1.4")
|
||||
|
||||
@@ -135,5 +135,53 @@ const char *audio_file_type_to_string(AudioFileType file_type);
|
||||
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
|
||||
size_t samples_to_scale);
|
||||
|
||||
/// @brief Unpacks a quantized audio sample into a Q31 fixed-point number.
|
||||
/// @param data Pointer to uint8_t array containing the audio sample
|
||||
/// @param bytes_per_sample The number of bytes per sample
|
||||
/// @return Q31 sample
|
||||
inline int32_t unpack_audio_sample_to_q31(const uint8_t *data, size_t bytes_per_sample) {
|
||||
int32_t sample = 0;
|
||||
if (bytes_per_sample == 1) {
|
||||
sample |= data[0] << 24;
|
||||
} else if (bytes_per_sample == 2) {
|
||||
sample |= data[0] << 16;
|
||||
sample |= data[1] << 24;
|
||||
} else if (bytes_per_sample == 3) {
|
||||
sample |= data[0] << 8;
|
||||
sample |= data[1] << 16;
|
||||
sample |= data[2] << 24;
|
||||
} else if (bytes_per_sample == 4) {
|
||||
sample |= data[0];
|
||||
sample |= data[1] << 8;
|
||||
sample |= data[2] << 16;
|
||||
sample |= data[3] << 24;
|
||||
}
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
/// @brief Packs a Q31 fixed-point number as an audio sample with the specified number of bytes per sample.
|
||||
/// Packs the most significant bits - no dithering is applied.
|
||||
/// @param sample Q31 fixed-point number to pack
|
||||
/// @param data Pointer to data array to store
|
||||
/// @param bytes_per_sample The audio data's bytes per sample
|
||||
inline void pack_q31_as_audio_sample(int32_t sample, uint8_t *data, size_t bytes_per_sample) {
|
||||
if (bytes_per_sample == 1) {
|
||||
data[0] = static_cast<uint8_t>(sample >> 24);
|
||||
} else if (bytes_per_sample == 2) {
|
||||
data[0] = static_cast<uint8_t>(sample >> 16);
|
||||
data[1] = static_cast<uint8_t>(sample >> 24);
|
||||
} else if (bytes_per_sample == 3) {
|
||||
data[0] = static_cast<uint8_t>(sample >> 8);
|
||||
data[1] = static_cast<uint8_t>(sample >> 16);
|
||||
data[2] = static_cast<uint8_t>(sample >> 24);
|
||||
} else if (bytes_per_sample == 4) {
|
||||
data[0] = static_cast<uint8_t>(sample);
|
||||
data[1] = static_cast<uint8_t>(sample >> 8);
|
||||
data[2] = static_cast<uint8_t>(sample >> 16);
|
||||
data[3] = static_cast<uint8_t>(sample >> 24);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace audio
|
||||
} // namespace esphome
|
||||
|
||||
@@ -171,7 +171,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
|
||||
|
||||
bytes_available_before_processing = this->input_transfer_buffer_->available();
|
||||
|
||||
if ((this->potentially_failed_count_ > 10) && (bytes_read == 0)) {
|
||||
if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) {
|
||||
// Failed to decode in last attempt and there is no new data
|
||||
|
||||
if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) {
|
||||
|
||||
@@ -386,7 +386,7 @@ def validate_click_timing(value):
|
||||
return value
|
||||
|
||||
|
||||
BINARY_SENSOR_SCHEMA = (
|
||||
_BINARY_SENSOR_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
@@ -458,19 +458,17 @@ BINARY_SENSOR_SCHEMA = (
|
||||
)
|
||||
)
|
||||
|
||||
_UNDEF = object()
|
||||
|
||||
|
||||
def binary_sensor_schema(
|
||||
class_: MockObjClass = _UNDEF,
|
||||
class_: MockObjClass = cv.UNDEFINED,
|
||||
*,
|
||||
icon: str = _UNDEF,
|
||||
entity_category: str = _UNDEF,
|
||||
device_class: str = _UNDEF,
|
||||
icon: str = cv.UNDEFINED,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
device_class: str = cv.UNDEFINED,
|
||||
) -> cv.Schema:
|
||||
schema = {}
|
||||
|
||||
if class_ is not _UNDEF:
|
||||
if class_ is not cv.UNDEFINED:
|
||||
# Not cv.optional
|
||||
schema[cv.GenerateID()] = cv.declare_id(class_)
|
||||
|
||||
@@ -479,10 +477,15 @@ def binary_sensor_schema(
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
(CONF_DEVICE_CLASS, device_class, validate_device_class),
|
||||
]:
|
||||
if default is not _UNDEF:
|
||||
if default is not cv.UNDEFINED:
|
||||
schema[cv.Optional(key, default=default)] = validator
|
||||
|
||||
return BINARY_SENSOR_SCHEMA.extend(schema)
|
||||
return _BINARY_SENSOR_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
BINARY_SENSOR_SCHEMA = binary_sensor_schema()
|
||||
BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
|
||||
|
||||
|
||||
async def setup_binary_sensor_core_(var, config):
|
||||
|
||||
@@ -4,7 +4,6 @@ from esphome.components import ble_client, esp32_ble_tracker, text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CHARACTERISTIC_UUID,
|
||||
CONF_ID,
|
||||
CONF_NOTIFY,
|
||||
CONF_SERVICE_UUID,
|
||||
CONF_TRIGGER_ID,
|
||||
@@ -32,9 +31,9 @@ BLETextSensorNotifyTrigger = ble_client_ns.class_(
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
text_sensor.TEXT_SENSOR_SCHEMA.extend(
|
||||
text_sensor.text_sensor_schema(BLETextSensor)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BLETextSensor),
|
||||
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,
|
||||
@@ -54,7 +53,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
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]))
|
||||
@@ -101,7 +100,6 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
await ble_client.register_ble_node(var, config)
|
||||
cg.add(var.set_enable_notify(config[CONF_NOTIFY]))
|
||||
await text_sensor.register_text_sensor(var, config)
|
||||
for conf in config.get(CONF_ON_NOTIFY, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await ble_client.register_ble_node(trigger, config)
|
||||
|
||||
@@ -73,9 +73,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
resp.address = this->address_;
|
||||
resp.handle = param->read.handle;
|
||||
resp.data.reserve(param->read.value_len);
|
||||
for (uint16_t i = 0; i < param->read.value_len; i++) {
|
||||
resp.data.push_back(param->read.value[i]);
|
||||
}
|
||||
// Use bulk insert instead of individual push_backs
|
||||
resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len);
|
||||
this->proxy_->get_api_connection()->send_bluetooth_gatt_read_response(resp);
|
||||
break;
|
||||
}
|
||||
@@ -127,9 +126,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
resp.address = this->address_;
|
||||
resp.handle = param->notify.handle;
|
||||
resp.data.reserve(param->notify.value_len);
|
||||
for (uint16_t i = 0; i < param->notify.value_len; i++) {
|
||||
resp.data.push_back(param->notify.value[i]);
|
||||
}
|
||||
// Use bulk insert instead of individual push_backs
|
||||
resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len);
|
||||
this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_data_response(resp);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p
|
||||
return false;
|
||||
|
||||
api::BluetoothLERawAdvertisementsResponse resp;
|
||||
// Pre-allocate the advertisements vector to avoid reallocations
|
||||
resp.advertisements.reserve(count);
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
auto &result = advertisements[i];
|
||||
api::BluetoothLERawAdvertisement adv;
|
||||
@@ -65,9 +68,8 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p
|
||||
|
||||
uint8_t length = result.adv_data_len + result.scan_rsp_len;
|
||||
adv.data.reserve(length);
|
||||
for (uint16_t i = 0; i < length; i++) {
|
||||
adv.data.push_back(result.ble_adv[i]);
|
||||
}
|
||||
// Use a bulk insert instead of individual push_backs
|
||||
adv.data.insert(adv.data.end(), &result.ble_adv[0], &result.ble_adv[length]);
|
||||
|
||||
resp.advertisements.push_back(std::move(adv));
|
||||
|
||||
@@ -85,21 +87,34 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi
|
||||
if (!device.get_name().empty())
|
||||
resp.name = device.get_name();
|
||||
resp.rssi = device.get_rssi();
|
||||
for (auto uuid : device.get_service_uuids()) {
|
||||
|
||||
// Pre-allocate vectors based on known sizes
|
||||
auto service_uuids = device.get_service_uuids();
|
||||
resp.service_uuids.reserve(service_uuids.size());
|
||||
for (auto uuid : service_uuids) {
|
||||
resp.service_uuids.push_back(uuid.to_string());
|
||||
}
|
||||
for (auto &data : device.get_service_datas()) {
|
||||
|
||||
// Pre-allocate service data vector
|
||||
auto service_datas = device.get_service_datas();
|
||||
resp.service_data.reserve(service_datas.size());
|
||||
for (auto &data : service_datas) {
|
||||
api::BluetoothServiceData service_data;
|
||||
service_data.uuid = data.uuid.to_string();
|
||||
service_data.data.assign(data.data.begin(), data.data.end());
|
||||
resp.service_data.push_back(std::move(service_data));
|
||||
}
|
||||
for (auto &data : device.get_manufacturer_datas()) {
|
||||
|
||||
// Pre-allocate manufacturer data vector
|
||||
auto manufacturer_datas = device.get_manufacturer_datas();
|
||||
resp.manufacturer_data.reserve(manufacturer_datas.size());
|
||||
for (auto &data : manufacturer_datas) {
|
||||
api::BluetoothServiceData manufacturer_data;
|
||||
manufacturer_data.uuid = data.uuid.to_string();
|
||||
manufacturer_data.data.assign(data.data.begin(), data.data.end());
|
||||
resp.manufacturer_data.push_back(std::move(manufacturer_data));
|
||||
}
|
||||
|
||||
this->api_connection_->send_bluetooth_le_advertisement(resp);
|
||||
}
|
||||
|
||||
@@ -161,11 +176,27 @@ void BluetoothProxy::loop() {
|
||||
}
|
||||
api::BluetoothGATTGetServicesResponse resp;
|
||||
resp.address = connection->get_address();
|
||||
resp.services.reserve(1); // Always one service per response in this implementation
|
||||
api::BluetoothGATTService service_resp;
|
||||
service_resp.uuid = get_128bit_uuid_vec(service_result.uuid);
|
||||
service_resp.handle = service_result.start_handle;
|
||||
uint16_t char_offset = 0;
|
||||
esp_gattc_char_elem_t char_result;
|
||||
// Get the number of characteristics directly with one call
|
||||
uint16_t total_char_count = 0;
|
||||
esp_gatt_status_t char_count_status = esp_ble_gattc_get_attr_count(
|
||||
connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_CHARACTERISTIC,
|
||||
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
|
||||
|
||||
if (char_count_status == ESP_GATT_OK && total_char_count > 0) {
|
||||
// Only reserve if we successfully got a count
|
||||
service_resp.characteristics.reserve(total_char_count);
|
||||
} else if (char_count_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", connection->get_connection_index(),
|
||||
connection->address_str().c_str(), char_count_status);
|
||||
}
|
||||
|
||||
// Now process characteristics
|
||||
while (true) { // characteristics
|
||||
uint16_t char_count = 1;
|
||||
esp_gatt_status_t char_status = esp_ble_gattc_get_all_char(
|
||||
@@ -187,6 +218,23 @@ void BluetoothProxy::loop() {
|
||||
characteristic_resp.handle = char_result.char_handle;
|
||||
characteristic_resp.properties = char_result.properties;
|
||||
char_offset++;
|
||||
|
||||
// Get the number of descriptors directly with one call
|
||||
uint16_t total_desc_count = 0;
|
||||
esp_gatt_status_t desc_count_status =
|
||||
esp_ble_gattc_get_attr_count(connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_DESCRIPTOR,
|
||||
char_result.char_handle, service_result.end_handle, 0, &total_desc_count);
|
||||
|
||||
if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) {
|
||||
// Only reserve if we successfully got a count
|
||||
characteristic_resp.descriptors.reserve(total_desc_count);
|
||||
} else if (desc_count_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d",
|
||||
connection->get_connection_index(), connection->address_str().c_str(), char_result.char_handle,
|
||||
desc_count_status);
|
||||
}
|
||||
|
||||
// Now process descriptors
|
||||
uint16_t desc_offset = 0;
|
||||
esp_gattc_descr_elem_t desc_result;
|
||||
while (true) { // descriptors
|
||||
|
||||
@@ -44,7 +44,7 @@ ButtonPressTrigger = button_ns.class_(
|
||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
|
||||
|
||||
|
||||
BUTTON_SCHEMA = (
|
||||
_BUTTON_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
@@ -60,15 +60,13 @@ BUTTON_SCHEMA = (
|
||||
)
|
||||
)
|
||||
|
||||
_UNDEF = object()
|
||||
|
||||
|
||||
def button_schema(
|
||||
class_: MockObjClass,
|
||||
*,
|
||||
icon: str = _UNDEF,
|
||||
entity_category: str = _UNDEF,
|
||||
device_class: str = _UNDEF,
|
||||
icon: str = cv.UNDEFINED,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
device_class: str = cv.UNDEFINED,
|
||||
) -> cv.Schema:
|
||||
schema = {cv.GenerateID(): cv.declare_id(class_)}
|
||||
|
||||
@@ -77,10 +75,15 @@ def button_schema(
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
(CONF_DEVICE_CLASS, device_class, validate_device_class),
|
||||
]:
|
||||
if default is not _UNDEF:
|
||||
if default is not cv.UNDEFINED:
|
||||
schema[cv.Optional(key, default=default)] = validator
|
||||
|
||||
return BUTTON_SCHEMA.extend(schema)
|
||||
return _BUTTON_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
BUTTON_SCHEMA = button_schema(Button)
|
||||
BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
|
||||
|
||||
|
||||
async def setup_button_core_(var, config):
|
||||
|
||||
@@ -11,9 +11,11 @@ from esphome.const import (
|
||||
CONF_CURRENT_TEMPERATURE_STATE_TOPIC,
|
||||
CONF_CUSTOM_FAN_MODE,
|
||||
CONF_CUSTOM_PRESET,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_FAN_MODE,
|
||||
CONF_FAN_MODE_COMMAND_TOPIC,
|
||||
CONF_FAN_MODE_STATE_TOPIC,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_MAX_TEMPERATURE,
|
||||
CONF_MIN_TEMPERATURE,
|
||||
@@ -46,6 +48,7 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -151,12 +154,11 @@ ControlTrigger = climate_ns.class_(
|
||||
"ControlTrigger", automation.Trigger.template(ClimateCall.operator("ref"))
|
||||
)
|
||||
|
||||
CLIMATE_SCHEMA = (
|
||||
_CLIMATE_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Climate),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent),
|
||||
cv.Optional(CONF_VISUAL, default={}): cv.Schema(
|
||||
{
|
||||
@@ -245,6 +247,31 @@ CLIMATE_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
def climate_schema(
|
||||
class_: MockObjClass,
|
||||
*,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
icon: str = cv.UNDEFINED,
|
||||
) -> cv.Schema:
|
||||
schema = {
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
}
|
||||
|
||||
for key, default, validator in [
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
(CONF_ICON, icon, cv.icon),
|
||||
]:
|
||||
if default is not cv.UNDEFINED:
|
||||
schema[cv.Optional(key, default=default)] = validator
|
||||
|
||||
return _CLIMATE_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
CLIMATE_SCHEMA = climate_schema(Climate)
|
||||
CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
|
||||
|
||||
|
||||
async def setup_climate_core_(var, config):
|
||||
await setup_entity(var, config)
|
||||
|
||||
@@ -419,6 +446,12 @@ async def register_climate(var, config):
|
||||
await setup_climate_core_(var, config)
|
||||
|
||||
|
||||
async def new_climate(config, *args):
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_climate(var, config)
|
||||
return var
|
||||
|
||||
|
||||
CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Climate),
|
||||
|
||||
@@ -5,7 +5,6 @@ from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_SOURCE_ID,
|
||||
)
|
||||
from esphome.core.entity_helpers import inherit_property_from
|
||||
@@ -15,12 +14,15 @@ from .. import copy_ns
|
||||
CopyCover = copy_ns.class_("CopyCover", cover.Cover, cg.Component)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CopyCover),
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
CONFIG_SCHEMA = (
|
||||
cover.cover_schema(CopyCover)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
|
||||
@@ -30,8 +32,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cover.register_cover(var, config)
|
||||
var = await cover.new_cover(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
source = await cg.get_variable(config[CONF_SOURCE_ID])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import lock
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID
|
||||
from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID
|
||||
from esphome.core.entity_helpers import inherit_property_from
|
||||
|
||||
from .. import copy_ns
|
||||
@@ -9,12 +9,15 @@ from .. import copy_ns
|
||||
CopyLock = copy_ns.class_("CopyLock", lock.Lock, cg.Component)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CopyLock),
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(lock.Lock),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
CONFIG_SCHEMA = (
|
||||
lock.lock_schema(CopyLock)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(lock.Lock),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
|
||||
@@ -23,8 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await lock.register_lock(var, config)
|
||||
var = await lock.new_lock(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
source = await cg.get_variable(config[CONF_SOURCE_ID])
|
||||
|
||||
@@ -9,12 +9,15 @@ from .. import copy_ns
|
||||
CopyText = copy_ns.class_("CopyText", text.Text, cg.Component)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CopyText),
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(text.Text),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
CONFIG_SCHEMA = (
|
||||
text.text_schema(CopyText)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(text.Text),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
|
||||
|
||||
@@ -5,6 +5,8 @@ from esphome.components import mqtt, web_server
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_OPEN,
|
||||
@@ -31,6 +33,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_WINDOW,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -89,12 +92,11 @@ CoverClosedTrigger = cover_ns.class_(
|
||||
|
||||
CONF_ON_CLOSED = "on_closed"
|
||||
|
||||
COVER_SCHEMA = (
|
||||
_COVER_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Cover),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
|
||||
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
|
||||
@@ -124,6 +126,33 @@ COVER_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
def cover_schema(
|
||||
class_: MockObjClass,
|
||||
*,
|
||||
device_class: str = cv.UNDEFINED,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
icon: str = cv.UNDEFINED,
|
||||
) -> cv.Schema:
|
||||
schema = {
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
}
|
||||
|
||||
for key, default, validator in [
|
||||
(CONF_DEVICE_CLASS, device_class, cv.one_of(*DEVICE_CLASSES, lower=True)),
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
(CONF_ICON, icon, cv.icon),
|
||||
]:
|
||||
if default is not cv.UNDEFINED:
|
||||
schema[cv.Optional(key, default=default)] = validator
|
||||
|
||||
return _COVER_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
COVER_SCHEMA = cover_schema(Cover)
|
||||
COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
|
||||
|
||||
|
||||
async def setup_cover_core_(var, config):
|
||||
await setup_entity(var, config)
|
||||
|
||||
@@ -163,6 +192,12 @@ async def register_cover(var, config):
|
||||
await setup_cover_core_(var, config)
|
||||
|
||||
|
||||
async def new_cover(config, *args):
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_cover(var, config)
|
||||
return var
|
||||
|
||||
|
||||
COVER_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Cover),
|
||||
|
||||
28
esphome/components/cst226/binary_sensor/__init__.py
Normal file
28
esphome/components/cst226/binary_sensor/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from .. import cst226_ns
|
||||
from ..touchscreen import CST226ButtonListener, CST226Touchscreen
|
||||
|
||||
CONF_CST226_ID = "cst226_id"
|
||||
|
||||
CST226Button = cst226_ns.class_(
|
||||
"CST226Button",
|
||||
binary_sensor.BinarySensor,
|
||||
cg.Component,
|
||||
CST226ButtonListener,
|
||||
cg.Parented.template(CST226Touchscreen),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST226Button).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_CST226_ID): cv.use_id(CST226Touchscreen),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_CST226_ID])
|
||||
22
esphome/components/cst226/binary_sensor/cs226_button.h
Normal file
22
esphome/components/cst226/binary_sensor/cs226_button.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "../touchscreen/cst226_touchscreen.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace cst226 {
|
||||
|
||||
class CST226Button : public binary_sensor::BinarySensor,
|
||||
public Component,
|
||||
public CST226ButtonListener,
|
||||
public Parented<CST226Touchscreen> {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void update_button(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace cst226
|
||||
} // namespace esphome
|
||||
19
esphome/components/cst226/binary_sensor/cstt6_button.cpp
Normal file
19
esphome/components/cst226/binary_sensor/cstt6_button.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "cs226_button.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace cst226 {
|
||||
|
||||
static const char *const TAG = "CST226.binary_sensor";
|
||||
|
||||
void CST226Button::setup() {
|
||||
this->parent_->register_button_listener(this);
|
||||
this->publish_initial_state(false);
|
||||
}
|
||||
|
||||
void CST226Button::dump_config() { LOG_BINARY_SENSOR("", "CST226 Button", this); }
|
||||
|
||||
void CST226Button::update_button(bool state) { this->publish_state(state); }
|
||||
|
||||
} // namespace cst226
|
||||
} // namespace esphome
|
||||
@@ -3,8 +3,10 @@
|
||||
namespace esphome {
|
||||
namespace cst226 {
|
||||
|
||||
static const char *const TAG = "cst226.touchscreen";
|
||||
|
||||
void CST226Touchscreen::setup() {
|
||||
esph_log_config(TAG, "Setting up CST226 Touchscreen...");
|
||||
ESP_LOGCONFIG(TAG, "Setting up CST226 Touchscreen...");
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
@@ -26,6 +28,11 @@ void CST226Touchscreen::update_touches() {
|
||||
return;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
if (data[0] == 0x83 && data[1] == 0x17 && data[5] == 0x80) {
|
||||
this->update_button_state_(true);
|
||||
return;
|
||||
}
|
||||
this->update_button_state_(false);
|
||||
if (data[6] != 0xAB || data[0] == 0xAB || data[5] == 0x80) {
|
||||
this->skip_update_ = true;
|
||||
return;
|
||||
@@ -43,13 +50,21 @@ void CST226Touchscreen::update_touches() {
|
||||
int16_t y = (data[index + 2] << 4) | (data[index + 3] & 0x0F);
|
||||
int16_t z = data[index + 4];
|
||||
this->add_raw_touch_position_(id, x, y, z);
|
||||
esph_log_v(TAG, "Read touch %d: %d/%d", id, x, y);
|
||||
ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
|
||||
index += 5;
|
||||
if (i == 0)
|
||||
index += 2;
|
||||
}
|
||||
}
|
||||
|
||||
bool CST226Touchscreen::read16_(uint16_t addr, uint8_t *data, size_t len) {
|
||||
if (this->read_register16(addr, data, len) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Read data from 0x%04X failed", addr);
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void CST226Touchscreen::continue_setup_() {
|
||||
uint8_t buffer[8];
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
@@ -58,7 +73,7 @@ void CST226Touchscreen::continue_setup_() {
|
||||
}
|
||||
buffer[0] = 0xD1;
|
||||
if (this->write_register16(0xD1, buffer, 1) != i2c::ERROR_OK) {
|
||||
esph_log_e(TAG, "Write byte to 0xD1 failed");
|
||||
ESP_LOGE(TAG, "Write byte to 0xD1 failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -66,7 +81,7 @@ void CST226Touchscreen::continue_setup_() {
|
||||
if (this->read16_(0xD204, buffer, 4)) {
|
||||
uint16_t chip_id = buffer[2] + (buffer[3] << 8);
|
||||
uint16_t project_id = buffer[0] + (buffer[1] << 8);
|
||||
esph_log_config(TAG, "Chip ID %X, project ID %x", chip_id, project_id);
|
||||
ESP_LOGCONFIG(TAG, "Chip ID %X, project ID %x", chip_id, project_id);
|
||||
}
|
||||
if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
|
||||
if (this->read16_(0xD1F8, buffer, 4)) {
|
||||
@@ -80,7 +95,14 @@ void CST226Touchscreen::continue_setup_() {
|
||||
}
|
||||
}
|
||||
this->setup_complete_ = true;
|
||||
esph_log_config(TAG, "CST226 Touchscreen setup complete");
|
||||
ESP_LOGCONFIG(TAG, "CST226 Touchscreen setup complete");
|
||||
}
|
||||
void CST226Touchscreen::update_button_state_(bool state) {
|
||||
if (this->button_touched_ == state)
|
||||
return;
|
||||
this->button_touched_ = state;
|
||||
for (auto *listener : this->button_listeners_)
|
||||
listener->update_button(state);
|
||||
}
|
||||
|
||||
void CST226Touchscreen::dump_config() {
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
namespace esphome {
|
||||
namespace cst226 {
|
||||
|
||||
static const char *const TAG = "cst226.touchscreen";
|
||||
|
||||
static const uint8_t CST226_REG_STATUS = 0x00;
|
||||
|
||||
class CST226ButtonListener {
|
||||
public:
|
||||
virtual void update_button(bool state) = 0;
|
||||
};
|
||||
|
||||
class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -22,22 +25,19 @@ class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
bool can_proceed() override { return this->setup_complete_ || this->is_failed(); }
|
||||
void register_button_listener(CST226ButtonListener *listener) { this->button_listeners_.push_back(listener); }
|
||||
|
||||
protected:
|
||||
bool read16_(uint16_t addr, uint8_t *data, size_t len) {
|
||||
if (this->read_register16(addr, data, len) != i2c::ERROR_OK) {
|
||||
esph_log_e(TAG, "Read data from 0x%04X failed", addr);
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool read16_(uint16_t addr, uint8_t *data, size_t len);
|
||||
void continue_setup_();
|
||||
void update_button_state_(bool state);
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
uint8_t chip_id_{};
|
||||
bool setup_complete_{};
|
||||
std::vector<CST226ButtonListener *> button_listeners_;
|
||||
bool button_touched_{};
|
||||
};
|
||||
|
||||
} // namespace cst226
|
||||
|
||||
@@ -5,7 +5,6 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CLOSE_ACTION,
|
||||
CONF_CLOSE_DURATION,
|
||||
CONF_ID,
|
||||
CONF_MAX_DURATION,
|
||||
CONF_OPEN_ACTION,
|
||||
CONF_OPEN_DURATION,
|
||||
@@ -30,45 +29,47 @@ CurrentBasedCover = current_based_ns.class_(
|
||||
"CurrentBasedCover", cover.Cover, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CurrentBasedCover),
|
||||
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor),
|
||||
cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range(
|
||||
min=0, min_included=False
|
||||
),
|
||||
cv.Optional(CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
|
||||
min=0, min_included=False
|
||||
),
|
||||
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Required(CONF_CLOSE_SENSOR): cv.use_id(sensor.Sensor),
|
||||
cv.Required(CONF_CLOSE_MOVING_CURRENT_THRESHOLD): cv.float_range(
|
||||
min=0, min_included=False
|
||||
),
|
||||
cv.Optional(CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
|
||||
min=0, min_included=False
|
||||
),
|
||||
cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage,
|
||||
cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_MALFUNCTION_DETECTION, default=True): cv.boolean,
|
||||
cv.Optional(CONF_MALFUNCTION_ACTION): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_START_SENSING_DELAY, default="500ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
CONFIG_SCHEMA = (
|
||||
cover.cover_schema(CurrentBasedCover)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor),
|
||||
cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range(
|
||||
min=0, min_included=False
|
||||
),
|
||||
cv.Optional(CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
|
||||
min=0, min_included=False
|
||||
),
|
||||
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Required(CONF_CLOSE_SENSOR): cv.use_id(sensor.Sensor),
|
||||
cv.Required(CONF_CLOSE_MOVING_CURRENT_THRESHOLD): cv.float_range(
|
||||
min=0, min_included=False
|
||||
),
|
||||
cv.Optional(CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
|
||||
min=0, min_included=False
|
||||
),
|
||||
cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage,
|
||||
cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_MALFUNCTION_DETECTION, default=True): cv.boolean,
|
||||
cv.Optional(CONF_MALFUNCTION_ACTION): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_START_SENSING_DELAY, default="500ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
var = await cover.new_cover(config)
|
||||
await cg.register_component(var, config)
|
||||
await cover.register_cover(var, config)
|
||||
|
||||
await automation.build_automation(
|
||||
var.get_stop_trigger(), [], config[CONF_STOP_ACTION]
|
||||
|
||||
@@ -56,21 +56,13 @@ void DallasTemperatureSensor::update() {
|
||||
});
|
||||
}
|
||||
|
||||
void IRAM_ATTR DallasTemperatureSensor::read_scratch_pad_int_() {
|
||||
for (uint8_t &i : this->scratch_pad_) {
|
||||
i = this->bus_->read8();
|
||||
}
|
||||
}
|
||||
|
||||
bool DallasTemperatureSensor::read_scratch_pad_() {
|
||||
bool success;
|
||||
{
|
||||
InterruptLock lock;
|
||||
success = this->send_command_(DALLAS_COMMAND_READ_SCRATCH_PAD);
|
||||
if (success)
|
||||
this->read_scratch_pad_int_();
|
||||
}
|
||||
if (!success) {
|
||||
bool success = this->send_command_(DALLAS_COMMAND_READ_SCRATCH_PAD);
|
||||
if (success) {
|
||||
for (uint8_t &i : this->scratch_pad_) {
|
||||
i = this->bus_->read8();
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - reading scratch pad failed bus reset", this->get_name().c_str());
|
||||
this->status_set_warning("bus reset failed");
|
||||
}
|
||||
@@ -113,17 +105,14 @@ void DallasTemperatureSensor::setup() {
|
||||
return;
|
||||
this->scratch_pad_[4] = res;
|
||||
|
||||
{
|
||||
InterruptLock lock;
|
||||
if (this->send_command_(DALLAS_COMMAND_WRITE_SCRATCH_PAD)) {
|
||||
this->bus_->write8(this->scratch_pad_[2]); // high alarm temp
|
||||
this->bus_->write8(this->scratch_pad_[3]); // low alarm temp
|
||||
this->bus_->write8(this->scratch_pad_[4]); // resolution
|
||||
}
|
||||
|
||||
// write value to EEPROM
|
||||
this->send_command_(DALLAS_COMMAND_COPY_SCRATCH_PAD);
|
||||
if (this->send_command_(DALLAS_COMMAND_WRITE_SCRATCH_PAD)) {
|
||||
this->bus_->write8(this->scratch_pad_[2]); // high alarm temp
|
||||
this->bus_->write8(this->scratch_pad_[3]); // low alarm temp
|
||||
this->bus_->write8(this->scratch_pad_[4]); // resolution
|
||||
}
|
||||
|
||||
// write value to EEPROM
|
||||
this->send_command_(DALLAS_COMMAND_COPY_SCRATCH_PAD);
|
||||
}
|
||||
|
||||
bool DallasTemperatureSensor::check_scratch_pad_() {
|
||||
@@ -138,6 +127,10 @@ bool DallasTemperatureSensor::check_scratch_pad_() {
|
||||
if (!chksum_validity) {
|
||||
ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str());
|
||||
this->status_set_warning("scratch pad checksum invalid");
|
||||
ESP_LOGD(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0],
|
||||
this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4],
|
||||
this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8],
|
||||
crc8(this->scratch_pad_, 8));
|
||||
}
|
||||
return chksum_validity;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ class DallasTemperatureSensor : public PollingComponent, public sensor::Sensor,
|
||||
/// Get the number of milliseconds we have to wait for the conversion phase.
|
||||
uint16_t millis_to_wait_for_conversion_() const;
|
||||
bool read_scratch_pad_();
|
||||
void read_scratch_pad_int_();
|
||||
bool check_scratch_pad_();
|
||||
float get_temp_c_();
|
||||
};
|
||||
|
||||
@@ -17,7 +17,6 @@ from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_FORCE_UPDATE,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_INVERTED,
|
||||
CONF_MAX_VALUE,
|
||||
CONF_MIN_VALUE,
|
||||
@@ -153,9 +152,10 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
},
|
||||
],
|
||||
): [
|
||||
climate.CLIMATE_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend(
|
||||
climate.climate_schema(DemoClimate)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DemoClimate),
|
||||
cv.Required(CONF_TYPE): cv.enum(CLIMATE_TYPES, int=True),
|
||||
}
|
||||
)
|
||||
@@ -183,9 +183,10 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
},
|
||||
],
|
||||
): [
|
||||
cover.COVER_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend(
|
||||
cover.cover_schema(DemoCover)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DemoCover),
|
||||
cv.Required(CONF_TYPE): cv.enum(COVER_TYPES, int=True),
|
||||
}
|
||||
)
|
||||
@@ -211,9 +212,10 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
},
|
||||
],
|
||||
): [
|
||||
fan.FAN_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend(
|
||||
fan.fan_schema(DemoFan)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoFan),
|
||||
cv.Required(CONF_TYPE): cv.enum(FAN_TYPES, int=True),
|
||||
}
|
||||
)
|
||||
@@ -251,7 +253,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
},
|
||||
],
|
||||
): [
|
||||
light.RGB_LIGHT_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend(
|
||||
light.light_schema(DemoLight, light.LightType.RGB)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoLight),
|
||||
cv.Required(CONF_TYPE): cv.enum(LIGHT_TYPES, int=True),
|
||||
@@ -377,39 +381,33 @@ async def to_code(config):
|
||||
await cg.register_component(var, conf)
|
||||
|
||||
for conf in config[CONF_CLIMATES]:
|
||||
var = cg.new_Pvariable(conf[CONF_ID])
|
||||
var = await climate.new_climate(conf)
|
||||
await cg.register_component(var, conf)
|
||||
await climate.register_climate(var, conf)
|
||||
cg.add(var.set_type(conf[CONF_TYPE]))
|
||||
|
||||
for conf in config[CONF_COVERS]:
|
||||
var = cg.new_Pvariable(conf[CONF_ID])
|
||||
var = await cover.new_cover(conf)
|
||||
await cg.register_component(var, conf)
|
||||
await cover.register_cover(var, conf)
|
||||
cg.add(var.set_type(conf[CONF_TYPE]))
|
||||
|
||||
for conf in config[CONF_FANS]:
|
||||
var = cg.new_Pvariable(conf[CONF_OUTPUT_ID])
|
||||
var = await fan.new_fan(conf)
|
||||
await cg.register_component(var, conf)
|
||||
await fan.register_fan(var, conf)
|
||||
cg.add(var.set_type(conf[CONF_TYPE]))
|
||||
|
||||
for conf in config[CONF_LIGHTS]:
|
||||
var = cg.new_Pvariable(conf[CONF_OUTPUT_ID])
|
||||
var = await light.new_light(conf)
|
||||
await cg.register_component(var, conf)
|
||||
await light.register_light(var, conf)
|
||||
cg.add(var.set_type(conf[CONF_TYPE]))
|
||||
|
||||
for conf in config[CONF_NUMBERS]:
|
||||
var = cg.new_Pvariable(conf[CONF_ID])
|
||||
await cg.register_component(var, conf)
|
||||
await number.register_number(
|
||||
var,
|
||||
var = await number.new_number(
|
||||
conf,
|
||||
min_value=conf[CONF_MIN_VALUE],
|
||||
max_value=conf[CONF_MAX_VALUE],
|
||||
step=conf[CONF_STEP],
|
||||
)
|
||||
await cg.register_component(var, conf)
|
||||
cg.add(var.set_type(conf[CONF_TYPE]))
|
||||
|
||||
for conf in config[CONF_SENSORS]:
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component
|
||||
|
||||
@@ -26,32 +27,30 @@ Sen0395StartAfterBootSwitch = dfrobot_sen0395_ns.class_(
|
||||
"Sen0395StartAfterBootSwitch", DfrobotSen0395Switch
|
||||
)
|
||||
|
||||
_SWITCH_SCHEMA = (
|
||||
switch.switch_schema(
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
|
||||
def _switch_schema(class_: MockObjClass) -> cv.Schema:
|
||||
return (
|
||||
switch.switch_schema(
|
||||
class_,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(
|
||||
DfrobotSen0395Component
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
"sensor_active": _SWITCH_SCHEMA.extend(
|
||||
{cv.GenerateID(): cv.declare_id(Sen0395PowerSwitch)}
|
||||
),
|
||||
"turn_on_led": _SWITCH_SCHEMA.extend(
|
||||
{cv.GenerateID(): cv.declare_id(Sen0395LedSwitch)}
|
||||
),
|
||||
"presence_via_uart": _SWITCH_SCHEMA.extend(
|
||||
{cv.GenerateID(): cv.declare_id(Sen0395UartPresenceSwitch)}
|
||||
),
|
||||
"start_after_boot": _SWITCH_SCHEMA.extend(
|
||||
{cv.GenerateID(): cv.declare_id(Sen0395StartAfterBootSwitch)}
|
||||
),
|
||||
"sensor_active": _switch_schema(Sen0395PowerSwitch),
|
||||
"turn_on_led": _switch_schema(Sen0395LedSwitch),
|
||||
"presence_via_uart": _switch_schema(Sen0395UartPresenceSwitch),
|
||||
"start_after_boot": _switch_schema(Sen0395StartAfterBootSwitch),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from esphome.const import (
|
||||
CONF_CLOSE_ACTION,
|
||||
CONF_CLOSE_DURATION,
|
||||
CONF_CLOSE_ENDSTOP,
|
||||
CONF_ID,
|
||||
CONF_MAX_DURATION,
|
||||
CONF_OPEN_ACTION,
|
||||
CONF_OPEN_DURATION,
|
||||
@@ -17,25 +16,27 @@ from esphome.const import (
|
||||
endstop_ns = cg.esphome_ns.namespace("endstop")
|
||||
EndstopCover = endstop_ns.class_("EndstopCover", cover.Cover, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(EndstopCover),
|
||||
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
CONFIG_SCHEMA = (
|
||||
cover.cover_schema(EndstopCover)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
var = await cover.new_cover(config)
|
||||
await cg.register_component(var, config)
|
||||
await cover.register_cover(var, config)
|
||||
|
||||
await automation.build_automation(
|
||||
var.get_stop_trigger(), [], config[CONF_STOP_ACTION]
|
||||
|
||||
@@ -2,42 +2,66 @@
|
||||
|
||||
#include "gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/rtc_io.h"
|
||||
#include "hal/gpio_hal.h"
|
||||
#include "soc/soc_caps.h"
|
||||
#include "soc/gpio_periph.h"
|
||||
#include <cinttypes>
|
||||
|
||||
#if (SOC_RTCIO_PIN_COUNT > 0)
|
||||
#include "hal/rtc_io_hal.h"
|
||||
#endif
|
||||
|
||||
#ifndef SOC_GPIO_SUPPORT_RTC_INDEPENDENT
|
||||
#define SOC_GPIO_SUPPORT_RTC_INDEPENDENT 0 // NOLINT
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32 {
|
||||
|
||||
static const char *const TAG = "esp32";
|
||||
|
||||
static const gpio_hal_context_t GPIO_HAL = {.dev = GPIO_HAL_GET_HW(GPIO_PORT_0)};
|
||||
|
||||
bool ESP32InternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static gpio_mode_t IRAM_ATTR flags_to_mode(gpio::Flags flags) {
|
||||
static gpio_mode_t flags_to_mode(gpio::Flags flags) {
|
||||
flags = (gpio::Flags)(flags & ~(gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN));
|
||||
if (flags == gpio::FLAG_INPUT) {
|
||||
if (flags == gpio::FLAG_INPUT)
|
||||
return GPIO_MODE_INPUT;
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
if (flags == gpio::FLAG_OUTPUT)
|
||||
return GPIO_MODE_OUTPUT;
|
||||
} else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) {
|
||||
if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN))
|
||||
return GPIO_MODE_OUTPUT_OD;
|
||||
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) {
|
||||
if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN))
|
||||
return GPIO_MODE_INPUT_OUTPUT_OD;
|
||||
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT)) {
|
||||
if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT))
|
||||
return GPIO_MODE_INPUT_OUTPUT;
|
||||
} else {
|
||||
// unsupported or gpio::FLAG_NONE
|
||||
return GPIO_MODE_DISABLE;
|
||||
}
|
||||
// unsupported or gpio::FLAG_NONE
|
||||
return GPIO_MODE_DISABLE;
|
||||
}
|
||||
|
||||
struct ISRPinArg {
|
||||
gpio_num_t pin;
|
||||
gpio::Flags flags;
|
||||
bool inverted;
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
bool use_rtc;
|
||||
int rtc_pin;
|
||||
#endif
|
||||
};
|
||||
|
||||
ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const {
|
||||
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
arg->pin = pin_;
|
||||
arg->pin = this->pin_;
|
||||
arg->flags = gpio::FLAG_NONE;
|
||||
arg->inverted = inverted_;
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
arg->use_rtc = rtc_gpio_is_valid_gpio(this->pin_);
|
||||
if (arg->use_rtc)
|
||||
arg->rtc_pin = rtc_io_number_get(this->pin_);
|
||||
#endif
|
||||
return ISRInternalGPIOPin((void *) arg);
|
||||
}
|
||||
|
||||
@@ -90,6 +114,7 @@ void ESP32InternalGPIOPin::setup() {
|
||||
if (flags_ & gpio::FLAG_OUTPUT) {
|
||||
gpio_set_drive_capability(pin_, drive_strength_);
|
||||
}
|
||||
ESP_LOGD(TAG, "rtc: %d", SOC_GPIO_SUPPORT_RTC_INDEPENDENT);
|
||||
}
|
||||
|
||||
void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
@@ -115,28 +140,65 @@ void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); }
|
||||
using namespace esp32;
|
||||
|
||||
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
|
||||
return bool(gpio_get_level(arg->pin)) != arg->inverted;
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
|
||||
return bool(gpio_hal_get_level(&GPIO_HAL, arg->pin)) != arg->inverted;
|
||||
}
|
||||
|
||||
void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
|
||||
gpio_set_level(arg->pin, value != arg->inverted ? 1 : 0);
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
|
||||
gpio_hal_set_level(&GPIO_HAL, arg->pin, value != arg->inverted);
|
||||
}
|
||||
|
||||
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
|
||||
// not supported
|
||||
}
|
||||
|
||||
void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
|
||||
gpio_set_direction(arg->pin, flags_to_mode(flags));
|
||||
gpio_pull_mode_t pull_mode = GPIO_FLOATING;
|
||||
if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) {
|
||||
pull_mode = GPIO_PULLUP_PULLDOWN;
|
||||
} else if (flags & gpio::FLAG_PULLUP) {
|
||||
pull_mode = GPIO_PULLUP_ONLY;
|
||||
} else if (flags & gpio::FLAG_PULLDOWN) {
|
||||
pull_mode = GPIO_PULLDOWN_ONLY;
|
||||
gpio::Flags diff = (gpio::Flags)(flags ^ arg->flags);
|
||||
if (diff & gpio::FLAG_OUTPUT) {
|
||||
if (flags & gpio::FLAG_OUTPUT) {
|
||||
gpio_hal_output_enable(&GPIO_HAL, arg->pin);
|
||||
if (flags & gpio::FLAG_OPEN_DRAIN)
|
||||
gpio_hal_od_enable(&GPIO_HAL, arg->pin);
|
||||
} else {
|
||||
gpio_hal_output_disable(&GPIO_HAL, arg->pin);
|
||||
}
|
||||
}
|
||||
gpio_set_pull_mode(arg->pin, pull_mode);
|
||||
if (diff & gpio::FLAG_INPUT) {
|
||||
if (flags & gpio::FLAG_INPUT) {
|
||||
gpio_hal_input_enable(&GPIO_HAL, arg->pin);
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
if (arg->use_rtc) {
|
||||
if (flags & gpio::FLAG_PULLUP) {
|
||||
rtcio_hal_pullup_enable(arg->rtc_pin);
|
||||
} else {
|
||||
rtcio_hal_pullup_disable(arg->rtc_pin);
|
||||
}
|
||||
if (flags & gpio::FLAG_PULLDOWN) {
|
||||
rtcio_hal_pulldown_enable(arg->rtc_pin);
|
||||
} else {
|
||||
rtcio_hal_pulldown_disable(arg->rtc_pin);
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (flags & gpio::FLAG_PULLUP) {
|
||||
gpio_hal_pullup_en(&GPIO_HAL, arg->pin);
|
||||
} else {
|
||||
gpio_hal_pullup_dis(&GPIO_HAL, arg->pin);
|
||||
}
|
||||
if (flags & gpio::FLAG_PULLDOWN) {
|
||||
gpio_hal_pulldown_en(&GPIO_HAL, arg->pin);
|
||||
} else {
|
||||
gpio_hal_pulldown_dis(&GPIO_HAL, arg->pin);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
gpio_hal_input_disable(&GPIO_HAL, arg->pin);
|
||||
}
|
||||
}
|
||||
arg->flags = flags;
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@@ -64,8 +64,7 @@ def _lookup_pin(value):
|
||||
def _translate_pin(value):
|
||||
if isinstance(value, dict) or value is None:
|
||||
raise cv.Invalid(
|
||||
"This variable only supports pin numbers, not full pin schemas "
|
||||
"(with inverted and mode)."
|
||||
"This variable only supports pin numbers, not full pin schemas (with inverted and mode)."
|
||||
)
|
||||
if isinstance(value, int) and not isinstance(value, bool):
|
||||
return value
|
||||
@@ -82,30 +81,22 @@ def _translate_pin(value):
|
||||
|
||||
@dataclass
|
||||
class ESP32ValidationFunctions:
|
||||
pin_validation: Any
|
||||
usage_validation: Any
|
||||
pin_validation: Callable[[Any], Any]
|
||||
usage_validation: Callable[[Any], Any]
|
||||
|
||||
|
||||
_esp32_validations = {
|
||||
VARIANT_ESP32: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_validate_gpio_pin, usage_validation=esp32_validate_supports
|
||||
),
|
||||
VARIANT_ESP32S2: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_s2_validate_gpio_pin,
|
||||
usage_validation=esp32_s2_validate_supports,
|
||||
VARIANT_ESP32C2: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_c2_validate_gpio_pin,
|
||||
usage_validation=esp32_c2_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32C3: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_c3_validate_gpio_pin,
|
||||
usage_validation=esp32_c3_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32S3: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_s3_validate_gpio_pin,
|
||||
usage_validation=esp32_s3_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32C2: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_c2_validate_gpio_pin,
|
||||
usage_validation=esp32_c2_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32C6: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_c6_validate_gpio_pin,
|
||||
usage_validation=esp32_c6_validate_supports,
|
||||
@@ -114,6 +105,14 @@ _esp32_validations = {
|
||||
pin_validation=esp32_h2_validate_gpio_pin,
|
||||
usage_validation=esp32_h2_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32S2: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_s2_validate_gpio_pin,
|
||||
usage_validation=esp32_s2_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32S3: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_s3_validate_gpio_pin,
|
||||
usage_validation=esp32_s3_validate_supports,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -31,8 +31,7 @@ def esp32_validate_gpio_pin(value):
|
||||
)
|
||||
if 9 <= value <= 10:
|
||||
_LOGGER.warning(
|
||||
"Pin %s (9-10) might already be used by the "
|
||||
"flash interface in QUAD IO flash mode.",
|
||||
"Pin %s (9-10) might already be used by the flash interface in QUAD IO flash mode.",
|
||||
value,
|
||||
)
|
||||
if value in (24, 28, 29, 30, 31):
|
||||
|
||||
@@ -22,7 +22,7 @@ def esp32_c2_validate_supports(value):
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 20:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-20)")
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-20)")
|
||||
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
|
||||
@@ -35,7 +35,7 @@ def esp32_c3_validate_supports(value):
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 21:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)")
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-21)")
|
||||
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
|
||||
@@ -36,7 +36,7 @@ def esp32_c6_validate_supports(value):
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 23:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-23)")
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-23)")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
@@ -45,7 +45,7 @@ def esp32_h2_validate_supports(value):
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 27:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)")
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-27)")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
@@ -44,6 +44,7 @@ CONF_ESP32_BLE_ID = "esp32_ble_id"
|
||||
CONF_SCAN_PARAMETERS = "scan_parameters"
|
||||
CONF_WINDOW = "window"
|
||||
CONF_ON_SCAN_END = "on_scan_end"
|
||||
CONF_SOFTWARE_COEXISTENCE = "software_coexistence"
|
||||
|
||||
DEFAULT_MAX_CONNECTIONS = 3
|
||||
IDF_MAX_CONNECTIONS = 9
|
||||
@@ -203,6 +204,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ON_SCAN_END): automation.validate_automation(
|
||||
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEEndOfScanTrigger)}
|
||||
),
|
||||
cv.OnlyWith(CONF_SOFTWARE_COEXISTENCE, "wifi", default=True): bool,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
)
|
||||
@@ -310,6 +312,8 @@ async def to_code(config):
|
||||
|
||||
if CORE.using_esp_idf:
|
||||
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
|
||||
if config.get(CONF_SOFTWARE_COEXISTENCE):
|
||||
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", True)
|
||||
# https://github.com/espressif/esp-idf/issues/4101
|
||||
# https://github.com/espressif/esp-idf/issues/2503
|
||||
# Match arduino CONFIG_BTU_TASK_STACK_SIZE
|
||||
@@ -331,6 +335,8 @@ async def to_code(config):
|
||||
|
||||
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
|
||||
cg.add_define("USE_ESP32_BLE_CLIENT")
|
||||
if config.get(CONF_SOFTWARE_COEXISTENCE):
|
||||
cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE")
|
||||
|
||||
|
||||
ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
#include <esp_coexist.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#include <esp32-hal-bt.h>
|
||||
#endif
|
||||
@@ -194,9 +198,17 @@ void ESP32BLETracker::loop() {
|
||||
https://github.com/espressif/esp-idf/issues/6688
|
||||
|
||||
*/
|
||||
if (this->scanner_state_ == ScannerState::IDLE && this->scan_continuous_ && !connecting && !disconnecting &&
|
||||
!promote_to_connecting) {
|
||||
this->start_scan_(false); // first = false
|
||||
if (this->scanner_state_ == ScannerState::IDLE && !connecting && !disconnecting && !promote_to_connecting) {
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
if (this->coex_prefer_ble_) {
|
||||
this->coex_prefer_ble_ = false;
|
||||
ESP_LOGD(TAG, "Setting coexistence preference to balanced.");
|
||||
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
|
||||
}
|
||||
#endif
|
||||
if (this->scan_continuous_) {
|
||||
this->start_scan_(false); // first = false
|
||||
}
|
||||
}
|
||||
// If there is a discovered client and no connecting
|
||||
// clients and no clients using the scanner to search for
|
||||
@@ -213,6 +225,13 @@ void ESP32BLETracker::loop() {
|
||||
ESP_LOGD(TAG, "Promoting client to connect...");
|
||||
// We only want to promote one client at a time.
|
||||
// once the scanner is fully stopped.
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
|
||||
if (!this->coex_prefer_ble_) {
|
||||
this->coex_prefer_ble_ = true;
|
||||
esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth
|
||||
}
|
||||
#endif
|
||||
client->set_state(ClientState::READY_TO_CONNECT);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -299,6 +299,9 @@ class ESP32BLETracker : public Component,
|
||||
int discovered_{0};
|
||||
int searching_{0};
|
||||
int disconnecting_{0};
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
bool coex_prefer_ble_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace esp8266 {
|
||||
|
||||
static const char *const TAG = "esp8266";
|
||||
|
||||
static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) {
|
||||
static int flags_to_mode(gpio::Flags flags, uint8_t pin) {
|
||||
if (flags == gpio::FLAG_INPUT) { // NOLINT(bugprone-branch-clone)
|
||||
return INPUT;
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
@@ -34,12 +34,36 @@ static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) {
|
||||
struct ISRPinArg {
|
||||
uint8_t pin;
|
||||
bool inverted;
|
||||
volatile uint32_t *in_reg;
|
||||
volatile uint32_t *out_set_reg;
|
||||
volatile uint32_t *out_clr_reg;
|
||||
volatile uint32_t *mode_set_reg;
|
||||
volatile uint32_t *mode_clr_reg;
|
||||
volatile uint32_t *func_reg;
|
||||
uint32_t mask;
|
||||
};
|
||||
|
||||
ISRInternalGPIOPin ESP8266GPIOPin::to_isr() const {
|
||||
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
arg->pin = pin_;
|
||||
arg->inverted = inverted_;
|
||||
arg->pin = this->pin_;
|
||||
arg->inverted = this->inverted_;
|
||||
if (this->pin_ < 16) {
|
||||
arg->in_reg = &GPI;
|
||||
arg->out_set_reg = &GPOS;
|
||||
arg->out_clr_reg = &GPOC;
|
||||
arg->mode_set_reg = &GPES;
|
||||
arg->mode_clr_reg = &GPEC;
|
||||
arg->func_reg = &GPF(this->pin_);
|
||||
arg->mask = 1 << this->pin_;
|
||||
} else {
|
||||
arg->in_reg = &GP16I;
|
||||
arg->out_set_reg = &GP16O;
|
||||
arg->out_clr_reg = nullptr;
|
||||
arg->mode_set_reg = &GP16E;
|
||||
arg->mode_clr_reg = nullptr;
|
||||
arg->func_reg = &GPF16;
|
||||
arg->mask = 1;
|
||||
}
|
||||
return ISRInternalGPIOPin((void *) arg);
|
||||
}
|
||||
|
||||
@@ -88,20 +112,57 @@ void ESP8266GPIOPin::detach_interrupt() const { detachInterrupt(pin_); }
|
||||
using namespace esp8266;
|
||||
|
||||
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
|
||||
return bool(digitalRead(arg->pin)) != arg->inverted; // NOLINT
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
|
||||
return bool(*arg->in_reg & arg->mask) != arg->inverted;
|
||||
}
|
||||
|
||||
void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
|
||||
digitalWrite(arg->pin, value != arg->inverted ? 1 : 0); // NOLINT
|
||||
if (arg->pin < 16) {
|
||||
if (value != arg->inverted) {
|
||||
*arg->out_set_reg = arg->mask;
|
||||
} else {
|
||||
*arg->out_clr_reg = arg->mask;
|
||||
}
|
||||
} else {
|
||||
if (value != arg->inverted) {
|
||||
*arg->out_set_reg |= 1;
|
||||
} else {
|
||||
*arg->out_set_reg &= ~1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
|
||||
GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin);
|
||||
}
|
||||
|
||||
void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
|
||||
pinMode(arg->pin, flags_to_mode(flags, arg->pin)); // NOLINT
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
|
||||
if (arg->pin < 16) {
|
||||
if (flags & gpio::FLAG_OUTPUT) {
|
||||
*arg->mode_set_reg = arg->mask;
|
||||
} else {
|
||||
*arg->mode_clr_reg = arg->mask;
|
||||
}
|
||||
if (flags & gpio::FLAG_PULLUP) {
|
||||
*arg->func_reg |= 1 << GPFPU;
|
||||
} else {
|
||||
*arg->func_reg &= ~(1 << GPFPU);
|
||||
}
|
||||
} else {
|
||||
if (flags & gpio::FLAG_OUTPUT) {
|
||||
*arg->mode_set_reg |= 1;
|
||||
} else {
|
||||
*arg->mode_set_reg &= ~1;
|
||||
}
|
||||
if (flags & gpio::FLAG_PULLDOWN) {
|
||||
*arg->func_reg |= 1 << GP16FPD;
|
||||
} else {
|
||||
*arg->func_reg &= ~(1 << GP16FPD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -41,7 +41,7 @@ EventTrigger = event_ns.class_("EventTrigger", automation.Trigger.template())
|
||||
|
||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
|
||||
|
||||
EVENT_SCHEMA = (
|
||||
_EVENT_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
@@ -58,19 +58,17 @@ EVENT_SCHEMA = (
|
||||
)
|
||||
)
|
||||
|
||||
_UNDEF = object()
|
||||
|
||||
|
||||
def event_schema(
|
||||
class_: MockObjClass = _UNDEF,
|
||||
class_: MockObjClass = cv.UNDEFINED,
|
||||
*,
|
||||
icon: str = _UNDEF,
|
||||
entity_category: str = _UNDEF,
|
||||
device_class: str = _UNDEF,
|
||||
icon: str = cv.UNDEFINED,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
device_class: str = cv.UNDEFINED,
|
||||
) -> cv.Schema:
|
||||
schema = {}
|
||||
|
||||
if class_ is not _UNDEF:
|
||||
if class_ is not cv.UNDEFINED:
|
||||
schema[cv.GenerateID()] = cv.declare_id(class_)
|
||||
|
||||
for key, default, validator in [
|
||||
@@ -78,10 +76,15 @@ def event_schema(
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
(CONF_DEVICE_CLASS, device_class, validate_device_class),
|
||||
]:
|
||||
if default is not _UNDEF:
|
||||
if default is not cv.UNDEFINED:
|
||||
schema[cv.Optional(key, default=default)] = validator
|
||||
|
||||
return EVENT_SCHEMA.extend(schema)
|
||||
return _EVENT_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
EVENT_SCHEMA = event_schema()
|
||||
EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event"))
|
||||
|
||||
|
||||
async def setup_event_core_(var, config, *, event_types: list[str]):
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_INVERTED,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ICON_RESTART_ALERT,
|
||||
)
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT
|
||||
|
||||
from .. import factory_reset_ns
|
||||
|
||||
@@ -16,21 +9,14 @@ FactoryResetSwitch = factory_reset_ns.class_(
|
||||
"FactoryResetSwitch", switch.Switch, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(FactoryResetSwitch),
|
||||
cv.Optional(CONF_INVERTED): cv.invalid(
|
||||
"Factory Reset switches do not support inverted mode!"
|
||||
),
|
||||
cv.Optional(CONF_ICON, default=ICON_RESTART_ALERT): cv.icon,
|
||||
cv.Optional(
|
||||
CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG
|
||||
): cv.entity_category,
|
||||
}
|
||||
CONFIG_SCHEMA = switch.switch_schema(
|
||||
FactoryResetSwitch,
|
||||
block_inverted=True,
|
||||
icon=ICON_RESTART_ALERT,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_component(var, config)
|
||||
await switch.register_switch(var, config)
|
||||
|
||||
@@ -5,6 +5,10 @@ from esphome.components import mqtt, web_server
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DIRECTION,
|
||||
CONF_DIRECTION_COMMAND_TOPIC,
|
||||
CONF_DIRECTION_STATE_TOPIC,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_OFF_SPEED_CYCLE,
|
||||
@@ -80,16 +84,21 @@ FanPresetSetTrigger = fan_ns.class_(
|
||||
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
|
||||
FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template())
|
||||
|
||||
FAN_SCHEMA = (
|
||||
_FAN_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Fan),
|
||||
cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum(
|
||||
RESTORE_MODES, upper=True, space="_"
|
||||
),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTFanComponent),
|
||||
cv.Optional(CONF_DIRECTION_STATE_TOPIC): cv.All(
|
||||
cv.requires_component("mqtt"), cv.publish_topic
|
||||
),
|
||||
cv.Optional(CONF_DIRECTION_COMMAND_TOPIC): cv.All(
|
||||
cv.requires_component("mqtt"), cv.subscribe_topic
|
||||
),
|
||||
cv.Optional(CONF_OSCILLATION_STATE_TOPIC): cv.All(
|
||||
cv.requires_component("mqtt"), cv.publish_topic
|
||||
),
|
||||
@@ -151,6 +160,37 @@ FAN_SCHEMA = (
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def fan_schema(
|
||||
class_: cg.Pvariable,
|
||||
*,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
icon: str = cv.UNDEFINED,
|
||||
default_restore_mode: str = cv.UNDEFINED,
|
||||
) -> cv.Schema:
|
||||
schema = {
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
}
|
||||
|
||||
for key, default, validator in [
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
(CONF_ICON, icon, cv.icon),
|
||||
(
|
||||
CONF_RESTORE_MODE,
|
||||
default_restore_mode,
|
||||
cv.enum(RESTORE_MODES, upper=True, space="_"),
|
||||
),
|
||||
]:
|
||||
if default is not cv.UNDEFINED:
|
||||
schema[cv.Optional(key, default=default)] = validator
|
||||
|
||||
return _FAN_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
FAN_SCHEMA = fan_schema(Fan)
|
||||
FAN_SCHEMA.add_extra(cv.deprecated_schema_constant("fan"))
|
||||
|
||||
_PRESET_MODES_SCHEMA = cv.All(
|
||||
cv.ensure_list(cv.string_strict),
|
||||
cv.Length(min=1),
|
||||
@@ -193,6 +233,14 @@ async def setup_fan_core_(var, config):
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
|
||||
if (
|
||||
direction_state_topic := config.get(CONF_DIRECTION_STATE_TOPIC)
|
||||
) is not None:
|
||||
cg.add(mqtt_.set_custom_direction_state_topic(direction_state_topic))
|
||||
if (
|
||||
direction_command_topic := config.get(CONF_DIRECTION_COMMAND_TOPIC)
|
||||
) is not None:
|
||||
cg.add(mqtt_.set_custom_direction_command_topic(direction_command_topic))
|
||||
if (
|
||||
oscillation_state_topic := config.get(CONF_OSCILLATION_STATE_TOPIC)
|
||||
) is not None:
|
||||
@@ -251,10 +299,9 @@ async def register_fan(var, config):
|
||||
await setup_fan_core_(var, config)
|
||||
|
||||
|
||||
async def create_fan_state(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
async def new_fan(config, *args):
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_fan(var, config)
|
||||
await cg.register_component(var, config)
|
||||
return var
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from esphome.const import (
|
||||
CONF_CLOSE_ACTION,
|
||||
CONF_CLOSE_DURATION,
|
||||
CONF_CLOSE_ENDSTOP,
|
||||
CONF_ID,
|
||||
CONF_MAX_DURATION,
|
||||
CONF_OPEN_ACTION,
|
||||
CONF_OPEN_DURATION,
|
||||
@@ -50,36 +49,43 @@ def validate_infer_endstop(config):
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_FEEDBACK_COVER_BASE_SCHEMA = cover.COVER_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(FeedbackCover),
|
||||
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional(CONF_OPEN_SENSOR): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional(CONF_OPEN_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional(CONF_CLOSE_SENSOR): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional(CONF_CLOSE_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ASSUMED_STATE): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_UPDATE_INTERVAL, "1000ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_INFER_ENDSTOP_FROM_MOVEMENT, False): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_DIRECTION_CHANGE_WAIT_TIME
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_ACCELERATION_WAIT_TIME, "0s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage,
|
||||
},
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
CONFIG_FEEDBACK_COVER_BASE_SCHEMA = (
|
||||
cover.cover_schema(FeedbackCover)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional(CONF_OPEN_SENSOR): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional(CONF_OPEN_OBSTACLE_SENSOR): cv.use_id(
|
||||
binary_sensor.BinarySensor
|
||||
),
|
||||
cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional(CONF_CLOSE_SENSOR): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional(CONF_CLOSE_OBSTACLE_SENSOR): cv.use_id(
|
||||
binary_sensor.BinarySensor
|
||||
),
|
||||
cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ASSUMED_STATE): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_UPDATE_INTERVAL, "1000ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_INFER_ENDSTOP_FROM_MOVEMENT, False): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_DIRECTION_CHANGE_WAIT_TIME
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_ACCELERATION_WAIT_TIME, "0s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage,
|
||||
},
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
@@ -90,9 +96,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
var = await cover.new_cover(config)
|
||||
await cg.register_component(var, config)
|
||||
await cover.register_cover(var, config)
|
||||
|
||||
# STOP
|
||||
await automation.build_automation(
|
||||
|
||||
@@ -10,8 +10,10 @@ static const char *const TAG = "gpio.one_wire";
|
||||
void GPIOOneWireBus::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up 1-wire bus...");
|
||||
this->t_pin_->setup();
|
||||
// clear bus with 480µs high, otherwise initial reset in search might fail
|
||||
this->t_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
|
||||
// clear bus with 480µs high, otherwise initial reset in search might fail
|
||||
this->pin_.digital_write(true);
|
||||
this->pin_.pin_mode(gpio::FLAG_OUTPUT);
|
||||
delayMicroseconds(480);
|
||||
this->search();
|
||||
}
|
||||
@@ -22,40 +24,49 @@ void GPIOOneWireBus::dump_config() {
|
||||
this->dump_devices_(TAG);
|
||||
}
|
||||
|
||||
bool HOT IRAM_ATTR GPIOOneWireBus::reset() {
|
||||
int HOT IRAM_ATTR GPIOOneWireBus::reset_int() {
|
||||
InterruptLock lock;
|
||||
// See reset here:
|
||||
// https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html
|
||||
// Wait for communication to clear (delay G)
|
||||
pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
|
||||
this->pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
|
||||
uint8_t retries = 125;
|
||||
do {
|
||||
if (--retries == 0)
|
||||
return false;
|
||||
return -1;
|
||||
delayMicroseconds(2);
|
||||
} while (!pin_.digital_read());
|
||||
} while (!this->pin_.digital_read());
|
||||
|
||||
bool r;
|
||||
bool r = false;
|
||||
|
||||
// Send 480µs LOW TX reset pulse (drive bus low, delay H)
|
||||
pin_.pin_mode(gpio::FLAG_OUTPUT);
|
||||
pin_.digital_write(false);
|
||||
this->pin_.digital_write(false);
|
||||
this->pin_.pin_mode(gpio::FLAG_OUTPUT);
|
||||
delayMicroseconds(480);
|
||||
|
||||
// Release the bus, delay I
|
||||
pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
|
||||
delayMicroseconds(70);
|
||||
this->pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
|
||||
uint32_t start = micros();
|
||||
delayMicroseconds(30);
|
||||
|
||||
while (micros() - start < 300) {
|
||||
// sample bus, 0=device(s) present, 1=no device present
|
||||
r = !this->pin_.digital_read();
|
||||
if (r)
|
||||
break;
|
||||
delayMicroseconds(1);
|
||||
}
|
||||
|
||||
// sample bus, 0=device(s) present, 1=no device present
|
||||
r = !pin_.digital_read();
|
||||
// delay J
|
||||
delayMicroseconds(410);
|
||||
return r;
|
||||
delayMicroseconds(start + 480 - micros());
|
||||
this->pin_.digital_write(true);
|
||||
this->pin_.pin_mode(gpio::FLAG_OUTPUT);
|
||||
return r ? 1 : 0;
|
||||
}
|
||||
|
||||
void HOT IRAM_ATTR GPIOOneWireBus::write_bit_(bool bit) {
|
||||
// drive bus low
|
||||
pin_.pin_mode(gpio::FLAG_OUTPUT);
|
||||
pin_.digital_write(false);
|
||||
this->pin_.digital_write(false);
|
||||
|
||||
// from datasheet:
|
||||
// write 0 low time: t_low0: min=60µs, max=120µs
|
||||
@@ -64,72 +75,62 @@ void HOT IRAM_ATTR GPIOOneWireBus::write_bit_(bool bit) {
|
||||
// recovery time: t_rec: min=1µs
|
||||
// ds18b20 appears to read the bus after roughly 14µs
|
||||
uint32_t delay0 = bit ? 6 : 60;
|
||||
uint32_t delay1 = bit ? 59 : 5;
|
||||
uint32_t delay1 = bit ? 64 : 10;
|
||||
|
||||
// delay A/C
|
||||
delayMicroseconds(delay0);
|
||||
// release bus
|
||||
pin_.digital_write(true);
|
||||
this->pin_.digital_write(true);
|
||||
// delay B/D
|
||||
delayMicroseconds(delay1);
|
||||
}
|
||||
|
||||
bool HOT IRAM_ATTR GPIOOneWireBus::read_bit_() {
|
||||
// drive bus low
|
||||
pin_.pin_mode(gpio::FLAG_OUTPUT);
|
||||
pin_.digital_write(false);
|
||||
this->pin_.digital_write(false);
|
||||
|
||||
// note: for reading we'll need very accurate timing, as the
|
||||
// timing for the digital_read() is tight; according to the datasheet,
|
||||
// we should read at the end of 16µs starting from the bus low
|
||||
// typically, the ds18b20 pulls the line high after 11µs for a logical 1
|
||||
// and 29µs for a logical 0
|
||||
|
||||
uint32_t start = micros();
|
||||
// datasheet says >1µs
|
||||
delayMicroseconds(2);
|
||||
// datasheet says >= 1µs
|
||||
delayMicroseconds(5);
|
||||
|
||||
// release bus, delay E
|
||||
pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
|
||||
|
||||
// measure from start value directly, to get best accurate timing no matter
|
||||
// how long pin_mode/delayMicroseconds took
|
||||
uint32_t now = micros();
|
||||
if (now - start < 12)
|
||||
delayMicroseconds(12 - (now - start));
|
||||
this->pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
|
||||
|
||||
delayMicroseconds(8);
|
||||
// sample bus to read bit from peer
|
||||
bool r = pin_.digital_read();
|
||||
bool r = this->pin_.digital_read();
|
||||
|
||||
// read slot is at least 60µs; get as close to 60µs to spend less time with interrupts locked
|
||||
now = micros();
|
||||
if (now - start < 60)
|
||||
delayMicroseconds(60 - (now - start));
|
||||
// read slot is at least 60µs
|
||||
delayMicroseconds(50);
|
||||
|
||||
this->pin_.digital_write(true);
|
||||
this->pin_.pin_mode(gpio::FLAG_OUTPUT);
|
||||
return r;
|
||||
}
|
||||
|
||||
void IRAM_ATTR GPIOOneWireBus::write8(uint8_t val) {
|
||||
InterruptLock lock;
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
this->write_bit_(bool((1u << i) & val));
|
||||
}
|
||||
}
|
||||
|
||||
void IRAM_ATTR GPIOOneWireBus::write64(uint64_t val) {
|
||||
InterruptLock lock;
|
||||
for (uint8_t i = 0; i < 64; i++) {
|
||||
this->write_bit_(bool((1ULL << i) & val));
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t IRAM_ATTR GPIOOneWireBus::read8() {
|
||||
InterruptLock lock;
|
||||
uint8_t ret = 0;
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
for (uint8_t i = 0; i < 8; i++)
|
||||
ret |= (uint8_t(this->read_bit_()) << i);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint64_t IRAM_ATTR GPIOOneWireBus::read64() {
|
||||
InterruptLock lock;
|
||||
uint64_t ret = 0;
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
ret |= (uint64_t(this->read_bit_()) << i);
|
||||
@@ -144,6 +145,7 @@ void GPIOOneWireBus::reset_search() {
|
||||
}
|
||||
|
||||
uint64_t IRAM_ATTR GPIOOneWireBus::search_int() {
|
||||
InterruptLock lock;
|
||||
if (this->last_device_flag_)
|
||||
return 0u;
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ class GPIOOneWireBus : public one_wire::OneWireBus, public Component {
|
||||
this->pin_ = pin->to_isr();
|
||||
}
|
||||
|
||||
bool reset() override;
|
||||
void write8(uint8_t val) override;
|
||||
void write64(uint64_t val) override;
|
||||
uint8_t read8() override;
|
||||
@@ -31,10 +30,12 @@ class GPIOOneWireBus : public one_wire::OneWireBus, public Component {
|
||||
bool last_device_flag_{false};
|
||||
uint64_t address_;
|
||||
|
||||
int reset_int() override;
|
||||
void reset_search() override;
|
||||
uint64_t search_int() override;
|
||||
void write_bit_(bool bit);
|
||||
bool read_bit_();
|
||||
bool read_bit_(uint32_t *t);
|
||||
};
|
||||
|
||||
} // namespace gpio
|
||||
|
||||
@@ -25,6 +25,7 @@ GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice)
|
||||
GPSListener = gps_ns.class_("GPSListener")
|
||||
|
||||
CONF_GPS_ID = "gps_id"
|
||||
CONF_HDOP = "hdop"
|
||||
MULTI_CONF = True
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
@@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_SPEED): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOMETER_PER_HOUR,
|
||||
accuracy_decimals=6,
|
||||
accuracy_decimals=3,
|
||||
),
|
||||
cv.Optional(CONF_COURSE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_DEGREES,
|
||||
@@ -48,12 +49,16 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_ALTITUDE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=1,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
cv.Optional(CONF_SATELLITES): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HDOP): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("20s"))
|
||||
@@ -92,5 +97,9 @@ async def to_code(config):
|
||||
sens = await sensor.new_sensor(config[CONF_SATELLITES])
|
||||
cg.add(var.set_satellites_sensor(sens))
|
||||
|
||||
if hdop_config := config.get(CONF_HDOP):
|
||||
sens = await sensor.new_sensor(hdop_config)
|
||||
cg.add(var.set_hdop_sensor(sens))
|
||||
|
||||
# https://platformio.org/lib/show/1655/TinyGPSPlus
|
||||
cg.add_library("mikalhart/TinyGPSPlus", "1.0.2")
|
||||
|
||||
@@ -28,6 +28,9 @@ void GPS::update() {
|
||||
|
||||
if (this->satellites_sensor_ != nullptr)
|
||||
this->satellites_sensor_->publish_state(this->satellites_);
|
||||
|
||||
if (this->hdop_sensor_ != nullptr)
|
||||
this->hdop_sensor_->publish_state(this->hdop_);
|
||||
}
|
||||
|
||||
void GPS::loop() {
|
||||
@@ -44,23 +47,23 @@ void GPS::loop() {
|
||||
|
||||
if (tiny_gps_.speed.isUpdated()) {
|
||||
this->speed_ = tiny_gps_.speed.kmph();
|
||||
ESP_LOGD(TAG, "Speed:");
|
||||
ESP_LOGD(TAG, " %f km/h", this->speed_);
|
||||
ESP_LOGD(TAG, "Speed: %.3f km/h", this->speed_);
|
||||
}
|
||||
if (tiny_gps_.course.isUpdated()) {
|
||||
this->course_ = tiny_gps_.course.deg();
|
||||
ESP_LOGD(TAG, "Course:");
|
||||
ESP_LOGD(TAG, " %f °", this->course_);
|
||||
ESP_LOGD(TAG, "Course: %.2f °", this->course_);
|
||||
}
|
||||
if (tiny_gps_.altitude.isUpdated()) {
|
||||
this->altitude_ = tiny_gps_.altitude.meters();
|
||||
ESP_LOGD(TAG, "Altitude:");
|
||||
ESP_LOGD(TAG, " %f m", this->altitude_);
|
||||
ESP_LOGD(TAG, "Altitude: %.2f m", this->altitude_);
|
||||
}
|
||||
if (tiny_gps_.satellites.isUpdated()) {
|
||||
this->satellites_ = tiny_gps_.satellites.value();
|
||||
ESP_LOGD(TAG, "Satellites:");
|
||||
ESP_LOGD(TAG, " %d", this->satellites_);
|
||||
ESP_LOGD(TAG, "Satellites: %d", this->satellites_);
|
||||
}
|
||||
if (tiny_gps_.hdop.isUpdated()) {
|
||||
this->hdop_ = tiny_gps_.hdop.hdop();
|
||||
ESP_LOGD(TAG, "HDOP: %.3f", this->hdop_);
|
||||
}
|
||||
|
||||
for (auto *listener : this->listeners_)
|
||||
|
||||
@@ -33,6 +33,7 @@ class GPS : public PollingComponent, public uart::UARTDevice {
|
||||
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 set_hdop_sensor(sensor::Sensor *hdop_sensor) { hdop_sensor_ = hdop_sensor; }
|
||||
|
||||
void register_listener(GPSListener *listener) {
|
||||
listener->parent_ = this;
|
||||
@@ -46,12 +47,13 @@ class GPS : public PollingComponent, public uart::UARTDevice {
|
||||
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;
|
||||
float latitude_ = NAN;
|
||||
float longitude_ = NAN;
|
||||
float speed_ = NAN;
|
||||
float course_ = NAN;
|
||||
float altitude_ = NAN;
|
||||
int satellites_ = 0;
|
||||
double hdop_ = NAN;
|
||||
|
||||
sensor::Sensor *latitude_sensor_{nullptr};
|
||||
sensor::Sensor *longitude_sensor_{nullptr};
|
||||
@@ -59,6 +61,7 @@ class GPS : public PollingComponent, public uart::UARTDevice {
|
||||
sensor::Sensor *course_sensor_{nullptr};
|
||||
sensor::Sensor *altitude_sensor_{nullptr};
|
||||
sensor::Sensor *satellites_sensor_{nullptr};
|
||||
sensor::Sensor *hdop_sensor_{nullptr};
|
||||
|
||||
bool has_time_{false};
|
||||
TinyGPSPlus tiny_gps_;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import cover, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CLOSE_DURATION, CONF_ID, CONF_OPEN_DURATION
|
||||
from esphome.const import CONF_CLOSE_DURATION, CONF_OPEN_DURATION
|
||||
|
||||
he60r_ns = cg.esphome_ns.namespace("he60r")
|
||||
HE60rCover = he60r_ns.class_("HE60rCover", cover.Cover, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cover.COVER_SCHEMA.extend(uart.UART_DEVICE_SCHEMA)
|
||||
cover.cover_schema(HE60rCover)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HE60rCover),
|
||||
cv.Optional(
|
||||
CONF_OPEN_DURATION, default="15s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
@@ -34,9 +34,8 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
var = await cover.new_cover(config)
|
||||
await cg.register_component(var, config)
|
||||
await cover.register_cover(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION]))
|
||||
|
||||
@@ -16,14 +16,17 @@ HttpRequestUpdate = http_request_ns.class_(
|
||||
|
||||
CONF_OTA_ID = "ota_id"
|
||||
|
||||
CONFIG_SCHEMA = update.UPDATE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HttpRequestUpdate),
|
||||
cv.GenerateID(CONF_OTA_ID): cv.use_id(OtaHttpRequestComponent),
|
||||
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
||||
cv.Required(CONF_SOURCE): cv.url,
|
||||
}
|
||||
).extend(cv.polling_component_schema("6h"))
|
||||
CONFIG_SCHEMA = (
|
||||
update.update_schema(HttpRequestUpdate)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_OTA_ID): cv.use_id(OtaHttpRequestComponent),
|
||||
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
||||
cv.Required(CONF_SOURCE): cv.url,
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("6h"))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -30,6 +30,7 @@ DEPENDENCIES = ["i2s_audio"]
|
||||
|
||||
CONF_ADC_PIN = "adc_pin"
|
||||
CONF_ADC_TYPE = "adc_type"
|
||||
CONF_CORRECT_DC_OFFSET = "correct_dc_offset"
|
||||
CONF_PDM = "pdm"
|
||||
|
||||
I2SAudioMicrophone = i2s_audio_ns.class_(
|
||||
@@ -88,10 +89,13 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
|
||||
default_sample_rate=16000,
|
||||
default_channel=CONF_RIGHT,
|
||||
default_bits_per_sample="32bit",
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_CORRECT_DC_OFFSET, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.typed_schema(
|
||||
{
|
||||
@@ -140,3 +144,5 @@ async def to_code(config):
|
||||
else:
|
||||
cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN]))
|
||||
cg.add(var.set_pdm(config[CONF_PDM]))
|
||||
|
||||
cg.add(var.set_correct_dc_offset(config[CONF_CORRECT_DC_OFFSET]))
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "esphome/components/audio/audio.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace i2s_audio {
|
||||
|
||||
@@ -22,6 +24,9 @@ static const uint32_t READ_DURATION_MS = 16;
|
||||
static const size_t TASK_STACK_SIZE = 4096;
|
||||
static const ssize_t TASK_PRIORITY = 23;
|
||||
|
||||
// Use an exponential moving average to correct a DC offset with weight factor 1/1000
|
||||
static const int32_t DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR = 1000;
|
||||
|
||||
static const char *const TAG = "i2s_audio.microphone";
|
||||
|
||||
enum MicrophoneEventGroupBits : uint32_t {
|
||||
@@ -70,21 +75,11 @@ void I2SAudioMicrophone::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->configure_stream_settings_();
|
||||
}
|
||||
|
||||
void I2SAudioMicrophone::start() {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
|
||||
xSemaphoreTake(this->active_listeners_semaphore_, 0);
|
||||
}
|
||||
|
||||
bool I2SAudioMicrophone::start_driver_() {
|
||||
if (!this->parent_->try_lock()) {
|
||||
return false; // Waiting for another i2s to return lock
|
||||
}
|
||||
esp_err_t err;
|
||||
|
||||
void I2SAudioMicrophone::configure_stream_settings_() {
|
||||
uint8_t channel_count = 1;
|
||||
#ifdef USE_I2S_LEGACY
|
||||
uint8_t bits_per_sample = this->bits_per_sample_;
|
||||
@@ -93,10 +88,10 @@ bool I2SAudioMicrophone::start_driver_() {
|
||||
channel_count = 2;
|
||||
}
|
||||
#else
|
||||
if (this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_AUTO) {
|
||||
this->slot_bit_width_ = I2S_SLOT_BIT_WIDTH_16BIT;
|
||||
uint8_t bits_per_sample = 16;
|
||||
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
|
||||
bits_per_sample = this->slot_bit_width_;
|
||||
}
|
||||
uint8_t bits_per_sample = this->slot_bit_width_;
|
||||
|
||||
if (this->slot_mode_ == I2S_SLOT_MODE_STEREO) {
|
||||
channel_count = 2;
|
||||
@@ -114,6 +109,26 @@ bool I2SAudioMicrophone::start_driver_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
if (this->pdm_) {
|
||||
bits_per_sample = 16; // PDM mics are always 16 bits per sample
|
||||
}
|
||||
|
||||
this->audio_stream_info_ = audio::AudioStreamInfo(bits_per_sample, channel_count, this->sample_rate_);
|
||||
}
|
||||
|
||||
void I2SAudioMicrophone::start() {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
|
||||
xSemaphoreTake(this->active_listeners_semaphore_, 0);
|
||||
}
|
||||
|
||||
bool I2SAudioMicrophone::start_driver_() {
|
||||
if (!this->parent_->try_lock()) {
|
||||
return false; // Waiting for another i2s to return lock
|
||||
}
|
||||
esp_err_t err;
|
||||
|
||||
#ifdef USE_I2S_LEGACY
|
||||
i2s_driver_config_t config = {
|
||||
.mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_RX),
|
||||
@@ -202,8 +217,6 @@ bool I2SAudioMicrophone::start_driver_() {
|
||||
i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config();
|
||||
#if SOC_I2S_SUPPORTS_PDM_RX
|
||||
if (this->pdm_) {
|
||||
bits_per_sample = 16; // PDM mics are always 16 bits per sample with the IDF 5 driver
|
||||
|
||||
i2s_pdm_rx_clk_config_t clk_cfg = {
|
||||
.sample_rate_hz = this->sample_rate_,
|
||||
.clk_src = clk_src,
|
||||
@@ -277,10 +290,8 @@ bool I2SAudioMicrophone::start_driver_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
this->audio_stream_info_ = audio::AudioStreamInfo(bits_per_sample, channel_count, this->sample_rate_);
|
||||
|
||||
this->status_clear_error();
|
||||
|
||||
this->configure_stream_settings_(); // redetermine the settings in case some settings were changed after compilation
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -361,9 +372,12 @@ void I2SAudioMicrophone::mic_task(void *params) {
|
||||
samples.resize(bytes_to_read);
|
||||
size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * pdMS_TO_TICKS(READ_DURATION_MS));
|
||||
samples.resize(bytes_read);
|
||||
if (this_microphone->correct_dc_offset_) {
|
||||
this_microphone->fix_dc_offset_(samples);
|
||||
}
|
||||
this_microphone->data_callbacks_.call(samples);
|
||||
} else {
|
||||
delay(READ_DURATION_MS);
|
||||
vTaskDelay(pdMS_TO_TICKS(READ_DURATION_MS));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,11 +387,34 @@ void I2SAudioMicrophone::mic_task(void *params) {
|
||||
|
||||
xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STOPPED);
|
||||
while (true) {
|
||||
// Continuously delay until the loop method delete the task
|
||||
delay(10);
|
||||
// Continuously delay until the loop method deletes the task
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
|
||||
void I2SAudioMicrophone::fix_dc_offset_(std::vector<uint8_t> &data) {
|
||||
const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1);
|
||||
const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size());
|
||||
|
||||
if (total_samples == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t offset_accumulator = 0;
|
||||
for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) {
|
||||
const uint32_t byte_index = sample_index * bytes_per_sample;
|
||||
int32_t sample = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample);
|
||||
offset_accumulator += sample;
|
||||
sample -= this->dc_offset_;
|
||||
audio::pack_q31_as_audio_sample(sample, &data[byte_index], bytes_per_sample);
|
||||
}
|
||||
|
||||
const int32_t new_offset = offset_accumulator / total_samples;
|
||||
this->dc_offset_ = new_offset / DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR +
|
||||
(DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR - 1) * this->dc_offset_ /
|
||||
DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR;
|
||||
}
|
||||
|
||||
size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) {
|
||||
size_t bytes_read = 0;
|
||||
#ifdef USE_I2S_LEGACY
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
#include "esphome/components/microphone/microphone.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/event_groups.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace i2s_audio {
|
||||
@@ -20,6 +22,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
||||
void stop() override;
|
||||
|
||||
void loop() override;
|
||||
|
||||
void set_correct_dc_offset(bool correct_dc_offset) { this->correct_dc_offset_ = correct_dc_offset; }
|
||||
|
||||
#ifdef USE_I2S_LEGACY
|
||||
void set_din_pin(int8_t pin) { this->din_pin_ = pin; }
|
||||
#else
|
||||
@@ -41,8 +46,16 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
||||
bool start_driver_();
|
||||
void stop_driver_();
|
||||
|
||||
/// @brief Attempts to correct a microphone DC offset; e.g., a microphones silent level is offset from 0. Applies a
|
||||
/// correction offset that is updated using an exponential moving average for all samples away from 0.
|
||||
/// @param data
|
||||
void fix_dc_offset_(std::vector<uint8_t> &data);
|
||||
|
||||
size_t read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait);
|
||||
|
||||
/// @brief Sets the Microphone ``audio_stream_info_`` member variable to the configured I2S settings.
|
||||
void configure_stream_settings_();
|
||||
|
||||
static void mic_task(void *params);
|
||||
|
||||
SemaphoreHandle_t active_listeners_semaphore_{nullptr};
|
||||
@@ -61,6 +74,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
||||
i2s_chan_handle_t rx_handle_;
|
||||
#endif
|
||||
bool pdm_{false};
|
||||
|
||||
bool correct_dc_offset_;
|
||||
int32_t dc_offset_{0};
|
||||
};
|
||||
|
||||
} // namespace i2s_audio
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "esp_timer.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace i2s_audio {
|
||||
|
||||
@@ -366,25 +368,15 @@ void I2SAudioSpeaker::speaker_task(void *params) {
|
||||
bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
|
||||
#endif
|
||||
|
||||
uint32_t write_timestamp = micros();
|
||||
int64_t now = esp_timer_get_time();
|
||||
|
||||
if (bytes_written != bytes_to_write) {
|
||||
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE);
|
||||
}
|
||||
|
||||
bytes_read -= bytes_written;
|
||||
|
||||
this_speaker->accumulated_frames_written_ += audio_stream_info.bytes_to_frames(bytes_written);
|
||||
const uint32_t new_playback_ms =
|
||||
audio_stream_info.frames_to_milliseconds_with_remainder(&this_speaker->accumulated_frames_written_);
|
||||
const uint32_t remainder_us =
|
||||
audio_stream_info.frames_to_microseconds(this_speaker->accumulated_frames_written_);
|
||||
|
||||
uint32_t pending_frames =
|
||||
audio_stream_info.bytes_to_frames(bytes_read + this_speaker->audio_ring_buffer_->available());
|
||||
const uint32_t pending_ms = audio_stream_info.frames_to_milliseconds_with_remainder(&pending_frames);
|
||||
|
||||
this_speaker->audio_output_callback_(new_playback_ms, remainder_us, pending_ms, write_timestamp);
|
||||
this_speaker->audio_output_callback_(audio_stream_info.bytes_to_frames(bytes_written),
|
||||
now + dma_buffers_duration_ms * 1000);
|
||||
|
||||
tx_dma_underflow = false;
|
||||
last_data_received_time = millis();
|
||||
@@ -637,7 +629,16 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
|
||||
std_slot_cfg =
|
||||
I2S_STD_MSB_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode);
|
||||
}
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
// There seems to be a bug on the ESP32 (non-variant) platform where setting the slot bit width higher then the bits
|
||||
// per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems to
|
||||
// make it play at the correct speed while sending more bits per slot.
|
||||
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
|
||||
std_slot_cfg.ws_width = static_cast<uint32_t>(this->slot_bit_width_);
|
||||
}
|
||||
#else
|
||||
std_slot_cfg.slot_bit_width = this->slot_bit_width_;
|
||||
#endif
|
||||
std_slot_cfg.slot_mask = slot_mask;
|
||||
|
||||
pin_config.dout = this->dout_pin_;
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import key_provider
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ENABLE_ON_BOOT,
|
||||
CONF_ID,
|
||||
CONF_MAX_LENGTH,
|
||||
CONF_MIN_LENGTH,
|
||||
@@ -28,6 +29,8 @@ CONF_ON_RESULT = "on_result"
|
||||
|
||||
key_collector_ns = cg.esphome_ns.namespace("key_collector")
|
||||
KeyCollector = key_collector_ns.class_("KeyCollector", cg.Component)
|
||||
EnableAction = key_collector_ns.class_("EnableAction", automation.Action)
|
||||
DisableAction = key_collector_ns.class_("DisableAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.COMPONENT_SCHEMA.extend(
|
||||
@@ -46,6 +49,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ON_RESULT): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_ON_TIMEOUT): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_TIMEOUT): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_END_KEYS, CONF_MAX_LENGTH),
|
||||
@@ -94,3 +98,34 @@ async def to_code(config):
|
||||
)
|
||||
if CONF_TIMEOUT in config:
|
||||
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
|
||||
cg.add(var.set_enabled(config[CONF_ENABLE_ON_BOOT]))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"key_collector.enable",
|
||||
EnableAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(KeyCollector),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def enable_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"key_collector.disable",
|
||||
DisableAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(KeyCollector),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def disable_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
@@ -45,6 +45,12 @@ void KeyCollector::set_provider(key_provider::KeyProvider *provider) {
|
||||
provider->add_on_key_callback([this](uint8_t key) { this->key_pressed_(key); });
|
||||
}
|
||||
|
||||
void KeyCollector::set_enabled(bool enabled) {
|
||||
this->enabled_ = enabled;
|
||||
if (!enabled)
|
||||
this->clear(false);
|
||||
}
|
||||
|
||||
void KeyCollector::clear(bool progress_update) {
|
||||
this->result_.clear();
|
||||
this->start_key_ = 0;
|
||||
@@ -55,6 +61,8 @@ void KeyCollector::clear(bool progress_update) {
|
||||
void KeyCollector::send_key(uint8_t key) { this->key_pressed_(key); }
|
||||
|
||||
void KeyCollector::key_pressed_(uint8_t key) {
|
||||
if (!this->enabled_)
|
||||
return;
|
||||
this->last_key_time_ = millis();
|
||||
if (!this->start_keys_.empty() && !this->start_key_) {
|
||||
if (this->start_keys_.find(key) != std::string::npos) {
|
||||
|
||||
@@ -25,6 +25,7 @@ class KeyCollector : public Component {
|
||||
Trigger<std::string, uint8_t, uint8_t> *get_result_trigger() const { return this->result_trigger_; };
|
||||
Trigger<std::string, uint8_t> *get_timeout_trigger() const { return this->timeout_trigger_; };
|
||||
void set_timeout(int timeout) { this->timeout_ = timeout; };
|
||||
void set_enabled(bool enabled);
|
||||
|
||||
void clear(bool progress_update = true);
|
||||
void send_key(uint8_t key);
|
||||
@@ -47,6 +48,15 @@ class KeyCollector : public Component {
|
||||
Trigger<std::string, uint8_t> *timeout_trigger_;
|
||||
uint32_t last_key_time_;
|
||||
uint32_t timeout_{0};
|
||||
bool enabled_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class EnableAction : public Action<Ts...>, public Parented<KeyCollector> {
|
||||
void play(Ts... x) override { this->parent_->set_enabled(true); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisableAction : public Action<Ts...>, public Parented<KeyCollector> {
|
||||
void play(Ts... x) override { this->parent_->set_enabled(false); }
|
||||
};
|
||||
|
||||
} // namespace key_collector
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import enum
|
||||
|
||||
import esphome.automation as auto
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, power_supply, web_server
|
||||
@@ -13,15 +15,18 @@ from esphome.const import (
|
||||
CONF_COLOR_TEMPERATURE,
|
||||
CONF_DEFAULT_TRANSITION_LENGTH,
|
||||
CONF_EFFECTS,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_FLASH_TRANSITION_LENGTH,
|
||||
CONF_GAMMA_CORRECT,
|
||||
CONF_GREEN,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_STATE,
|
||||
CONF_ON_TURN_OFF,
|
||||
CONF_ON_TURN_ON,
|
||||
CONF_OUTPUT_ID,
|
||||
CONF_POWER_SUPPLY,
|
||||
CONF_RED,
|
||||
CONF_RESTORE_MODE,
|
||||
@@ -33,6 +38,7 @@ from esphome.const import (
|
||||
CONF_WHITE,
|
||||
)
|
||||
from esphome.core import coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
|
||||
from .automation import LIGHT_STATE_SCHEMA
|
||||
@@ -141,6 +147,51 @@ ADDRESSABLE_LIGHT_SCHEMA = RGB_LIGHT_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
class LightType(enum.IntEnum):
|
||||
"""Light type enum."""
|
||||
|
||||
BINARY = 0
|
||||
BRIGHTNESS_ONLY = 1
|
||||
RGB = 2
|
||||
ADDRESSABLE = 3
|
||||
|
||||
|
||||
def light_schema(
|
||||
class_: MockObjClass,
|
||||
type_: LightType,
|
||||
*,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
icon: str = cv.UNDEFINED,
|
||||
default_restore_mode: str = cv.UNDEFINED,
|
||||
) -> cv.Schema:
|
||||
schema = {
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(class_),
|
||||
}
|
||||
|
||||
for key, default, validator in [
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
(CONF_ICON, icon, cv.icon),
|
||||
(
|
||||
CONF_RESTORE_MODE,
|
||||
default_restore_mode,
|
||||
cv.enum(RESTORE_MODES, upper=True, space="_"),
|
||||
),
|
||||
]:
|
||||
if default is not cv.UNDEFINED:
|
||||
schema[cv.Optional(key, default=default)] = validator
|
||||
|
||||
if type_ == LightType.BINARY:
|
||||
return BINARY_LIGHT_SCHEMA.extend(schema)
|
||||
if type_ == LightType.BRIGHTNESS_ONLY:
|
||||
return BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend(schema)
|
||||
if type_ == LightType.RGB:
|
||||
return RGB_LIGHT_SCHEMA.extend(schema)
|
||||
if type_ == LightType.ADDRESSABLE:
|
||||
return ADDRESSABLE_LIGHT_SCHEMA.extend(schema)
|
||||
|
||||
raise ValueError(f"Invalid light type: {type_}")
|
||||
|
||||
|
||||
def validate_color_temperature_channels(value):
|
||||
if (
|
||||
CONF_COLD_WHITE_COLOR_TEMPERATURE in value
|
||||
@@ -223,6 +274,12 @@ async def register_light(output_var, config):
|
||||
await setup_light_core_(light_var, output_var, config)
|
||||
|
||||
|
||||
async def new_light(config, *args):
|
||||
output_var = cg.new_Pvariable(config[CONF_OUTPUT_ID], *args)
|
||||
await register_light(output_var, config)
|
||||
return output_var
|
||||
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_LIGHT")
|
||||
|
||||
@@ -4,6 +4,8 @@ import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_LOCK,
|
||||
@@ -12,6 +14,7 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -31,7 +34,19 @@ LockCondition = lock_ns.class_("LockCondition", Condition)
|
||||
LockLockTrigger = lock_ns.class_("LockLockTrigger", automation.Trigger.template())
|
||||
LockUnlockTrigger = lock_ns.class_("LockUnlockTrigger", automation.Trigger.template())
|
||||
|
||||
LOCK_SCHEMA = (
|
||||
LockState = lock_ns.enum("LockState")
|
||||
|
||||
LOCK_STATES = {
|
||||
"LOCKED": LockState.LOCK_STATE_LOCKED,
|
||||
"UNLOCKED": LockState.LOCK_STATE_UNLOCKED,
|
||||
"JAMMED": LockState.LOCK_STATE_JAMMED,
|
||||
"LOCKING": LockState.LOCK_STATE_LOCKING,
|
||||
"UNLOCKING": LockState.LOCK_STATE_UNLOCKING,
|
||||
}
|
||||
|
||||
validate_lock_state = cv.enum(LOCK_STATES, upper=True)
|
||||
|
||||
_LOCK_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
@@ -52,7 +67,33 @@ LOCK_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def setup_lock_core_(var, config):
|
||||
def lock_schema(
|
||||
class_: MockObjClass = cv.UNDEFINED,
|
||||
*,
|
||||
icon: str = cv.UNDEFINED,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
) -> cv.Schema:
|
||||
schema = {}
|
||||
|
||||
if class_ is not cv.UNDEFINED:
|
||||
schema[cv.GenerateID()] = cv.declare_id(class_)
|
||||
|
||||
for key, default, validator in [
|
||||
(CONF_ICON, icon, cv.icon),
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
]:
|
||||
if default is not cv.UNDEFINED:
|
||||
schema[cv.Optional(key, default=default)] = validator
|
||||
|
||||
return _LOCK_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
LOCK_SCHEMA = lock_schema()
|
||||
LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock"))
|
||||
|
||||
|
||||
async def _setup_lock_core(var, config):
|
||||
await setup_entity(var, config)
|
||||
|
||||
for conf in config.get(CONF_ON_LOCK, []):
|
||||
@@ -74,12 +115,18 @@ async def register_lock(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_lock(var))
|
||||
await setup_lock_core_(var, config)
|
||||
await _setup_lock_core(var, config)
|
||||
|
||||
|
||||
async def new_lock(config, *args):
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_lock(var, config)
|
||||
return var
|
||||
|
||||
|
||||
LOCK_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Lock),
|
||||
cv.GenerateID(CONF_ID): cv.use_id(Lock),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/lock/lock.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace lock {
|
||||
@@ -72,16 +72,5 @@ class LockUnlockTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class LockPublishAction : public Action<Ts...> {
|
||||
public:
|
||||
LockPublishAction(Lock *a_lock) : lock_(a_lock) {}
|
||||
TEMPLATABLE_VALUE(LockState, state)
|
||||
|
||||
void play(Ts... x) override { this->lock_->publish_state(this->state_.value(x...)); }
|
||||
|
||||
protected:
|
||||
Lock *lock_;
|
||||
};
|
||||
|
||||
} // namespace lock
|
||||
} // namespace esphome
|
||||
|
||||
@@ -79,6 +79,7 @@ DEFAULT = "DEFAULT"
|
||||
|
||||
CONF_INITIAL_LEVEL = "initial_level"
|
||||
CONF_LOGGER_ID = "logger_id"
|
||||
CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size"
|
||||
|
||||
UART_SELECTION_ESP32 = {
|
||||
VARIANT_ESP32: [UART0, UART1, UART2],
|
||||
@@ -180,6 +181,20 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int,
|
||||
cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes,
|
||||
cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean,
|
||||
cv.SplitDefault(
|
||||
CONF_TASK_LOG_BUFFER_SIZE,
|
||||
esp32=768, # Default: 768 bytes (~5-6 messages with 70-byte text plus thread names)
|
||||
): cv.All(
|
||||
cv.only_on_esp32,
|
||||
cv.validate_bytes,
|
||||
cv.Any(
|
||||
cv.int_(0), # Disabled
|
||||
cv.int_range(
|
||||
min=640, # Min: ~4-5 messages with 70-byte text plus thread names
|
||||
max=32768, # Max: Depends on message sizes, typically ~300 messages with default size
|
||||
),
|
||||
),
|
||||
),
|
||||
cv.SplitDefault(
|
||||
CONF_HARDWARE_UART,
|
||||
esp8266=UART0,
|
||||
@@ -238,6 +253,12 @@ async def to_code(config):
|
||||
baud_rate,
|
||||
config[CONF_TX_BUFFER_SIZE],
|
||||
)
|
||||
if CORE.is_esp32:
|
||||
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
|
||||
if task_log_buffer_size > 0:
|
||||
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
||||
cg.add(log.init_log_buffer(task_log_buffer_size))
|
||||
|
||||
cg.add(log.set_log_level(initial_level))
|
||||
if CONF_HARDWARE_UART in config:
|
||||
cg.add(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "logger.h"
|
||||
#include <cinttypes>
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#include <memory> // For unique_ptr
|
||||
#endif
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -10,127 +13,121 @@ namespace logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
static const char *const LOG_LEVEL_COLORS[] = {
|
||||
"", // NONE
|
||||
ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), // ERROR
|
||||
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW), // WARNING
|
||||
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN), // INFO
|
||||
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA), // CONFIG
|
||||
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN), // DEBUG
|
||||
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY), // VERBOSE
|
||||
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE), // VERY_VERBOSE
|
||||
};
|
||||
static const char *const LOG_LEVEL_LETTERS[] = {
|
||||
"", // NONE
|
||||
"E", // ERROR
|
||||
"W", // WARNING
|
||||
"I", // INFO
|
||||
"C", // CONFIG
|
||||
"D", // DEBUG
|
||||
"V", // VERBOSE
|
||||
"VV", // VERY_VERBOSE
|
||||
};
|
||||
#ifdef USE_ESP32
|
||||
// Implementation for ESP32 (multi-core with atomic support)
|
||||
// Main thread: synchronous logging with direct buffer access
|
||||
// Other threads: console output with stack buffer, callbacks via async buffer
|
||||
void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
||||
if (level > this->level_for(tag) || recursion_guard_.load(std::memory_order_relaxed))
|
||||
return;
|
||||
recursion_guard_.store(true, std::memory_order_relaxed);
|
||||
|
||||
void Logger::write_header_(int level, const char *tag, int line) {
|
||||
if (level < 0)
|
||||
level = 0;
|
||||
if (level > 7)
|
||||
level = 7;
|
||||
|
||||
const char *color = LOG_LEVEL_COLORS[level];
|
||||
const char *letter = LOG_LEVEL_LETTERS[level];
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
||||
#else
|
||||
void *current_task = nullptr;
|
||||
#endif
|
||||
if (current_task == main_task_) {
|
||||
this->printf_to_buffer_("%s[%s][%s:%03u]: ", color, letter, tag, line);
|
||||
} else {
|
||||
const char *thread_name = ""; // NOLINT(clang-analyzer-deadcode.DeadStores)
|
||||
#if defined(USE_ESP32)
|
||||
thread_name = pcTaskGetName(current_task);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
thread_name = pcTaskGetTaskName(current_task);
|
||||
#endif
|
||||
this->printf_to_buffer_("%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line,
|
||||
ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), thread_name, color);
|
||||
}
|
||||
}
|
||||
|
||||
// For main task: call log_message_to_buffer_and_send_ which does console and callback logging
|
||||
if (current_task == main_task_) {
|
||||
this->log_message_to_buffer_and_send_(level, tag, line, format, args);
|
||||
recursion_guard_.store(false, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
// For non-main tasks: use stack-allocated buffer only for console output
|
||||
if (this->baud_rate_ > 0) { // If logging is enabled, write to console
|
||||
// Maximum size for console log messages (includes null terminator)
|
||||
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
|
||||
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
|
||||
int buffer_at = 0; // Initialize buffer position
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at,
|
||||
MAX_CONSOLE_LOG_MSG_SIZE);
|
||||
this->write_msg_(console_buffer);
|
||||
}
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
// For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered
|
||||
if (this->log_callback_.size() > 0) {
|
||||
// This will be processed in the main loop
|
||||
this->log_buffer_->send_message_thread_safe(static_cast<uint8_t>(level), tag, static_cast<uint16_t>(line),
|
||||
current_task, format, args);
|
||||
}
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
recursion_guard_.store(false, std::memory_order_release);
|
||||
}
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifndef USE_ESP32
|
||||
// Implementation for platforms that do not support atomic operations
|
||||
// or have to consider logging in other tasks
|
||||
void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
||||
if (level > this->level_for(tag) || recursion_guard_)
|
||||
return;
|
||||
|
||||
recursion_guard_ = true;
|
||||
this->reset_buffer_();
|
||||
this->write_header_(level, tag, line);
|
||||
this->vprintf_to_buffer_(format, args);
|
||||
this->write_footer_();
|
||||
this->log_message_(level, tag);
|
||||
|
||||
// Format and send to both console and callbacks
|
||||
this->log_message_to_buffer_and_send_(level, tag, line, format, args);
|
||||
|
||||
recursion_guard_ = false;
|
||||
}
|
||||
#endif // !USE_ESP32
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// Implementation for ESP8266 with flash string support.
|
||||
// Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266.
|
||||
void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format,
|
||||
va_list args) { // NOLINT
|
||||
if (level > this->level_for(tag) || recursion_guard_)
|
||||
return;
|
||||
|
||||
recursion_guard_ = true;
|
||||
this->reset_buffer_();
|
||||
// copy format string
|
||||
this->tx_buffer_at_ = 0;
|
||||
|
||||
// Copy format string from progmem
|
||||
auto *format_pgm_p = reinterpret_cast<const uint8_t *>(format);
|
||||
size_t len = 0;
|
||||
char ch = '.';
|
||||
while (!this->is_buffer_full_() && ch != '\0') {
|
||||
while (this->tx_buffer_at_ < this->tx_buffer_size_ && ch != '\0') {
|
||||
this->tx_buffer_[this->tx_buffer_at_++] = ch = (char) progmem_read_byte(format_pgm_p++);
|
||||
}
|
||||
// Buffer full form copying format
|
||||
if (this->is_buffer_full_())
|
||||
|
||||
// Buffer full from copying format
|
||||
if (this->tx_buffer_at_ >= this->tx_buffer_size_) {
|
||||
recursion_guard_ = false; // Make sure to reset the recursion guard before returning
|
||||
return;
|
||||
}
|
||||
|
||||
// length of format string, includes null terminator
|
||||
uint32_t offset = this->tx_buffer_at_;
|
||||
// Save the offset before calling format_log_to_buffer_with_terminator_
|
||||
// since it will increment tx_buffer_at_ to the end of the formatted string
|
||||
uint32_t msg_start = this->tx_buffer_at_;
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_,
|
||||
&this->tx_buffer_at_, this->tx_buffer_size_);
|
||||
|
||||
// Write to console and send callback starting at the msg_start
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_ + msg_start);
|
||||
}
|
||||
this->call_log_callbacks_(level, tag, this->tx_buffer_ + msg_start);
|
||||
|
||||
// now apply vsnprintf
|
||||
this->write_header_(level, tag, line);
|
||||
this->vprintf_to_buffer_(this->tx_buffer_, args);
|
||||
this->write_footer_();
|
||||
this->log_message_(level, tag, offset);
|
||||
recursion_guard_ = false;
|
||||
}
|
||||
#endif
|
||||
#endif // USE_STORE_LOG_STR_IN_FLASH
|
||||
|
||||
int HOT Logger::level_for(const char *tag) {
|
||||
if (this->log_levels_.count(tag) != 0)
|
||||
return this->log_levels_[tag];
|
||||
inline int Logger::level_for(const char *tag) {
|
||||
auto it = this->log_levels_.find(tag);
|
||||
if (it != this->log_levels_.end())
|
||||
return it->second;
|
||||
return this->current_level_;
|
||||
}
|
||||
|
||||
void HOT Logger::log_message_(int level, const char *tag, int offset) {
|
||||
// remove trailing newline
|
||||
if (this->tx_buffer_[this->tx_buffer_at_ - 1] == '\n') {
|
||||
this->tx_buffer_at_--;
|
||||
}
|
||||
// make sure null terminator is present
|
||||
this->set_null_terminator_();
|
||||
|
||||
const char *msg = this->tx_buffer_ + offset;
|
||||
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(msg);
|
||||
}
|
||||
|
||||
void HOT Logger::call_log_callbacks_(int level, const char *tag, const char *msg) {
|
||||
#ifdef USE_ESP32
|
||||
// Suppress network-logging if memory constrained, but still log to serial
|
||||
// ports. In some configurations (eg BLE enabled) there may be some transient
|
||||
// Suppress network-logging if memory constrained
|
||||
// In some configurations (eg BLE enabled) there may be some transient
|
||||
// memory exhaustion, and trying to log when OOM can lead to a crash. Skipping
|
||||
// here usually allows the stack to recover instead.
|
||||
// See issue #1234 for analysis.
|
||||
if (xPortGetFreeHeapSize() < 2048)
|
||||
return;
|
||||
#endif
|
||||
|
||||
this->log_callback_.call(level, tag, msg);
|
||||
}
|
||||
|
||||
@@ -141,21 +138,50 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
|
||||
this->main_task_ = xTaskGetCurrentTaskHandle();
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
void Logger::init_log_buffer(size_t total_buffer_size) {
|
||||
this->log_buffer_ = esphome::make_unique<logger::TaskLogBuffer>(total_buffer_size);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32)
|
||||
void Logger::loop() {
|
||||
#ifdef USE_ARDUINO
|
||||
if (this->uart_ != UART_SELECTION_USB_CDC) {
|
||||
return;
|
||||
#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO)
|
||||
if (this->uart_ == UART_SELECTION_USB_CDC) {
|
||||
static bool opened = false;
|
||||
if (opened == Serial) {
|
||||
return;
|
||||
}
|
||||
if (false == opened) {
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
opened = !opened;
|
||||
}
|
||||
static bool opened = false;
|
||||
if (opened == Serial) {
|
||||
return;
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
// Process any buffered messages when available
|
||||
if (this->log_buffer_->has_messages()) {
|
||||
logger::TaskLogBuffer::LogMessage *message;
|
||||
const char *text;
|
||||
void *received_token;
|
||||
|
||||
// Process messages from the buffer
|
||||
while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) {
|
||||
this->tx_buffer_at_ = 0;
|
||||
// Use the thread name that was stored when the message was created
|
||||
// This avoids potential crashes if the task no longer exists
|
||||
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
||||
this->write_header_to_buffer_(message->level, message->tag, message->line, thread_name, this->tx_buffer_,
|
||||
&this->tx_buffer_at_, this->tx_buffer_size_);
|
||||
this->write_body_to_buffer_(text, message->text_length, this->tx_buffer_, &this->tx_buffer_at_,
|
||||
this->tx_buffer_size_);
|
||||
this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_);
|
||||
this->tx_buffer_[this->tx_buffer_at_] = '\0';
|
||||
this->call_log_callbacks_(message->level, message->tag, this->tx_buffer_);
|
||||
this->log_buffer_->release_message_main_loop(received_token);
|
||||
}
|
||||
}
|
||||
if (false == opened) {
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
opened = !opened;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
@@ -171,7 +197,7 @@ void Logger::add_on_log_callback(std::function<void(int, const char *, const cha
|
||||
this->log_callback_.add(std::move(callback));
|
||||
}
|
||||
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
|
||||
const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
|
||||
static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
|
||||
|
||||
void Logger::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Logger:");
|
||||
@@ -181,12 +207,16 @@ void Logger::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32, this->baud_rate_);
|
||||
ESP_LOGCONFIG(TAG, " Hardware UART: %s", get_uart_selection_());
|
||||
#endif
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
if (this->log_buffer_) {
|
||||
ESP_LOGCONFIG(TAG, " Task Log Buffer Size: %u", this->log_buffer_->size());
|
||||
}
|
||||
#endif
|
||||
|
||||
for (auto &it : this->log_levels_) {
|
||||
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_LEVELS[it.second]);
|
||||
}
|
||||
}
|
||||
void Logger::write_footer_() { this->write_to_buffer_(ESPHOME_LOG_RESET_COLOR, strlen(ESPHOME_LOG_RESET_COLOR)); }
|
||||
|
||||
void Logger::set_log_level(int level) {
|
||||
if (level > ESPHOME_LOG_LEVEL) {
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
|
||||
#include <cstdarg>
|
||||
#include <map>
|
||||
#ifdef USE_ESP32
|
||||
#include <atomic>
|
||||
#endif
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#include "task_log_buffer.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
#include <HardwareSerial.h>
|
||||
@@ -26,6 +33,29 @@ namespace esphome {
|
||||
|
||||
namespace logger {
|
||||
|
||||
// Color and letter constants for log levels
|
||||
static const char *const LOG_LEVEL_COLORS[] = {
|
||||
"", // NONE
|
||||
ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), // ERROR
|
||||
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW), // WARNING
|
||||
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN), // INFO
|
||||
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA), // CONFIG
|
||||
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN), // DEBUG
|
||||
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY), // VERBOSE
|
||||
ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE), // VERY_VERBOSE
|
||||
};
|
||||
|
||||
static const char *const LOG_LEVEL_LETTERS[] = {
|
||||
"", // NONE
|
||||
"E", // ERROR
|
||||
"W", // WARNING
|
||||
"I", // INFO
|
||||
"C", // CONFIG
|
||||
"D", // DEBUG
|
||||
"V", // VERBOSE
|
||||
"VV", // VERY_VERBOSE
|
||||
};
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
|
||||
/** Enum for logging UART selection
|
||||
*
|
||||
@@ -57,7 +87,10 @@ enum UARTSelection {
|
||||
class Logger : public Component {
|
||||
public:
|
||||
explicit Logger(uint32_t baud_rate, size_t tx_buffer_size);
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
void init_log_buffer(size_t total_buffer_size);
|
||||
#endif
|
||||
#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32)
|
||||
void loop() override;
|
||||
#endif
|
||||
/// Manually set the baud rate for serial, set to 0 to disable.
|
||||
@@ -87,7 +120,7 @@ class Logger : public Component {
|
||||
void pre_setup();
|
||||
void dump_config() override;
|
||||
|
||||
int level_for(const char *tag);
|
||||
inline int level_for(const char *tag);
|
||||
|
||||
/// Register a callback that will be called for every log message sent
|
||||
void add_on_log_callback(std::function<void(int, const char *, const char *)> &&callback);
|
||||
@@ -103,46 +136,66 @@ class Logger : public Component {
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void write_header_(int level, const char *tag, int line);
|
||||
void write_footer_();
|
||||
void log_message_(int level, const char *tag, int offset = 0);
|
||||
void call_log_callbacks_(int level, const char *tag, const char *msg);
|
||||
void write_msg_(const char *msg);
|
||||
|
||||
inline bool is_buffer_full_() const { return this->tx_buffer_at_ >= this->tx_buffer_size_; }
|
||||
inline int buffer_remaining_capacity_() const { return this->tx_buffer_size_ - this->tx_buffer_at_; }
|
||||
inline void reset_buffer_() { this->tx_buffer_at_ = 0; }
|
||||
inline void set_null_terminator_() {
|
||||
// does not increment buffer_at
|
||||
this->tx_buffer_[this->tx_buffer_at_] = '\0';
|
||||
}
|
||||
inline void write_to_buffer_(char value) {
|
||||
if (!this->is_buffer_full_())
|
||||
this->tx_buffer_[this->tx_buffer_at_++] = value;
|
||||
}
|
||||
inline void write_to_buffer_(const char *value, int length) {
|
||||
for (int i = 0; i < length && !this->is_buffer_full_(); i++) {
|
||||
this->tx_buffer_[this->tx_buffer_at_++] = value[i];
|
||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||
// It's the caller's responsibility to initialize buffer_at (typically to 0)
|
||||
inline void HOT format_log_to_buffer_with_terminator_(int level, const char *tag, int line, const char *format,
|
||||
va_list args, char *buffer, int *buffer_at, int buffer_size) {
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size);
|
||||
#else
|
||||
this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size);
|
||||
#endif
|
||||
this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, args);
|
||||
this->write_footer_to_buffer_(buffer, buffer_at, buffer_size);
|
||||
|
||||
// Always ensure the buffer has a null terminator, even if we need to
|
||||
// overwrite the last character of the actual content
|
||||
if (*buffer_at >= buffer_size) {
|
||||
buffer[buffer_size - 1] = '\0'; // Truncate and ensure null termination
|
||||
} else {
|
||||
buffer[*buffer_at] = '\0'; // Normal case, append null terminator
|
||||
}
|
||||
}
|
||||
inline void vprintf_to_buffer_(const char *format, va_list args) {
|
||||
if (this->is_buffer_full_())
|
||||
return;
|
||||
int remaining = this->buffer_remaining_capacity_();
|
||||
int ret = vsnprintf(this->tx_buffer_ + this->tx_buffer_at_, remaining, format, args);
|
||||
if (ret < 0) {
|
||||
// Encoding error, do not increment buffer_at
|
||||
|
||||
// Helper to format and send a log message to both console and callbacks
|
||||
inline void HOT log_message_to_buffer_and_send_(int level, const char *tag, int line, const char *format,
|
||||
va_list args) {
|
||||
// Format to tx_buffer and prepare for output
|
||||
this->tx_buffer_at_ = 0; // Initialize buffer position
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_,
|
||||
this->tx_buffer_size_);
|
||||
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console
|
||||
}
|
||||
this->call_log_callbacks_(level, tag, this->tx_buffer_);
|
||||
}
|
||||
|
||||
// Write the body of the log message to the buffer
|
||||
inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, int *buffer_at, int buffer_size) {
|
||||
// Calculate available space
|
||||
const int available = buffer_size - *buffer_at;
|
||||
if (available <= 0)
|
||||
return;
|
||||
|
||||
// Determine copy length (minimum of remaining capacity and string length)
|
||||
const size_t copy_len = (length < static_cast<size_t>(available)) ? length : available;
|
||||
|
||||
// Copy the data
|
||||
if (copy_len > 0) {
|
||||
memcpy(buffer + *buffer_at, value, copy_len);
|
||||
*buffer_at += copy_len;
|
||||
}
|
||||
if (ret >= remaining) {
|
||||
// output was too long, truncated
|
||||
ret = remaining;
|
||||
}
|
||||
this->tx_buffer_at_ += ret;
|
||||
}
|
||||
inline void printf_to_buffer_(const char *format, ...) {
|
||||
|
||||
// Format string to explicit buffer with varargs
|
||||
inline void printf_to_buffer_(const char *format, char *buffer, int *buffer_at, int buffer_size, ...) {
|
||||
va_list arg;
|
||||
va_start(arg, format);
|
||||
this->vprintf_to_buffer_(format, arg);
|
||||
va_start(arg, buffer_size);
|
||||
this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, arg);
|
||||
va_end(arg);
|
||||
}
|
||||
|
||||
@@ -169,10 +222,82 @@ class Logger : public Component {
|
||||
std::map<std::string, int> log_levels_{};
|
||||
CallbackManager<void(int, const char *, const char *)> log_callback_{};
|
||||
int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE};
|
||||
/// Prevents recursive log calls, if true a log message is already being processed.
|
||||
bool recursion_guard_ = false;
|
||||
#ifdef USE_ESP32
|
||||
std::atomic<bool> recursion_guard_{false};
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
std::unique_ptr<logger::TaskLogBuffer> log_buffer_; // Will be initialized with init_log_buffer
|
||||
#endif
|
||||
#else
|
||||
bool recursion_guard_{false};
|
||||
#endif
|
||||
void *main_task_ = nullptr;
|
||||
CallbackManager<void(int)> level_callback_{};
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
const char *HOT get_thread_name_() {
|
||||
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
||||
if (current_task == main_task_) {
|
||||
return nullptr; // Main task
|
||||
} else {
|
||||
#if defined(USE_ESP32)
|
||||
return pcTaskGetName(current_task);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
return pcTaskGetTaskName(current_task);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
inline void HOT write_header_to_buffer_(int level, const char *tag, int line, const char *thread_name, char *buffer,
|
||||
int *buffer_at, int buffer_size) {
|
||||
// Format header
|
||||
if (level < 0)
|
||||
level = 0;
|
||||
if (level > 7)
|
||||
level = 7;
|
||||
|
||||
const char *color = esphome::logger::LOG_LEVEL_COLORS[level];
|
||||
const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level];
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
if (thread_name != nullptr) {
|
||||
// Non-main task with thread name
|
||||
this->printf_to_buffer_("%s[%s][%s:%03u]%s[%s]%s: ", buffer, buffer_at, buffer_size, color, letter, tag, line,
|
||||
ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), thread_name, color);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Main task or non ESP32/LibreTiny platform
|
||||
this->printf_to_buffer_("%s[%s][%s:%03u]: ", buffer, buffer_at, buffer_size, color, letter, tag, line);
|
||||
}
|
||||
|
||||
inline void HOT format_body_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format,
|
||||
va_list args) {
|
||||
// Get remaining capacity in the buffer
|
||||
const int remaining = buffer_size - *buffer_at;
|
||||
if (remaining <= 0)
|
||||
return;
|
||||
|
||||
const int ret = vsnprintf(buffer + *buffer_at, remaining, format, args);
|
||||
|
||||
if (ret < 0) {
|
||||
return; // Encoding error, do not increment buffer_at
|
||||
}
|
||||
|
||||
// Update buffer_at with the formatted length (handle truncation)
|
||||
int formatted_len = (ret >= remaining) ? remaining : ret;
|
||||
*buffer_at += formatted_len;
|
||||
|
||||
// Remove all trailing newlines right after formatting
|
||||
while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n') {
|
||||
(*buffer_at)--;
|
||||
}
|
||||
}
|
||||
|
||||
inline void HOT write_footer_to_buffer_(char *buffer, int *buffer_at, int buffer_size) {
|
||||
static const int RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR);
|
||||
this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size);
|
||||
}
|
||||
};
|
||||
extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
|
||||
138
esphome/components/logger/task_log_buffer.cpp
Normal file
138
esphome/components/logger/task_log_buffer.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
|
||||
#include "task_log_buffer.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
namespace esphome {
|
||||
namespace logger {
|
||||
|
||||
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
|
||||
// Store the buffer size
|
||||
this->size_ = total_buffer_size;
|
||||
// Allocate memory for the ring buffer using ESPHome's RAM allocator
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
this->storage_ = allocator.allocate(this->size_);
|
||||
// Create a static ring buffer with RINGBUF_TYPE_NOSPLIT for message integrity
|
||||
this->ring_buffer_ = xRingbufferCreateStatic(this->size_, RINGBUF_TYPE_NOSPLIT, this->storage_, &this->structure_);
|
||||
}
|
||||
|
||||
TaskLogBuffer::~TaskLogBuffer() {
|
||||
if (this->ring_buffer_ != nullptr) {
|
||||
// Delete the ring buffer
|
||||
vRingbufferDelete(this->ring_buffer_);
|
||||
this->ring_buffer_ = nullptr;
|
||||
|
||||
// Free the allocated memory
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
allocator.deallocate(this->storage_, this->size_);
|
||||
this->storage_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **text, void **received_token) {
|
||||
if (message == nullptr || text == nullptr || received_token == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t item_size = 0;
|
||||
void *received_item = xRingbufferReceive(ring_buffer_, &item_size, 0);
|
||||
if (received_item == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LogMessage *msg = static_cast<LogMessage *>(received_item);
|
||||
*message = msg;
|
||||
*text = msg->text_data();
|
||||
*received_token = received_item;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TaskLogBuffer::release_message_main_loop(void *token) {
|
||||
if (token == nullptr) {
|
||||
return;
|
||||
}
|
||||
vRingbufferReturnItem(ring_buffer_, token);
|
||||
// Update counter to mark all messages as processed
|
||||
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
||||
const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
int ret = vsnprintf(nullptr, 0, format, args_copy);
|
||||
va_end(args_copy);
|
||||
|
||||
if (ret <= 0) {
|
||||
return false; // Formatting error or empty message
|
||||
}
|
||||
|
||||
// Calculate actual text length (capped to maximum size)
|
||||
static constexpr size_t MAX_TEXT_SIZE = 255;
|
||||
size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret;
|
||||
|
||||
// Calculate total size needed (header + text length + null terminator)
|
||||
size_t total_size = sizeof(LogMessage) + text_length + 1;
|
||||
|
||||
// Acquire memory directly from the ring buffer
|
||||
void *acquired_memory = nullptr;
|
||||
BaseType_t result = xRingbufferSendAcquire(ring_buffer_, &acquired_memory, total_size, 0);
|
||||
|
||||
if (result != pdTRUE || acquired_memory == nullptr) {
|
||||
return false; // Failed to acquire memory
|
||||
}
|
||||
|
||||
// Set up the message header in the acquired memory
|
||||
LogMessage *msg = static_cast<LogMessage *>(acquired_memory);
|
||||
msg->level = level;
|
||||
msg->tag = tag;
|
||||
msg->line = line;
|
||||
|
||||
// Store the thread name now instead of waiting until main loop processing
|
||||
// This avoids crashes if the task completes or is deleted between when this message
|
||||
// is enqueued and when it's processed by the main loop
|
||||
const char *thread_name = pcTaskGetName(task_handle);
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||
} else {
|
||||
msg->thread_name[0] = '\0'; // Empty string if no thread name
|
||||
}
|
||||
|
||||
// Format the message text directly into the acquired memory
|
||||
// We add 1 to text_length to ensure space for null terminator during formatting
|
||||
char *text_area = msg->text_data();
|
||||
ret = vsnprintf(text_area, text_length + 1, format, args);
|
||||
|
||||
// Handle unexpected formatting error
|
||||
if (ret <= 0) {
|
||||
vRingbufferReturnItem(ring_buffer_, acquired_memory);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove trailing newlines
|
||||
while (text_length > 0 && text_area[text_length - 1] == '\n') {
|
||||
text_length--;
|
||||
}
|
||||
|
||||
msg->text_length = text_length;
|
||||
// Complete the send operation with the acquired memory
|
||||
result = xRingbufferSendComplete(ring_buffer_, acquired_memory);
|
||||
|
||||
if (result != pdTRUE) {
|
||||
return false; // Failed to complete the message send
|
||||
}
|
||||
|
||||
// Message sent successfully, increment the counter
|
||||
message_counter_.fetch_add(1, std::memory_order_relaxed);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace logger
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
69
esphome/components/logger/task_log_buffer.h
Normal file
69
esphome/components/logger/task_log_buffer.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/ringbuf.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace logger {
|
||||
|
||||
class TaskLogBuffer {
|
||||
public:
|
||||
// Structure for a log message header (text data follows immediately after)
|
||||
struct LogMessage {
|
||||
const char *tag; // We store the pointer, assuming tags are static
|
||||
char thread_name[16]; // Store thread name directly (only used for non-main threads)
|
||||
uint16_t text_length; // Length of the message text (up to ~64KB)
|
||||
uint16_t line; // Source code line number
|
||||
uint8_t level; // Log level (0-7)
|
||||
|
||||
// Methods for accessing message contents
|
||||
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); }
|
||||
|
||||
inline const char *text_data() const { return reinterpret_cast<const char *>(this) + sizeof(LogMessage); }
|
||||
};
|
||||
|
||||
// Constructor that takes a total buffer size
|
||||
explicit TaskLogBuffer(size_t total_buffer_size);
|
||||
~TaskLogBuffer();
|
||||
|
||||
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
|
||||
bool borrow_message_main_loop(LogMessage **message, const char **text, void **received_token);
|
||||
|
||||
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
|
||||
void release_message_main_loop(void *token);
|
||||
|
||||
// Thread-safe - send a message to the ring buffer from any thread
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
||||
const char *format, va_list args);
|
||||
|
||||
// Check if there are messages ready to be processed using an atomic counter for performance
|
||||
inline bool HOT has_messages() const {
|
||||
return message_counter_.load(std::memory_order_relaxed) != last_processed_counter_;
|
||||
}
|
||||
|
||||
// Get the total buffer size in bytes
|
||||
inline size_t size() const { return size_; }
|
||||
|
||||
private:
|
||||
RingbufHandle_t ring_buffer_{nullptr}; // FreeRTOS ring buffer handle
|
||||
StaticRingbuffer_t structure_; // Static structure for the ring buffer
|
||||
uint8_t *storage_{nullptr}; // Pointer to allocated memory
|
||||
size_t size_{0}; // Size of allocated memory
|
||||
|
||||
// Atomic counter for message tracking (only differences matter)
|
||||
std::atomic<uint16_t> message_counter_{0}; // Incremented when messages are committed
|
||||
mutable uint16_t last_processed_counter_{0}; // Tracks last processed message
|
||||
};
|
||||
|
||||
} // namespace logger
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
@@ -19,9 +19,8 @@ from ..widgets import get_widgets, wait_for_widgets
|
||||
|
||||
LVGLText = lvgl_ns.class_("LVGLText", text.Text)
|
||||
|
||||
CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(
|
||||
CONFIG_SCHEMA = text.text_schema(LVGLText).extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(LVGLText),
|
||||
cv.Required(CONF_WIDGET): cv.use_id(LvText),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -22,8 +22,6 @@ static const ssize_t DETECTION_QUEUE_LENGTH = 5;
|
||||
static const size_t DATA_TIMEOUT_MS = 50;
|
||||
|
||||
static const uint32_t RING_BUFFER_DURATION_MS = 120;
|
||||
static const uint32_t RING_BUFFER_SAMPLES = RING_BUFFER_DURATION_MS * (AUDIO_SAMPLE_FREQUENCY / 1000);
|
||||
static const size_t RING_BUFFER_SIZE = RING_BUFFER_SAMPLES * sizeof(int16_t);
|
||||
|
||||
static const uint32_t INFERENCE_TASK_STACK_SIZE = 3072;
|
||||
static const UBaseType_t INFERENCE_TASK_PRIORITY = 3;
|
||||
@@ -141,13 +139,15 @@ void MicroWakeWord::inference_task(void *params) {
|
||||
xEventGroupSetBits(this_mww->event_group_, EventGroupBits::TASK_STARTING);
|
||||
|
||||
{ // Ensures any C++ objects fall out of scope to deallocate before deleting the task
|
||||
const size_t new_samples_to_read = this_mww->features_step_size_ * (AUDIO_SAMPLE_FREQUENCY / 1000);
|
||||
|
||||
const size_t new_bytes_to_process =
|
||||
this_mww->microphone_source_->get_audio_stream_info().ms_to_bytes(this_mww->features_step_size_);
|
||||
std::unique_ptr<audio::AudioSourceTransferBuffer> audio_buffer;
|
||||
int8_t features_buffer[PREPROCESSOR_FEATURE_SIZE];
|
||||
|
||||
if (!(xEventGroupGetBits(this_mww->event_group_) & ERROR_BITS)) {
|
||||
// Allocate audio transfer buffer
|
||||
audio_buffer = audio::AudioSourceTransferBuffer::create(new_samples_to_read * sizeof(int16_t));
|
||||
audio_buffer = audio::AudioSourceTransferBuffer::create(new_bytes_to_process);
|
||||
|
||||
if (audio_buffer == nullptr) {
|
||||
xEventGroupSetBits(this_mww->event_group_, EventGroupBits::ERROR_MEMORY);
|
||||
@@ -156,7 +156,8 @@ void MicroWakeWord::inference_task(void *params) {
|
||||
|
||||
if (!(xEventGroupGetBits(this_mww->event_group_) & ERROR_BITS)) {
|
||||
// Allocate ring buffer
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer = RingBuffer::create(RING_BUFFER_SIZE);
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer = RingBuffer::create(
|
||||
this_mww->microphone_source_->get_audio_stream_info().ms_to_bytes(RING_BUFFER_DURATION_MS));
|
||||
if (temp_ring_buffer.use_count() == 0) {
|
||||
xEventGroupSetBits(this_mww->event_group_, EventGroupBits::ERROR_MEMORY);
|
||||
}
|
||||
@@ -171,13 +172,13 @@ void MicroWakeWord::inference_task(void *params) {
|
||||
while (!(xEventGroupGetBits(this_mww->event_group_) & COMMAND_STOP)) {
|
||||
audio_buffer->transfer_data_from_source(pdMS_TO_TICKS(DATA_TIMEOUT_MS));
|
||||
|
||||
if (audio_buffer->available() < new_samples_to_read * sizeof(int16_t)) {
|
||||
if (audio_buffer->available() < new_bytes_to_process) {
|
||||
// Insufficient data to generate new spectrogram features, read more next iteration
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate new spectrogram features
|
||||
size_t processed_samples = this_mww->generate_features_(
|
||||
uint32_t processed_samples = this_mww->generate_features_(
|
||||
(int16_t *) audio_buffer->get_buffer_start(), audio_buffer->available() / sizeof(int16_t), features_buffer);
|
||||
audio_buffer->decrease_buffer_length(processed_samples * sizeof(int16_t));
|
||||
|
||||
@@ -297,7 +298,8 @@ void MicroWakeWord::loop() {
|
||||
if ((this->inference_task_handle_ == nullptr) && !this->status_has_error()) {
|
||||
// Setup preprocesor feature generator. If done in the task, it would lock the task to its initial core, as it
|
||||
// uses floating point operations.
|
||||
if (!FrontendPopulateState(&this->frontend_config_, &this->frontend_state_, AUDIO_SAMPLE_FREQUENCY)) {
|
||||
if (!FrontendPopulateState(&this->frontend_config_, &this->frontend_state_,
|
||||
this->microphone_source_->get_audio_stream_info().get_sample_rate())) {
|
||||
this->status_momentary_error(
|
||||
"Failed to allocate buffers for spectrogram feature processor, attempting again in 1 second", 1000);
|
||||
return;
|
||||
|
||||
@@ -121,8 +121,6 @@ class MicroWakeWord : public Component {
|
||||
/// @param audio_features (int8_t *) Buffer containing new spectrogram features
|
||||
/// @return True if successful, false if any errors were encountered
|
||||
bool update_model_probabilities_(const int8_t audio_features[PREPROCESSOR_FEATURE_SIZE]);
|
||||
|
||||
inline uint16_t new_samples_to_get_() { return (this->features_step_size_ * (AUDIO_SAMPLE_FREQUENCY / 1000)); }
|
||||
};
|
||||
|
||||
} // namespace micro_wake_word
|
||||
|
||||
@@ -15,8 +15,6 @@ namespace micro_wake_word {
|
||||
static const uint8_t PREPROCESSOR_FEATURE_SIZE = 40;
|
||||
// Duration of each slice used as input into the preprocessor
|
||||
static const uint8_t FEATURE_DURATION_MS = 30;
|
||||
// Audio sample frequency in hertz
|
||||
static const uint16_t AUDIO_SAMPLE_FREQUENCY = 16000;
|
||||
|
||||
static const float FILTERBANK_LOWER_BAND_LIMIT = 125.0;
|
||||
static const float FILTERBANK_UPPER_BAND_LIMIT = 7500.0;
|
||||
|
||||
@@ -159,12 +159,13 @@ void StreamingModel::reset_probabilities() {
|
||||
this->ignore_windows_ = -MIN_SLICES_BEFORE_DETECTION;
|
||||
}
|
||||
|
||||
WakeWordModel::WakeWordModel(const std::string &id, const uint8_t *model_start, uint8_t probability_cutoff,
|
||||
WakeWordModel::WakeWordModel(const std::string &id, const uint8_t *model_start, uint8_t default_probability_cutoff,
|
||||
size_t sliding_window_average_size, const std::string &wake_word, size_t tensor_arena_size,
|
||||
bool default_enabled, bool internal_only) {
|
||||
this->id_ = id;
|
||||
this->model_start_ = model_start;
|
||||
this->probability_cutoff_ = probability_cutoff;
|
||||
this->default_probability_cutoff_ = default_probability_cutoff;
|
||||
this->probability_cutoff_ = default_probability_cutoff;
|
||||
this->sliding_window_size_ = sliding_window_average_size;
|
||||
this->recent_streaming_probabilities_.resize(sliding_window_average_size, 0);
|
||||
this->wake_word_ = wake_word;
|
||||
@@ -222,10 +223,11 @@ DetectionEvent WakeWordModel::determine_detected() {
|
||||
return detection_event;
|
||||
}
|
||||
|
||||
VADModel::VADModel(const uint8_t *model_start, uint8_t probability_cutoff, size_t sliding_window_size,
|
||||
VADModel::VADModel(const uint8_t *model_start, uint8_t default_probability_cutoff, size_t sliding_window_size,
|
||||
size_t tensor_arena_size) {
|
||||
this->model_start_ = model_start;
|
||||
this->probability_cutoff_ = probability_cutoff;
|
||||
this->default_probability_cutoff_ = default_probability_cutoff;
|
||||
this->probability_cutoff_ = default_probability_cutoff;
|
||||
this->sliding_window_size_ = sliding_window_size;
|
||||
this->recent_streaming_probabilities_.resize(sliding_window_size, 0);
|
||||
this->tensor_arena_size_ = tensor_arena_size;
|
||||
|
||||
@@ -50,9 +50,14 @@ class StreamingModel {
|
||||
virtual void disable() { this->enabled_ = false; }
|
||||
|
||||
/// @brief Return true if the model is enabled.
|
||||
bool is_enabled() { return this->enabled_; }
|
||||
bool is_enabled() const { return this->enabled_; }
|
||||
|
||||
bool get_unprocessed_probability_status() { return this->unprocessed_probability_status_; }
|
||||
bool get_unprocessed_probability_status() const { return this->unprocessed_probability_status_; }
|
||||
|
||||
// Quantized probability cutoffs mapping 0.0 - 1.0 to 0 - 255
|
||||
uint8_t get_default_probability_cutoff() const { return this->default_probability_cutoff_; }
|
||||
uint8_t get_probability_cutoff() const { return this->probability_cutoff_; }
|
||||
void set_probability_cutoff(uint8_t probability_cutoff) { this->probability_cutoff_ = probability_cutoff; }
|
||||
|
||||
protected:
|
||||
/// @brief Allocates tensor and variable arenas and sets up the model interpreter
|
||||
@@ -69,8 +74,10 @@ class StreamingModel {
|
||||
uint8_t current_stride_step_{0};
|
||||
int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION};
|
||||
|
||||
uint8_t probability_cutoff_; // Quantized probability cutoff mapping 0.0 - 1.0 to 0 - 255
|
||||
uint8_t default_probability_cutoff_;
|
||||
uint8_t probability_cutoff_;
|
||||
size_t sliding_window_size_;
|
||||
|
||||
size_t last_n_index_{0};
|
||||
size_t tensor_arena_size_;
|
||||
std::vector<uint8_t> recent_streaming_probabilities_;
|
||||
@@ -88,14 +95,14 @@ class WakeWordModel final : public StreamingModel {
|
||||
/// @brief Constructs a wake word model object
|
||||
/// @param id (std::string) identifier for this model
|
||||
/// @param model_start (const uint8_t *) pointer to the start of the model's TFLite FlatBuffer
|
||||
/// @param probability_cutoff (uint8_t) probability cutoff for acceping the wake word has been said
|
||||
/// @param default_probability_cutoff (uint8_t) probability cutoff for acceping the wake word has been said
|
||||
/// @param sliding_window_average_size (size_t) the length of the sliding window computing the mean rolling
|
||||
/// probability
|
||||
/// @param wake_word (std::string) Friendly name of the wake word
|
||||
/// @param tensor_arena_size (size_t) Size in bytes for allocating the tensor arena
|
||||
/// @param default_enabled (bool) If true, it will be enabled by default on first boot
|
||||
/// @param internal_only (bool) If true, the model will not be exposed to HomeAssistant as an available model
|
||||
WakeWordModel(const std::string &id, const uint8_t *model_start, uint8_t probability_cutoff,
|
||||
WakeWordModel(const std::string &id, const uint8_t *model_start, uint8_t default_probability_cutoff,
|
||||
size_t sliding_window_average_size, const std::string &wake_word, size_t tensor_arena_size,
|
||||
bool default_enabled, bool internal_only);
|
||||
|
||||
@@ -132,7 +139,7 @@ class WakeWordModel final : public StreamingModel {
|
||||
|
||||
class VADModel final : public StreamingModel {
|
||||
public:
|
||||
VADModel(const uint8_t *model_start, uint8_t probability_cutoff, size_t sliding_window_size,
|
||||
VADModel(const uint8_t *model_start, uint8_t default_probability_cutoff, size_t sliding_window_size,
|
||||
size_t tensor_arena_size);
|
||||
|
||||
void log_model_config() override;
|
||||
|
||||
@@ -123,11 +123,8 @@ def microphone_source_schema(
|
||||
)
|
||||
|
||||
|
||||
_UNDEF = object()
|
||||
|
||||
|
||||
def final_validate_microphone_source_schema(
|
||||
component_name: str, sample_rate: int = _UNDEF
|
||||
component_name: str, sample_rate: int = cv.UNDEFINED
|
||||
):
|
||||
"""Validates that the microphone source can provide audio in the correct format. In particular it validates the sample rate and the enabled channels.
|
||||
|
||||
@@ -141,7 +138,7 @@ def final_validate_microphone_source_schema(
|
||||
"""
|
||||
|
||||
def _validate_audio_compatability(config):
|
||||
if sample_rate is not _UNDEF:
|
||||
if sample_rate is not cv.UNDEFINED:
|
||||
# Issues require changing the microphone configuration
|
||||
# - Verifies sample rates match
|
||||
audio.final_validate_audio_schema(
|
||||
@@ -165,13 +162,22 @@ def final_validate_microphone_source_schema(
|
||||
return _validate_audio_compatability
|
||||
|
||||
|
||||
async def microphone_source_to_code(config):
|
||||
async def microphone_source_to_code(config, passive=False):
|
||||
"""Creates a MicrophoneSource variable for codegen.
|
||||
|
||||
Setting passive to true makes the MicrophoneSource never start/stop the microphone, but only receives audio when another component has actively started the Microphone. If false, then the microphone needs to be explicitly started/stopped.
|
||||
|
||||
Args:
|
||||
config (Schema): Created with `microphone_source_schema` specifying bits per sample, channels, and gain factor
|
||||
passive (bool): Enable passive mode for the MicrophoneSource
|
||||
"""
|
||||
mic = await cg.get_variable(config[CONF_MICROPHONE])
|
||||
mic_source = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
mic,
|
||||
config[CONF_BITS_PER_SAMPLE],
|
||||
config[CONF_GAIN_FACTOR],
|
||||
passive,
|
||||
)
|
||||
for channel in config[CONF_CHANNELS]:
|
||||
cg.add(mic_source.add_channel(channel))
|
||||
|
||||
@@ -3,34 +3,58 @@
|
||||
namespace esphome {
|
||||
namespace microphone {
|
||||
|
||||
static const int32_t Q25_MAX_VALUE = (1 << 25) - 1;
|
||||
static const int32_t Q25_MIN_VALUE = ~Q25_MAX_VALUE;
|
||||
|
||||
void MicrophoneSource::add_data_callback(std::function<void(const std::vector<uint8_t> &)> &&data_callback) {
|
||||
std::function<void(const std::vector<uint8_t> &)> filtered_callback =
|
||||
[this, data_callback](const std::vector<uint8_t> &data) {
|
||||
if (this->enabled_) {
|
||||
data_callback(this->process_audio_(data));
|
||||
if (this->enabled_ || this->passive_) {
|
||||
if (this->processed_samples_.use_count() == 0) {
|
||||
// Create vector if its unused
|
||||
this->processed_samples_ = std::make_shared<std::vector<uint8_t>>();
|
||||
}
|
||||
|
||||
// Take temporary ownership of samples vector to avoid deallaction before the callback finishes
|
||||
std::shared_ptr<std::vector<uint8_t>> output_samples = this->processed_samples_;
|
||||
this->process_audio_(data, *output_samples);
|
||||
data_callback(*output_samples);
|
||||
}
|
||||
};
|
||||
this->mic_->add_data_callback(std::move(filtered_callback));
|
||||
}
|
||||
|
||||
audio::AudioStreamInfo MicrophoneSource::get_audio_stream_info() {
|
||||
return audio::AudioStreamInfo(this->bits_per_sample_, this->channels_.count(),
|
||||
this->mic_->get_audio_stream_info().get_sample_rate());
|
||||
}
|
||||
|
||||
void MicrophoneSource::start() {
|
||||
if (!this->enabled_) {
|
||||
if (!this->enabled_ && !this->passive_) {
|
||||
this->enabled_ = true;
|
||||
this->mic_->start();
|
||||
}
|
||||
}
|
||||
|
||||
void MicrophoneSource::stop() {
|
||||
if (this->enabled_) {
|
||||
if (this->enabled_ && !this->passive_) {
|
||||
this->enabled_ = false;
|
||||
this->mic_->stop();
|
||||
this->processed_samples_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> MicrophoneSource::process_audio_(const std::vector<uint8_t> &data) {
|
||||
// Bit depth conversions are obtained by truncating bits or padding with zeros - no dithering is applied.
|
||||
void MicrophoneSource::process_audio_(const std::vector<uint8_t> &data, std::vector<uint8_t> &filtered_data) {
|
||||
// - Bit depth conversions are obtained by truncating bits or padding with zeros - no dithering is applied.
|
||||
// - In the comments, Qxx refers to a fixed point number with xx bits of precision for representing fractional values.
|
||||
// For example, audio with a bit depth of 16 can store a sample in a int16, which can be considered a Q15 number.
|
||||
// - All samples are converted to Q25 before applying the gain factor - this results in a small precision loss for
|
||||
// data with 32 bits per sample. Since the maximum gain factor is 64 = (1<<6), this ensures that applying the gain
|
||||
// will never overflow a 32 bit signed integer. This still retains more bit depth than what is audibly noticeable.
|
||||
// - Loops for reading/writing data buffers are unrolled, assuming little endian, for a small performance increase.
|
||||
|
||||
const size_t source_bytes_per_sample = this->mic_->get_audio_stream_info().samples_to_bytes(1);
|
||||
const size_t source_channels = this->mic_->get_audio_stream_info().get_channels();
|
||||
const uint32_t source_channels = this->mic_->get_audio_stream_info().get_channels();
|
||||
|
||||
const size_t source_bytes_per_frame = this->mic_->get_audio_stream_info().frames_to_bytes(1);
|
||||
|
||||
@@ -38,60 +62,33 @@ std::vector<uint8_t> MicrophoneSource::process_audio_(const std::vector<uint8_t>
|
||||
const size_t target_bytes_per_sample = (this->bits_per_sample_ + 7) / 8;
|
||||
const size_t target_bytes_per_frame = target_bytes_per_sample * this->channels_.count();
|
||||
|
||||
std::vector<uint8_t> filtered_data;
|
||||
filtered_data.reserve(target_bytes_per_frame * total_frames);
|
||||
filtered_data.resize(target_bytes_per_frame * total_frames);
|
||||
|
||||
const int32_t target_min_value = -(1 << (8 * target_bytes_per_sample - 1));
|
||||
const int32_t target_max_value = (1 << (8 * target_bytes_per_sample - 1)) - 1;
|
||||
uint8_t *current_data = filtered_data.data();
|
||||
|
||||
for (size_t frame_index = 0; frame_index < total_frames; ++frame_index) {
|
||||
for (size_t channel_index = 0; channel_index < source_channels; ++channel_index) {
|
||||
for (uint32_t frame_index = 0; frame_index < total_frames; ++frame_index) {
|
||||
for (uint32_t channel_index = 0; channel_index < source_channels; ++channel_index) {
|
||||
if (this->channels_.test(channel_index)) {
|
||||
// Channel's current sample is included in the target mask. Convert bits per sample, if necessary.
|
||||
|
||||
size_t sample_index = frame_index * source_bytes_per_frame + channel_index * source_bytes_per_sample;
|
||||
const uint32_t sample_index = frame_index * source_bytes_per_frame + channel_index * source_bytes_per_sample;
|
||||
|
||||
int32_t sample = 0;
|
||||
|
||||
// Copy the data into the most significant bits of the sample variable to ensure the sign bit is correct
|
||||
uint8_t bit_offset = (4 - source_bytes_per_sample) * 8;
|
||||
for (int i = 0; i < source_bytes_per_sample; ++i) {
|
||||
sample |= data[sample_index + i] << bit_offset;
|
||||
bit_offset += 8;
|
||||
}
|
||||
|
||||
// Shift data back to the least significant bits
|
||||
if (source_bytes_per_sample >= target_bytes_per_sample) {
|
||||
// Keep source bytes per sample of data so that the gain multiplication uses all significant bits instead of
|
||||
// shifting to the target bytes per sample immediately, potentially losing information.
|
||||
sample >>= (4 - source_bytes_per_sample) * 8; // ``source_bytes_per_sample`` bytes of valid data
|
||||
} else {
|
||||
// Keep padded zeros to match the target bytes per sample
|
||||
sample >>= (4 - target_bytes_per_sample) * 8; // ``target_bytes_per_sample`` bytes of valid data
|
||||
}
|
||||
int32_t sample = audio::unpack_audio_sample_to_q31(&data[sample_index], source_bytes_per_sample); // Q31
|
||||
sample >>= 6; // Q31 -> Q25
|
||||
|
||||
// Apply gain using multiplication
|
||||
sample *= this->gain_factor_;
|
||||
sample *= this->gain_factor_; // Q25
|
||||
|
||||
// Match target output bytes by shifting out the least significant bits
|
||||
if (source_bytes_per_sample > target_bytes_per_sample) {
|
||||
sample >>= 8 * (source_bytes_per_sample -
|
||||
target_bytes_per_sample); // ``target_bytes_per_sample`` bytes of valid data
|
||||
}
|
||||
// Clamp ``sample`` in case gain multiplication overflows 25 bits
|
||||
sample = clamp<int32_t>(sample, Q25_MIN_VALUE, Q25_MAX_VALUE); // Q25
|
||||
|
||||
// Clamp ``sample`` to the target bytes per sample range in case gain multiplication overflows
|
||||
sample = clamp<int32_t>(sample, target_min_value, target_max_value);
|
||||
sample *= (1 << 6); // Q25 -> Q31
|
||||
|
||||
// Copy ``target_bytes_per_sample`` bytes to the output buffer.
|
||||
for (int i = 0; i < target_bytes_per_sample; ++i) {
|
||||
filtered_data.push_back(static_cast<uint8_t>(sample));
|
||||
sample >>= 8;
|
||||
}
|
||||
audio::pack_q31_as_audio_sample(sample, current_data, target_bytes_per_sample);
|
||||
current_data = current_data + target_bytes_per_sample;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filtered_data;
|
||||
}
|
||||
|
||||
} // namespace microphone
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user