mirror of
https://github.com/esphome/esphome.git
synced 2025-11-02 16:11:53 +00:00
Compare commits
73 Commits
add-heap-t
...
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 |
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -73,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
|
||||
@@ -547,79 +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
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
} 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 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;
|
||||
}
|
||||
// fully sent
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
|
||||
uint8_t header[3];
|
||||
header[0] = 0x01; // indicator
|
||||
@@ -753,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
|
||||
@@ -977,79 +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
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
} 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 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;
|
||||
}
|
||||
// fully sent
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
APIError APIPlaintextFrameHelper::close() {
|
||||
state_ = State::CLOSED;
|
||||
@@ -1067,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_;
|
||||
|
||||
|
||||
@@ -5377,6 +5377,307 @@ void SelectCommandRequest::dump_to(std::string &out) const {
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool ListEntitiesSirenResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 6: {
|
||||
this->disabled_by_default = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 8: {
|
||||
this->supports_duration = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 9: {
|
||||
this->supports_volume = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 10: {
|
||||
this->entity_category = value.as_enum<enums::EntityCategory>();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool ListEntitiesSirenResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->object_id = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 3: {
|
||||
this->name = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 4: {
|
||||
this->unique_id = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 5: {
|
||||
this->icon = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 7: {
|
||||
this->tones.push_back(value.as_string());
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool ListEntitiesSirenResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->key = value.as_fixed32();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->object_id);
|
||||
buffer.encode_fixed32(2, this->key);
|
||||
buffer.encode_string(3, this->name);
|
||||
buffer.encode_string(4, this->unique_id);
|
||||
buffer.encode_string(5, this->icon);
|
||||
buffer.encode_bool(6, this->disabled_by_default);
|
||||
for (auto &it : this->tones) {
|
||||
buffer.encode_string(7, it, true);
|
||||
}
|
||||
buffer.encode_bool(8, this->supports_duration);
|
||||
buffer.encode_bool(9, this->supports_volume);
|
||||
buffer.encode_enum<enums::EntityCategory>(10, this->entity_category);
|
||||
}
|
||||
void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const {
|
||||
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
|
||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||
ProtoSize::add_string_field(total_size, 1, this->name, false);
|
||||
ProtoSize::add_string_field(total_size, 1, this->unique_id, false);
|
||||
ProtoSize::add_string_field(total_size, 1, this->icon, false);
|
||||
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false);
|
||||
if (!this->tones.empty()) {
|
||||
for (const auto &it : this->tones) {
|
||||
ProtoSize::add_string_field(total_size, 1, it, true);
|
||||
}
|
||||
}
|
||||
ProtoSize::add_bool_field(total_size, 1, this->supports_duration, false);
|
||||
ProtoSize::add_bool_field(total_size, 1, this->supports_volume, false);
|
||||
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void ListEntitiesSirenResponse::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("ListEntitiesSirenResponse {\n");
|
||||
out.append(" object_id: ");
|
||||
out.append("'").append(this->object_id).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" key: ");
|
||||
sprintf(buffer, "%" PRIu32, this->key);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" name: ");
|
||||
out.append("'").append(this->name).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" unique_id: ");
|
||||
out.append("'").append(this->unique_id).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" icon: ");
|
||||
out.append("'").append(this->icon).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" disabled_by_default: ");
|
||||
out.append(YESNO(this->disabled_by_default));
|
||||
out.append("\n");
|
||||
|
||||
for (const auto &it : this->tones) {
|
||||
out.append(" tones: ");
|
||||
out.append("'").append(it).append("'");
|
||||
out.append("\n");
|
||||
}
|
||||
|
||||
out.append(" supports_duration: ");
|
||||
out.append(YESNO(this->supports_duration));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" supports_volume: ");
|
||||
out.append(YESNO(this->supports_volume));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" entity_category: ");
|
||||
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool SirenStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->state = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool SirenStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->key = value.as_fixed32();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void SirenStateResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_fixed32(1, this->key);
|
||||
buffer.encode_bool(2, this->state);
|
||||
}
|
||||
void SirenStateResponse::calculate_size(uint32_t &total_size) const {
|
||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||
ProtoSize::add_bool_field(total_size, 1, this->state, false);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void SirenStateResponse::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("SirenStateResponse {\n");
|
||||
out.append(" key: ");
|
||||
sprintf(buffer, "%" PRIu32, this->key);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" state: ");
|
||||
out.append(YESNO(this->state));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->has_state = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 3: {
|
||||
this->state = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 4: {
|
||||
this->has_tone = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 6: {
|
||||
this->has_duration = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 7: {
|
||||
this->duration = value.as_uint32();
|
||||
return true;
|
||||
}
|
||||
case 8: {
|
||||
this->has_volume = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool SirenCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 5: {
|
||||
this->tone = value.as_string();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->key = value.as_fixed32();
|
||||
return true;
|
||||
}
|
||||
case 9: {
|
||||
this->volume = value.as_float();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void SirenCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_fixed32(1, this->key);
|
||||
buffer.encode_bool(2, this->has_state);
|
||||
buffer.encode_bool(3, this->state);
|
||||
buffer.encode_bool(4, this->has_tone);
|
||||
buffer.encode_string(5, this->tone);
|
||||
buffer.encode_bool(6, this->has_duration);
|
||||
buffer.encode_uint32(7, this->duration);
|
||||
buffer.encode_bool(8, this->has_volume);
|
||||
buffer.encode_float(9, this->volume);
|
||||
}
|
||||
void SirenCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||
ProtoSize::add_bool_field(total_size, 1, this->has_state, false);
|
||||
ProtoSize::add_bool_field(total_size, 1, this->state, false);
|
||||
ProtoSize::add_bool_field(total_size, 1, this->has_tone, false);
|
||||
ProtoSize::add_string_field(total_size, 1, this->tone, false);
|
||||
ProtoSize::add_bool_field(total_size, 1, this->has_duration, false);
|
||||
ProtoSize::add_uint32_field(total_size, 1, this->duration, false);
|
||||
ProtoSize::add_bool_field(total_size, 1, this->has_volume, false);
|
||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void SirenCommandRequest::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("SirenCommandRequest {\n");
|
||||
out.append(" key: ");
|
||||
sprintf(buffer, "%" PRIu32, this->key);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" has_state: ");
|
||||
out.append(YESNO(this->has_state));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" state: ");
|
||||
out.append(YESNO(this->state));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" has_tone: ");
|
||||
out.append(YESNO(this->has_tone));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" tone: ");
|
||||
out.append("'").append(this->tone).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" has_duration: ");
|
||||
out.append(YESNO(this->has_duration));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" duration: ");
|
||||
sprintf(buffer, "%" PRIu32, this->duration);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" has_volume: ");
|
||||
out.append(YESNO(this->has_volume));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" volume: ");
|
||||
sprintf(buffer, "%g", this->volume);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool ListEntitiesLockResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 6: {
|
||||
|
||||
@@ -1284,6 +1284,65 @@ class SelectCommandRequest : public ProtoMessage {
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
};
|
||||
class ListEntitiesSirenResponse : public ProtoMessage {
|
||||
public:
|
||||
std::string object_id{};
|
||||
uint32_t key{0};
|
||||
std::string name{};
|
||||
std::string unique_id{};
|
||||
std::string icon{};
|
||||
bool disabled_by_default{false};
|
||||
std::vector<std::string> tones{};
|
||||
bool supports_duration{false};
|
||||
bool supports_volume{false};
|
||||
enums::EntityCategory entity_category{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class SirenStateResponse : public ProtoMessage {
|
||||
public:
|
||||
uint32_t key{0};
|
||||
bool state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class SirenCommandRequest : public ProtoMessage {
|
||||
public:
|
||||
uint32_t key{0};
|
||||
bool has_state{false};
|
||||
bool state{false};
|
||||
bool has_tone{false};
|
||||
std::string tone{};
|
||||
bool has_duration{false};
|
||||
uint32_t duration{0};
|
||||
bool has_volume{false};
|
||||
float volume{0.0f};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class ListEntitiesLockResponse : public ProtoMessage {
|
||||
public:
|
||||
std::string object_id{};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,7 +135,7 @@ 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.
|
||||
/// @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
|
||||
@@ -160,5 +160,28 @@ inline int32_t unpack_audio_sample_to_q31(const uint8_t *data, size_t bytes_per_
|
||||
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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -629,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_;
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -6,12 +6,10 @@ namespace microphone {
|
||||
static const int32_t Q25_MAX_VALUE = (1 << 25) - 1;
|
||||
static const int32_t Q25_MIN_VALUE = ~Q25_MAX_VALUE;
|
||||
|
||||
static const uint32_t HISTORY_VALUES = 32;
|
||||
|
||||
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_) {
|
||||
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>>();
|
||||
@@ -32,13 +30,14 @@ audio::AudioStreamInfo MicrophoneSource::get_audio_stream_info() {
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -63,8 +62,9 @@ void MicrophoneSource::process_audio_(const std::vector<uint8_t> &data, std::vec
|
||||
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();
|
||||
|
||||
filtered_data.reserve(target_bytes_per_frame * total_frames);
|
||||
filtered_data.resize(0);
|
||||
filtered_data.resize(target_bytes_per_frame * total_frames);
|
||||
|
||||
uint8_t *current_data = filtered_data.data();
|
||||
|
||||
for (uint32_t frame_index = 0; frame_index < total_frames; ++frame_index) {
|
||||
for (uint32_t channel_index = 0; channel_index < source_channels; ++channel_index) {
|
||||
@@ -82,26 +82,10 @@ void MicrophoneSource::process_audio_(const std::vector<uint8_t> &data, std::vec
|
||||
// Clamp ``sample`` in case gain multiplication overflows 25 bits
|
||||
sample = clamp<int32_t>(sample, Q25_MIN_VALUE, Q25_MAX_VALUE); // Q25
|
||||
|
||||
// Copy ``target_bytes_per_sample`` bytes to the output buffer.
|
||||
if (target_bytes_per_sample == 1) {
|
||||
sample >>= 18; // Q25 -> Q7
|
||||
filtered_data.push_back(static_cast<uint8_t>(sample));
|
||||
} else if (target_bytes_per_sample == 2) {
|
||||
sample >>= 10; // Q25 -> Q15
|
||||
filtered_data.push_back(static_cast<uint8_t>(sample));
|
||||
filtered_data.push_back(static_cast<uint8_t>(sample >> 8));
|
||||
} else if (target_bytes_per_sample == 3) {
|
||||
sample >>= 2; // Q25 -> Q23
|
||||
filtered_data.push_back(static_cast<uint8_t>(sample));
|
||||
filtered_data.push_back(static_cast<uint8_t>(sample >> 8));
|
||||
filtered_data.push_back(static_cast<uint8_t>(sample >> 16));
|
||||
} else {
|
||||
sample *= (1 << 6); // Q25 -> Q31
|
||||
filtered_data.push_back(static_cast<uint8_t>(sample));
|
||||
filtered_data.push_back(static_cast<uint8_t>(sample >> 8));
|
||||
filtered_data.push_back(static_cast<uint8_t>(sample >> 16));
|
||||
filtered_data.push_back(static_cast<uint8_t>(sample >> 24));
|
||||
}
|
||||
sample *= (1 << 6); // Q25 -> Q31
|
||||
|
||||
audio::pack_q31_as_audio_sample(sample, current_data, target_bytes_per_sample);
|
||||
current_data = current_data + target_bytes_per_sample;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ class MicrophoneSource {
|
||||
* Note that this class cannot convert sample rates!
|
||||
*/
|
||||
public:
|
||||
MicrophoneSource(Microphone *mic, uint8_t bits_per_sample, int32_t gain_factor)
|
||||
: mic_(mic), bits_per_sample_(bits_per_sample), gain_factor_(gain_factor) {}
|
||||
MicrophoneSource(Microphone *mic, uint8_t bits_per_sample, int32_t gain_factor, bool passive)
|
||||
: mic_(mic), bits_per_sample_(bits_per_sample), gain_factor_(gain_factor), passive_(passive) {}
|
||||
|
||||
/// @brief Enables a channel to be processed through the callback.
|
||||
///
|
||||
@@ -59,8 +59,9 @@ class MicrophoneSource {
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
bool is_running() const { return (this->mic_->is_running() && this->enabled_); }
|
||||
bool is_stopped() const { return !this->enabled_; }
|
||||
bool is_passive() const { return this->passive_; }
|
||||
bool is_running() const { return (this->mic_->is_running() && (this->enabled_ || this->passive_)); }
|
||||
bool is_stopped() const { return !this->is_running(); };
|
||||
|
||||
protected:
|
||||
void process_audio_(const std::vector<uint8_t> &data, std::vector<uint8_t> &filtered_data);
|
||||
@@ -72,6 +73,7 @@ class MicrophoneSource {
|
||||
std::bitset<8> channels_;
|
||||
int32_t gain_factor_;
|
||||
bool enabled_{false};
|
||||
bool passive_; // Only pass audio if ``mic_`` is already running
|
||||
};
|
||||
|
||||
} // namespace microphone
|
||||
|
||||
15
esphome/components/mipi_spi/__init__.py
Normal file
15
esphome/components/mipi_spi/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
DOMAIN = "mipi_spi"
|
||||
|
||||
CONF_DRAW_FROM_ORIGIN = "draw_from_origin"
|
||||
CONF_SPI_16 = "spi_16"
|
||||
CONF_PIXEL_MODE = "pixel_mode"
|
||||
CONF_COLOR_DEPTH = "color_depth"
|
||||
CONF_BUS_MODE = "bus_mode"
|
||||
CONF_USE_AXIS_FLIPS = "use_axis_flips"
|
||||
CONF_NATIVE_WIDTH = "native_width"
|
||||
CONF_NATIVE_HEIGHT = "native_height"
|
||||
|
||||
MODE_RGB = "RGB"
|
||||
MODE_BGR = "BGR"
|
||||
474
esphome/components/mipi_spi/display.py
Normal file
474
esphome/components/mipi_spi/display.py
Normal file
@@ -0,0 +1,474 @@
|
||||
import logging
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import ALLOW_EXTRA
|
||||
from esphome.const import (
|
||||
CONF_BRIGHTNESS,
|
||||
CONF_COLOR_ORDER,
|
||||
CONF_CS_PIN,
|
||||
CONF_DATA_RATE,
|
||||
CONF_DC_PIN,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
CONF_INIT_SEQUENCE,
|
||||
CONF_INVERT_COLORS,
|
||||
CONF_LAMBDA,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_MODEL,
|
||||
CONF_OFFSET_HEIGHT,
|
||||
CONF_OFFSET_WIDTH,
|
||||
CONF_RESET_PIN,
|
||||
CONF_ROTATION,
|
||||
CONF_SWAP_XY,
|
||||
CONF_TRANSFORM,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.core import TimePeriod
|
||||
|
||||
from ..const import CONF_DRAW_ROUNDING
|
||||
from ..lvgl.defines import CONF_COLOR_DEPTH
|
||||
from . import (
|
||||
CONF_BUS_MODE,
|
||||
CONF_DRAW_FROM_ORIGIN,
|
||||
CONF_NATIVE_HEIGHT,
|
||||
CONF_NATIVE_WIDTH,
|
||||
CONF_PIXEL_MODE,
|
||||
CONF_SPI_16,
|
||||
CONF_USE_AXIS_FLIPS,
|
||||
DOMAIN,
|
||||
MODE_BGR,
|
||||
MODE_RGB,
|
||||
)
|
||||
from .models import (
|
||||
DELAY_FLAG,
|
||||
MADCTL_BGR,
|
||||
MADCTL_MV,
|
||||
MADCTL_MX,
|
||||
MADCTL_MY,
|
||||
MADCTL_XFLIP,
|
||||
MADCTL_YFLIP,
|
||||
DriverChip,
|
||||
amoled,
|
||||
cyd,
|
||||
ili,
|
||||
jc,
|
||||
lanbon,
|
||||
lilygo,
|
||||
waveshare,
|
||||
)
|
||||
from .models.commands import BRIGHTNESS, DISPON, INVOFF, INVON, MADCTL, PIXFMT, SLPOUT
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
LOGGER = logging.getLogger(DOMAIN)
|
||||
mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi")
|
||||
MipiSpi = mipi_spi_ns.class_(
|
||||
"MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice
|
||||
)
|
||||
ColorOrder = display.display_ns.enum("ColorMode")
|
||||
ColorBitness = display.display_ns.enum("ColorBitness")
|
||||
Model = mipi_spi_ns.enum("Model")
|
||||
|
||||
COLOR_ORDERS = {
|
||||
MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
|
||||
MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
|
||||
}
|
||||
|
||||
COLOR_DEPTHS = {
|
||||
8: ColorBitness.COLOR_BITNESS_332,
|
||||
16: ColorBitness.COLOR_BITNESS_565,
|
||||
}
|
||||
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
|
||||
|
||||
|
||||
DriverChip("CUSTOM", initsequence={})
|
||||
|
||||
MODELS = DriverChip.models
|
||||
# These statements are noops, but serve to suppress linting of side-effect-only imports
|
||||
for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare):
|
||||
pass
|
||||
|
||||
PixelMode = mipi_spi_ns.enum("PixelMode")
|
||||
|
||||
PIXEL_MODE_18BIT = "18bit"
|
||||
PIXEL_MODE_16BIT = "16bit"
|
||||
|
||||
PIXEL_MODES = {
|
||||
PIXEL_MODE_16BIT: 0x55,
|
||||
PIXEL_MODE_18BIT: 0x66,
|
||||
}
|
||||
|
||||
|
||||
def validate_dimension(rounding):
|
||||
def validator(value):
|
||||
value = cv.positive_int(value)
|
||||
if value % rounding != 0:
|
||||
raise cv.Invalid(f"Dimensions and offsets must be divisible by {rounding}")
|
||||
return value
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def map_sequence(value):
|
||||
"""
|
||||
The format is a repeated sequence of [CMD, <data>] where <data> is s a sequence of bytes. The length is inferred
|
||||
from the length of the sequence and should not be explicit.
|
||||
A delay can be inserted by specifying "- delay N" where N is in ms
|
||||
"""
|
||||
if isinstance(value, str) and value.lower().startswith("delay "):
|
||||
value = value.lower()[6:]
|
||||
delay = cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)),
|
||||
)(value)
|
||||
return DELAY_FLAG, delay.total_milliseconds
|
||||
if isinstance(value, int):
|
||||
return (value,)
|
||||
value = cv.All(cv.ensure_list(cv.int_range(0, 255)), cv.Length(1, 254))(value)
|
||||
return tuple(value)
|
||||
|
||||
|
||||
def power_of_two(value):
|
||||
value = cv.int_range(1, 128)(value)
|
||||
if value & (value - 1) != 0:
|
||||
raise cv.Invalid("value must be a power of two")
|
||||
return value
|
||||
|
||||
|
||||
def dimension_schema(rounding):
|
||||
return cv.Any(
|
||||
cv.dimensions,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_WIDTH): validate_dimension(rounding),
|
||||
cv.Required(CONF_HEIGHT): validate_dimension(rounding),
|
||||
cv.Optional(CONF_OFFSET_HEIGHT, default=0): validate_dimension(
|
||||
rounding
|
||||
),
|
||||
cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def model_schema(bus_mode, model: DriverChip, swapsies: bool):
|
||||
transform = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
||||
}
|
||||
)
|
||||
if model.get_default(CONF_SWAP_XY, False) == cv.UNDEFINED:
|
||||
transform = transform.extend(
|
||||
{
|
||||
cv.Optional(CONF_SWAP_XY): cv.invalid(
|
||||
"Axis swapping not supported by this model"
|
||||
)
|
||||
}
|
||||
)
|
||||
else:
|
||||
transform = transform.extend(
|
||||
{
|
||||
cv.Required(CONF_SWAP_XY): cv.boolean,
|
||||
}
|
||||
)
|
||||
# CUSTOM model will need to provide a custom init sequence
|
||||
iseqconf = (
|
||||
cv.Required(CONF_INIT_SEQUENCE)
|
||||
if model.initsequence is None
|
||||
else cv.Optional(CONF_INIT_SEQUENCE)
|
||||
)
|
||||
# Dimensions are optional if the model has a default width and the transform is not overridden
|
||||
cv_dimensions = (
|
||||
cv.Optional if model.get_default(CONF_WIDTH) and not swapsies else cv.Required
|
||||
)
|
||||
pixel_modes = PIXEL_MODES if bus_mode == TYPE_SINGLE else (PIXEL_MODE_16BIT,)
|
||||
color_depth = (
|
||||
("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit")
|
||||
)
|
||||
schema = (
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
spi.spi_device_schema(
|
||||
cs_pin_required=False,
|
||||
default_mode="MODE3" if bus_mode == TYPE_OCTAL else "MODE0",
|
||||
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
|
||||
mode=bus_mode,
|
||||
)
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
model.option(pin, cv.UNDEFINED): pins.gpio_output_pin_schema
|
||||
for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_DC_PIN)
|
||||
}
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(MipiSpi),
|
||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(
|
||||
model.get_default(CONF_DRAW_ROUNDING, 1)
|
||||
),
|
||||
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
|
||||
pins.gpio_output_pin_schema
|
||||
),
|
||||
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(
|
||||
COLOR_ORDERS, upper=True
|
||||
),
|
||||
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
|
||||
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
|
||||
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any(
|
||||
cv.one_of(*pixel_modes, lower=True),
|
||||
cv.int_range(0, 255, min_included=True, max_included=True),
|
||||
),
|
||||
cv.Optional(CONF_TRANSFORM): transform,
|
||||
cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of(
|
||||
bus_mode, lower=True
|
||||
),
|
||||
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
|
||||
iseqconf: cv.ensure_list(map_sequence),
|
||||
}
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
model.option(x): cv.boolean
|
||||
for x in [
|
||||
CONF_DRAW_FROM_ORIGIN,
|
||||
CONF_SPI_16,
|
||||
CONF_INVERT_COLORS,
|
||||
CONF_USE_AXIS_FLIPS,
|
||||
]
|
||||
}
|
||||
)
|
||||
)
|
||||
if brightness := model.get_default(CONF_BRIGHTNESS):
|
||||
schema = schema.extend(
|
||||
{
|
||||
cv.Optional(CONF_BRIGHTNESS, default=brightness): cv.int_range(
|
||||
0, 0xFF, min_included=True, max_included=True
|
||||
),
|
||||
}
|
||||
)
|
||||
if bus_mode != TYPE_SINGLE:
|
||||
return cv.All(schema, cv.only_with_esp_idf)
|
||||
return schema
|
||||
|
||||
|
||||
def rotation_as_transform(model, config):
|
||||
"""
|
||||
Check if a rotation can be implemented in hardware using the MADCTL register.
|
||||
A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y.
|
||||
"""
|
||||
rotation = config.get(CONF_ROTATION, 0)
|
||||
return rotation and (
|
||||
model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180
|
||||
)
|
||||
|
||||
|
||||
def config_schema(config):
|
||||
# First get the model and bus mode
|
||||
config = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True),
|
||||
},
|
||||
extra=ALLOW_EXTRA,
|
||||
)(config)
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
bus_modes = model.modes
|
||||
config = cv.Schema(
|
||||
{
|
||||
model.option(CONF_BUS_MODE, TYPE_SINGLE): cv.one_of(*bus_modes, lower=True),
|
||||
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True),
|
||||
},
|
||||
extra=ALLOW_EXTRA,
|
||||
)(config)
|
||||
bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
|
||||
swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True
|
||||
config = model_schema(bus_mode, model, swapsies)(config)
|
||||
# Check for invalid combinations of MADCTL config
|
||||
if init_sequence := config.get(CONF_INIT_SEQUENCE):
|
||||
if MADCTL in [x[0] for x in init_sequence] and CONF_TRANSFORM in config:
|
||||
raise cv.Invalid(
|
||||
f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence"
|
||||
)
|
||||
|
||||
if bus_mode == TYPE_QUAD and CONF_DC_PIN in config:
|
||||
raise cv.Invalid("DC pin is not supported in quad mode")
|
||||
if config[CONF_PIXEL_MODE] == PIXEL_MODE_18BIT and bus_mode != TYPE_SINGLE:
|
||||
raise cv.Invalid("18-bit pixel mode is not supported on a quad or octal bus")
|
||||
if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config:
|
||||
raise cv.Invalid(f"DC pin is required in {bus_mode} mode")
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = config_schema
|
||||
|
||||
|
||||
def get_transform(model, config):
|
||||
can_transform = rotation_as_transform(model, config)
|
||||
transform = config.get(
|
||||
CONF_TRANSFORM,
|
||||
{
|
||||
CONF_MIRROR_X: model.get_default(CONF_MIRROR_X, False),
|
||||
CONF_MIRROR_Y: model.get_default(CONF_MIRROR_Y, False),
|
||||
CONF_SWAP_XY: model.get_default(CONF_SWAP_XY, False),
|
||||
},
|
||||
)
|
||||
|
||||
# Can we use the MADCTL register to set the rotation?
|
||||
if can_transform and CONF_TRANSFORM not in config:
|
||||
rotation = config[CONF_ROTATION]
|
||||
if rotation == 180:
|
||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||
elif rotation == 90:
|
||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||
else:
|
||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||
transform[CONF_TRANSFORM] = True
|
||||
return transform
|
||||
|
||||
|
||||
def get_sequence(model, config):
|
||||
"""
|
||||
Create the init sequence for the display.
|
||||
Use the default sequence from the model, if any, and append any custom sequence provided in the config.
|
||||
Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence
|
||||
Pixel format, color order, and orientation will be set.
|
||||
"""
|
||||
sequence = list(model.initsequence)
|
||||
custom_sequence = config.get(CONF_INIT_SEQUENCE, [])
|
||||
sequence.extend(custom_sequence)
|
||||
# Ensure each command is a tuple
|
||||
sequence = [x if isinstance(x, tuple) else (x,) for x in sequence]
|
||||
commands = [x[0] for x in sequence]
|
||||
# Set pixel format if not already in the custom sequence
|
||||
if PIXFMT not in commands:
|
||||
pixel_mode = config[CONF_PIXEL_MODE]
|
||||
if not isinstance(pixel_mode, int):
|
||||
pixel_mode = PIXEL_MODES[pixel_mode]
|
||||
sequence.append((PIXFMT, pixel_mode))
|
||||
# Does the chip use the flipping bits for mirroring rather than the reverse order bits?
|
||||
use_flip = config[CONF_USE_AXIS_FLIPS]
|
||||
if MADCTL not in commands:
|
||||
madctl = 0
|
||||
transform = get_transform(model, config)
|
||||
if transform.get(CONF_TRANSFORM):
|
||||
LOGGER.info("Using hardware transform to implement rotation")
|
||||
if transform.get(CONF_MIRROR_X):
|
||||
madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX
|
||||
if transform.get(CONF_MIRROR_Y):
|
||||
madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY
|
||||
if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined
|
||||
madctl |= MADCTL_MV
|
||||
if config[CONF_COLOR_ORDER] == MODE_BGR:
|
||||
madctl |= MADCTL_BGR
|
||||
sequence.append((MADCTL, madctl))
|
||||
if INVON not in commands and INVOFF not in commands:
|
||||
if config[CONF_INVERT_COLORS]:
|
||||
sequence.append((INVON,))
|
||||
else:
|
||||
sequence.append((INVOFF,))
|
||||
if BRIGHTNESS not in commands:
|
||||
if brightness := config.get(
|
||||
CONF_BRIGHTNESS, model.get_default(CONF_BRIGHTNESS)
|
||||
):
|
||||
sequence.append((BRIGHTNESS, brightness))
|
||||
if SLPOUT not in commands:
|
||||
sequence.append((SLPOUT,))
|
||||
sequence.append((DISPON,))
|
||||
|
||||
# Flatten the sequence into a list of bytes, with the length of each command
|
||||
# or the delay flag inserted where needed
|
||||
return sum(
|
||||
tuple(
|
||||
(x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:]
|
||||
for x in sequence
|
||||
),
|
||||
(),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
transform = get_transform(model, config)
|
||||
if CONF_DIMENSIONS in config:
|
||||
# Explicit dimensions, just use as is
|
||||
dimensions = config[CONF_DIMENSIONS]
|
||||
if isinstance(dimensions, dict):
|
||||
width = dimensions[CONF_WIDTH]
|
||||
height = dimensions[CONF_HEIGHT]
|
||||
offset_width = dimensions[CONF_OFFSET_WIDTH]
|
||||
offset_height = dimensions[CONF_OFFSET_HEIGHT]
|
||||
else:
|
||||
(width, height) = dimensions
|
||||
offset_width = 0
|
||||
offset_height = 0
|
||||
else:
|
||||
# Default dimensions, use model defaults and transform if needed
|
||||
width = model.get_default(CONF_WIDTH)
|
||||
height = model.get_default(CONF_HEIGHT)
|
||||
offset_width = model.get_default(CONF_OFFSET_WIDTH, 0)
|
||||
offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0)
|
||||
|
||||
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
|
||||
# the offset is asymmetric
|
||||
if transform[CONF_MIRROR_X]:
|
||||
native_width = model.get_default(
|
||||
CONF_NATIVE_WIDTH, width + offset_width * 2
|
||||
)
|
||||
offset_width = native_width - width - offset_width
|
||||
if transform[CONF_MIRROR_Y]:
|
||||
native_height = model.get_default(
|
||||
CONF_NATIVE_HEIGHT, height + offset_height * 2
|
||||
)
|
||||
offset_height = native_height - height - offset_height
|
||||
# Swap default dimensions if swap_xy is set
|
||||
if transform[CONF_SWAP_XY] is True:
|
||||
width, height = height, width
|
||||
offset_height, offset_width = offset_width, offset_height
|
||||
|
||||
color_depth = config[CONF_COLOR_DEPTH]
|
||||
if color_depth.endswith("bit"):
|
||||
color_depth = color_depth[:-3]
|
||||
color_depth = COLOR_DEPTHS[int(color_depth)]
|
||||
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID], width, height, offset_width, offset_height, color_depth
|
||||
)
|
||||
cg.add(var.set_init_sequence(get_sequence(model, config)))
|
||||
if rotation_as_transform(model, config):
|
||||
if CONF_TRANSFORM in config:
|
||||
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
|
||||
else:
|
||||
config[CONF_ROTATION] = 0
|
||||
cg.add(var.set_model(config[CONF_MODEL]))
|
||||
cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN]))
|
||||
cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING]))
|
||||
cg.add(var.set_spi_16(config[CONF_SPI_16]))
|
||||
if enable_pin := config.get(CONF_ENABLE_PIN):
|
||||
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
|
||||
cg.add(var.set_enable_pins(enable))
|
||||
|
||||
if reset_pin := config.get(CONF_RESET_PIN):
|
||||
reset = await cg.gpio_pin_expression(reset_pin)
|
||||
cg.add(var.set_reset_pin(reset))
|
||||
|
||||
if dc_pin := config.get(CONF_DC_PIN):
|
||||
dc_pin = await cg.gpio_pin_expression(dc_pin)
|
||||
cg.add(var.set_dc_pin(dc_pin))
|
||||
|
||||
if lamb := config.get(CONF_LAMBDA):
|
||||
lambda_ = await cg.process_lambda(
|
||||
lamb, [(display.DisplayRef, "it")], return_type=cg.void
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
481
esphome/components/mipi_spi/mipi_spi.cpp
Normal file
481
esphome/components/mipi_spi/mipi_spi.cpp
Normal file
@@ -0,0 +1,481 @@
|
||||
#include "mipi_spi.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace mipi_spi {
|
||||
|
||||
void MipiSpi::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up MIPI SPI");
|
||||
this->spi_setup();
|
||||
if (this->dc_pin_ != nullptr) {
|
||||
this->dc_pin_->setup();
|
||||
this->dc_pin_->digital_write(false);
|
||||
}
|
||||
for (auto *pin : this->enable_pins_) {
|
||||
pin->setup();
|
||||
pin->digital_write(true);
|
||||
}
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
this->bus_width_ = this->parent_->get_bus_width();
|
||||
|
||||
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
|
||||
auto when = millis() + 120;
|
||||
delay(10);
|
||||
size_t index = 0;
|
||||
auto &vec = this->init_sequence_;
|
||||
while (index != vec.size()) {
|
||||
if (vec.size() - index < 2) {
|
||||
ESP_LOGE(TAG, "Malformed init sequence");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint8_t cmd = vec[index++];
|
||||
uint8_t x = vec[index++];
|
||||
if (x == DELAY_FLAG) {
|
||||
ESP_LOGD(TAG, "Delay %dms", cmd);
|
||||
delay(cmd);
|
||||
} else {
|
||||
uint8_t num_args = x & 0x7F;
|
||||
if (vec.size() - index < num_args) {
|
||||
ESP_LOGE(TAG, "Malformed init sequence");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
auto arg_byte = vec[index];
|
||||
switch (cmd) {
|
||||
case SLEEP_OUT: {
|
||||
// are we ready, boots?
|
||||
int duration = when - millis();
|
||||
if (duration > 0) {
|
||||
ESP_LOGD(TAG, "Sleep %dms", duration);
|
||||
delay(duration);
|
||||
}
|
||||
} break;
|
||||
|
||||
case INVERT_ON:
|
||||
this->invert_colors_ = true;
|
||||
break;
|
||||
case MADCTL_CMD:
|
||||
this->madctl_ = arg_byte;
|
||||
break;
|
||||
case PIXFMT:
|
||||
this->pixel_mode_ = arg_byte & 0x11 ? PIXEL_MODE_16 : PIXEL_MODE_18;
|
||||
break;
|
||||
case BRIGHTNESS:
|
||||
this->brightness_ = arg_byte;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const auto *ptr = vec.data() + index;
|
||||
ESP_LOGD(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
|
||||
this->write_command_(cmd, ptr, num_args);
|
||||
index += num_args;
|
||||
if (cmd == SLEEP_OUT)
|
||||
delay(10);
|
||||
}
|
||||
}
|
||||
this->setup_complete_ = true;
|
||||
if (this->draw_from_origin_)
|
||||
check_buffer_();
|
||||
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
|
||||
}
|
||||
|
||||
void MipiSpi::update() {
|
||||
if (!this->setup_complete_ || this->is_failed()) {
|
||||
return;
|
||||
}
|
||||
this->do_update_();
|
||||
if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
|
||||
return;
|
||||
ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_);
|
||||
// Some chips require that the drawing window be aligned on certain boundaries
|
||||
auto dr = this->draw_rounding_;
|
||||
this->x_low_ = this->x_low_ / dr * dr;
|
||||
this->y_low_ = this->y_low_ / dr * dr;
|
||||
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
|
||||
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
|
||||
if (this->draw_from_origin_) {
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_ - 1;
|
||||
}
|
||||
int w = this->x_high_ - this->x_low_ + 1;
|
||||
int h = this->y_high_ - this->y_low_ + 1;
|
||||
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_,
|
||||
this->width_ - w - this->x_low_);
|
||||
// invalidate watermarks
|
||||
this->x_low_ = this->width_;
|
||||
this->y_low_ = this->height_;
|
||||
this->x_high_ = 0;
|
||||
this->y_high_ = 0;
|
||||
}
|
||||
|
||||
void MipiSpi::fill(Color color) {
|
||||
if (!this->check_buffer_())
|
||||
return;
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->get_width_internal() - 1;
|
||||
this->y_high_ = this->get_height_internal() - 1;
|
||||
switch (this->color_depth_) {
|
||||
case display::COLOR_BITNESS_332: {
|
||||
auto new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
|
||||
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
auto new_color = display::ColorUtil::color_to_565(color);
|
||||
if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) {
|
||||
// Upper and lower is equal can use quicker memset operation. Takes ~20ms.
|
||||
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
|
||||
} else {
|
||||
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
|
||||
auto len = this->buffer_bytes_ / 2;
|
||||
while (len--) {
|
||||
*ptr_16++ = new_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MipiSpi::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
|
||||
return;
|
||||
}
|
||||
if (!this->check_buffer_())
|
||||
return;
|
||||
size_t pos = (y * this->width_) + x;
|
||||
switch (this->color_depth_) {
|
||||
case display::COLOR_BITNESS_332: {
|
||||
uint8_t new_color = display::ColorUtil::color_to_332(color);
|
||||
if (this->buffer_[pos] == new_color)
|
||||
return;
|
||||
this->buffer_[pos] = new_color;
|
||||
break;
|
||||
}
|
||||
|
||||
case display::COLOR_BITNESS_565: {
|
||||
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
|
||||
uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5);
|
||||
uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3);
|
||||
uint16_t new_color = hi_byte | (lo_byte << 8); // big endian
|
||||
if (ptr_16[pos] == new_color)
|
||||
return;
|
||||
ptr_16[pos] = new_color;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
// low and high watermark may speed up drawing from buffer
|
||||
if (x < this->x_low_)
|
||||
this->x_low_ = x;
|
||||
if (y < this->y_low_)
|
||||
this->y_low_ = y;
|
||||
if (x > this->x_high_)
|
||||
this->x_high_ = x;
|
||||
if (y > this->y_high_)
|
||||
this->y_high_ = y;
|
||||
}
|
||||
|
||||
void MipiSpi::reset_params_() {
|
||||
if (!this->is_ready())
|
||||
return;
|
||||
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
||||
if (this->brightness_.has_value())
|
||||
this->write_command_(BRIGHTNESS, this->brightness_.value());
|
||||
}
|
||||
|
||||
void MipiSpi::write_init_sequence_() {
|
||||
size_t index = 0;
|
||||
auto &vec = this->init_sequence_;
|
||||
while (index != vec.size()) {
|
||||
if (vec.size() - index < 2) {
|
||||
ESP_LOGE(TAG, "Malformed init sequence");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint8_t cmd = vec[index++];
|
||||
uint8_t x = vec[index++];
|
||||
if (x == DELAY_FLAG) {
|
||||
ESP_LOGV(TAG, "Delay %dms", cmd);
|
||||
delay(cmd);
|
||||
} else {
|
||||
uint8_t num_args = x & 0x7F;
|
||||
if (vec.size() - index < num_args) {
|
||||
ESP_LOGE(TAG, "Malformed init sequence");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
const auto *ptr = vec.data() + index;
|
||||
this->write_command_(cmd, ptr, num_args);
|
||||
index += num_args;
|
||||
}
|
||||
}
|
||||
this->setup_complete_ = true;
|
||||
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
|
||||
}
|
||||
|
||||
void MipiSpi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
|
||||
ESP_LOGVV(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
|
||||
uint8_t buf[4];
|
||||
x1 += this->offset_width_;
|
||||
x2 += this->offset_width_;
|
||||
y1 += this->offset_height_;
|
||||
y2 += this->offset_height_;
|
||||
put16_be(buf, y1);
|
||||
put16_be(buf + 2, y2);
|
||||
this->write_command_(RASET, buf, sizeof buf);
|
||||
put16_be(buf, x1);
|
||||
put16_be(buf + 2, x2);
|
||||
this->write_command_(CASET, buf, sizeof buf);
|
||||
}
|
||||
|
||||
void MipiSpi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
if (!this->setup_complete_ || this->is_failed())
|
||||
return;
|
||||
if (w <= 0 || h <= 0)
|
||||
return;
|
||||
if (bitness != this->color_depth_ || big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) {
|
||||
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
return;
|
||||
}
|
||||
if (this->draw_from_origin_) {
|
||||
auto stride = x_offset + w + x_pad;
|
||||
for (int y = 0; y != h; y++) {
|
||||
memcpy(this->buffer_ + ((y + y_start) * this->width_ + x_start) * 2,
|
||||
ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2);
|
||||
}
|
||||
ptr = this->buffer_;
|
||||
w = this->width_;
|
||||
h += y_start;
|
||||
x_start = 0;
|
||||
y_start = 0;
|
||||
x_offset = 0;
|
||||
y_offset = 0;
|
||||
}
|
||||
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
|
||||
}
|
||||
|
||||
void MipiSpi::write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride) {
|
||||
stride -= w;
|
||||
uint8_t transfer_buffer[6 * 256];
|
||||
size_t idx = 0; // index into transfer_buffer
|
||||
while (h-- != 0) {
|
||||
for (auto x = w; x-- != 0;) {
|
||||
auto color_val = *ptr++;
|
||||
// deal with byte swapping
|
||||
transfer_buffer[idx++] = (color_val & 0xF8); // Blue
|
||||
transfer_buffer[idx++] = ((color_val & 0x7) << 5) | ((color_val & 0xE000) >> 11); // Green
|
||||
transfer_buffer[idx++] = (color_val >> 5) & 0xF8; // Red
|
||||
if (idx == sizeof(transfer_buffer)) {
|
||||
this->write_array(transfer_buffer, idx);
|
||||
idx = 0;
|
||||
}
|
||||
}
|
||||
ptr += stride;
|
||||
}
|
||||
if (idx != 0)
|
||||
this->write_array(transfer_buffer, idx);
|
||||
}
|
||||
|
||||
void MipiSpi::write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
|
||||
stride -= w;
|
||||
uint8_t transfer_buffer[6 * 256];
|
||||
size_t idx = 0; // index into transfer_buffer
|
||||
while (h-- != 0) {
|
||||
for (auto x = w; x-- != 0;) {
|
||||
auto color_val = *ptr++;
|
||||
transfer_buffer[idx++] = color_val & 0xE0; // Red
|
||||
transfer_buffer[idx++] = (color_val << 3) & 0xE0; // Green
|
||||
transfer_buffer[idx++] = color_val << 6; // Blue
|
||||
if (idx == sizeof(transfer_buffer)) {
|
||||
this->write_array(transfer_buffer, idx);
|
||||
idx = 0;
|
||||
}
|
||||
}
|
||||
ptr += stride;
|
||||
}
|
||||
if (idx != 0)
|
||||
this->write_array(transfer_buffer, idx);
|
||||
}
|
||||
|
||||
void MipiSpi::write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
|
||||
stride -= w;
|
||||
uint8_t transfer_buffer[6 * 256];
|
||||
size_t idx = 0; // index into transfer_buffer
|
||||
while (h-- != 0) {
|
||||
for (auto x = w; x-- != 0;) {
|
||||
auto color_val = *ptr++;
|
||||
transfer_buffer[idx++] = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
|
||||
transfer_buffer[idx++] = (color_val & 0x3) << 3;
|
||||
if (idx == sizeof(transfer_buffer)) {
|
||||
this->write_array(transfer_buffer, idx);
|
||||
idx = 0;
|
||||
}
|
||||
}
|
||||
ptr += stride;
|
||||
}
|
||||
if (idx != 0)
|
||||
this->write_array(transfer_buffer, idx);
|
||||
}
|
||||
|
||||
void MipiSpi::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
|
||||
int x_pad) {
|
||||
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
|
||||
auto stride = x_offset + w + x_pad;
|
||||
const auto *offset_ptr = ptr;
|
||||
if (this->color_depth_ == display::COLOR_BITNESS_332) {
|
||||
offset_ptr += y_offset * stride + x_offset;
|
||||
} else {
|
||||
stride *= 2;
|
||||
offset_ptr += y_offset * stride + x_offset * 2;
|
||||
}
|
||||
|
||||
switch (this->bus_width_) {
|
||||
case 4:
|
||||
this->enable();
|
||||
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
|
||||
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't
|
||||
// bother
|
||||
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h * 2, 4);
|
||||
} else {
|
||||
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, nullptr, 0, 4);
|
||||
for (int y = 0; y != h; y++) {
|
||||
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 4);
|
||||
offset_ptr += stride;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 8:
|
||||
this->write_command_(WDATA);
|
||||
this->enable();
|
||||
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
|
||||
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h * 2, 8);
|
||||
} else {
|
||||
for (int y = 0; y != h; y++) {
|
||||
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 8);
|
||||
offset_ptr += stride;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
this->write_command_(WDATA);
|
||||
this->enable();
|
||||
|
||||
if (this->color_depth_ == display::COLOR_BITNESS_565) {
|
||||
// Source buffer is 16-bit RGB565
|
||||
if (this->pixel_mode_ == PIXEL_MODE_18) {
|
||||
// Convert RGB565 to RGB666
|
||||
this->write_18_from_16_bit_(reinterpret_cast<const uint16_t *>(offset_ptr), w, h, stride / 2);
|
||||
} else {
|
||||
// Direct RGB565 output
|
||||
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
|
||||
this->write_array(ptr, w * h * 2);
|
||||
} else {
|
||||
for (int y = 0; y != h; y++) {
|
||||
this->write_array(offset_ptr, w * 2);
|
||||
offset_ptr += stride;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Source buffer is 8-bit RGB332
|
||||
if (this->pixel_mode_ == PIXEL_MODE_18) {
|
||||
// Convert RGB332 to RGB666
|
||||
this->write_18_from_8_bit_(offset_ptr, w, h, stride);
|
||||
} else {
|
||||
this->write_16_from_8_bit_(offset_ptr, w, h, stride);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void MipiSpi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
|
||||
ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
|
||||
if (this->bus_width_ == 4) {
|
||||
this->enable();
|
||||
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
|
||||
this->disable();
|
||||
} else if (this->bus_width_ == 8) {
|
||||
this->dc_pin_->digital_write(false);
|
||||
this->enable();
|
||||
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
|
||||
this->disable();
|
||||
this->dc_pin_->digital_write(true);
|
||||
if (len != 0) {
|
||||
this->enable();
|
||||
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
|
||||
this->disable();
|
||||
}
|
||||
} else {
|
||||
this->dc_pin_->digital_write(false);
|
||||
this->enable();
|
||||
this->write_byte(cmd);
|
||||
this->disable();
|
||||
this->dc_pin_->digital_write(true);
|
||||
if (len != 0) {
|
||||
if (this->spi_16_) {
|
||||
for (size_t i = 0; i != len; i++) {
|
||||
this->enable();
|
||||
this->write_byte(0);
|
||||
this->write_byte(bytes[i]);
|
||||
this->disable();
|
||||
}
|
||||
} else {
|
||||
this->enable();
|
||||
this->write_array(bytes, len);
|
||||
this->disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MipiSpi::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MIPI_SPI Display");
|
||||
ESP_LOGCONFIG(TAG, " Model: %s", this->model_);
|
||||
ESP_LOGCONFIG(TAG, " Width: %u", this->width_);
|
||||
ESP_LOGCONFIG(TAG, " Height: %u", this->height_);
|
||||
if (this->offset_width_ != 0)
|
||||
ESP_LOGCONFIG(TAG, " Offset width: %u", this->offset_width_);
|
||||
if (this->offset_height_ != 0)
|
||||
ESP_LOGCONFIG(TAG, " Offset height: %u", this->offset_height_);
|
||||
ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->madctl_ & MADCTL_MV));
|
||||
ESP_LOGCONFIG(TAG, " Mirror X: %s", YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)));
|
||||
ESP_LOGCONFIG(TAG, " Mirror Y: %s", YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)));
|
||||
ESP_LOGCONFIG(TAG, " Color depth: %d bits", this->color_depth_ == display::COLOR_BITNESS_565 ? 16 : 8);
|
||||
ESP_LOGCONFIG(TAG, " Invert colors: %s", YESNO(this->invert_colors_));
|
||||
ESP_LOGCONFIG(TAG, " Color order: %s", this->madctl_ & MADCTL_BGR ? "BGR" : "RGB");
|
||||
ESP_LOGCONFIG(TAG, " Pixel mode: %s", this->pixel_mode_ == PIXEL_MODE_18 ? "18bit" : "16bit");
|
||||
if (this->brightness_.has_value())
|
||||
ESP_LOGCONFIG(TAG, " Brightness: %u", this->brightness_.value());
|
||||
if (this->spi_16_)
|
||||
ESP_LOGCONFIG(TAG, " SPI 16bit: YES");
|
||||
ESP_LOGCONFIG(TAG, " Draw rounding: %u", this->draw_rounding_);
|
||||
if (this->draw_from_origin_)
|
||||
ESP_LOGCONFIG(TAG, " Draw from origin: YES");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
ESP_LOGCONFIG(TAG, " SPI Mode: %d", this->mode_);
|
||||
ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", static_cast<unsigned>(this->data_rate_ / 1000000));
|
||||
ESP_LOGCONFIG(TAG, " SPI Bus width: %d", this->bus_width_);
|
||||
}
|
||||
|
||||
} // namespace mipi_spi
|
||||
} // namespace esphome
|
||||
171
esphome/components/mipi_spi/mipi_spi.h
Normal file
171
esphome/components/mipi_spi/mipi_spi.h
Normal file
@@ -0,0 +1,171 @@
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace mipi_spi {
|
||||
|
||||
constexpr static const char *const TAG = "display.mipi_spi";
|
||||
static const uint8_t SW_RESET_CMD = 0x01;
|
||||
static const uint8_t SLEEP_OUT = 0x11;
|
||||
static const uint8_t NORON = 0x13;
|
||||
static const uint8_t INVERT_OFF = 0x20;
|
||||
static const uint8_t INVERT_ON = 0x21;
|
||||
static const uint8_t ALL_ON = 0x23;
|
||||
static const uint8_t WRAM = 0x24;
|
||||
static const uint8_t MIPI = 0x26;
|
||||
static const uint8_t DISPLAY_ON = 0x29;
|
||||
static const uint8_t RASET = 0x2B;
|
||||
static const uint8_t CASET = 0x2A;
|
||||
static const uint8_t WDATA = 0x2C;
|
||||
static const uint8_t TEON = 0x35;
|
||||
static const uint8_t MADCTL_CMD = 0x36;
|
||||
static const uint8_t PIXFMT = 0x3A;
|
||||
static const uint8_t BRIGHTNESS = 0x51;
|
||||
static const uint8_t SWIRE1 = 0x5A;
|
||||
static const uint8_t SWIRE2 = 0x5B;
|
||||
static const uint8_t PAGESEL = 0xFE;
|
||||
|
||||
static const uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
|
||||
static const uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
|
||||
static const uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
|
||||
static const uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
|
||||
static const uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
|
||||
static const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
|
||||
static const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
||||
|
||||
static const uint8_t DELAY_FLAG = 0xFF;
|
||||
// store a 16 bit value in a buffer, big endian.
|
||||
static inline void put16_be(uint8_t *buf, uint16_t value) {
|
||||
buf[0] = value >> 8;
|
||||
buf[1] = value;
|
||||
}
|
||||
|
||||
enum PixelMode {
|
||||
PIXEL_MODE_16,
|
||||
PIXEL_MODE_18,
|
||||
};
|
||||
|
||||
class MipiSpi : public display::DisplayBuffer,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
spi::DATA_RATE_1MHZ> {
|
||||
public:
|
||||
MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth)
|
||||
: width_(width),
|
||||
height_(height),
|
||||
offset_width_(offset_width),
|
||||
offset_height_(offset_height),
|
||||
color_depth_(color_depth) {}
|
||||
void set_model(const char *model) { this->model_ = model; }
|
||||
void update() override;
|
||||
void setup() override;
|
||||
display::ColorOrder get_color_mode() {
|
||||
return this->madctl_ & MADCTL_BGR ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB;
|
||||
}
|
||||
|
||||
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); }
|
||||
void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; }
|
||||
void set_invert_colors(bool invert_colors) {
|
||||
this->invert_colors_ = invert_colors;
|
||||
this->reset_params_();
|
||||
}
|
||||
void set_brightness(uint8_t brightness) {
|
||||
this->brightness_ = brightness;
|
||||
this->reset_params_();
|
||||
}
|
||||
|
||||
void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; }
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
void dump_config() override;
|
||||
|
||||
int get_width_internal() override { return this->width_; }
|
||||
int get_height_internal() override { return this->height_; }
|
||||
bool can_proceed() override { return this->setup_complete_; }
|
||||
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
|
||||
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
|
||||
void set_spi_16(bool spi_16) { this->spi_16_ = spi_16; }
|
||||
|
||||
protected:
|
||||
bool check_buffer_() {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
if (this->buffer_ != nullptr)
|
||||
return true;
|
||||
auto bytes_per_pixel = this->color_depth_ == display::COLOR_BITNESS_565 ? 2 : 1;
|
||||
this->init_internal_(this->width_ * this->height_ * bytes_per_pixel);
|
||||
if (this->buffer_ == nullptr) {
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
this->buffer_bytes_ = this->width_ * this->height_ * bytes_per_pixel;
|
||||
return true;
|
||||
}
|
||||
void fill(Color color) override;
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
void write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride);
|
||||
void write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
|
||||
void write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
|
||||
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
|
||||
int x_pad);
|
||||
/**
|
||||
* the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the
|
||||
* sample code.)
|
||||
*
|
||||
* Immediately after enabling /CS send 4 bytes in single-dataline SPI mode:
|
||||
* 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be
|
||||
* sent in 1-dataline SPI. The second indicates quad mode.
|
||||
* 1: 0x00
|
||||
* 2: The command (register address) byte.
|
||||
* 3: 0x00
|
||||
*
|
||||
* This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte.
|
||||
* At the conclusion of the write, de-assert /CS.
|
||||
*
|
||||
* @param cmd
|
||||
* @param bytes
|
||||
* @param len
|
||||
*/
|
||||
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len);
|
||||
|
||||
void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); }
|
||||
void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); }
|
||||
void reset_params_();
|
||||
void write_init_sequence_();
|
||||
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
|
||||
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
std::vector<GPIOPin *> enable_pins_{};
|
||||
GPIOPin *dc_pin_{nullptr};
|
||||
uint16_t x_low_{1};
|
||||
uint16_t y_low_{1};
|
||||
uint16_t x_high_{0};
|
||||
uint16_t y_high_{0};
|
||||
bool setup_complete_{};
|
||||
|
||||
bool invert_colors_{};
|
||||
size_t width_;
|
||||
size_t height_;
|
||||
int16_t offset_width_;
|
||||
int16_t offset_height_;
|
||||
size_t buffer_bytes_{0};
|
||||
display::ColorBitness color_depth_;
|
||||
PixelMode pixel_mode_{PIXEL_MODE_16};
|
||||
uint8_t bus_width_{};
|
||||
bool spi_16_{};
|
||||
uint8_t madctl_{};
|
||||
bool draw_from_origin_{false};
|
||||
unsigned draw_rounding_{2};
|
||||
optional<uint8_t> brightness_{};
|
||||
const char *model_{"Unknown"};
|
||||
std::vector<uint8_t> init_sequence_{};
|
||||
};
|
||||
} // namespace mipi_spi
|
||||
} // namespace esphome
|
||||
65
esphome/components/mipi_spi/models/__init__.py
Normal file
65
esphome/components/mipi_spi/models/__init__.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_HEIGHT, CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, CONF_WIDTH
|
||||
|
||||
from .. import CONF_NATIVE_HEIGHT, CONF_NATIVE_WIDTH
|
||||
|
||||
MADCTL_MY = 0x80 # Bit 7 Bottom to top
|
||||
MADCTL_MX = 0x40 # Bit 6 Right to left
|
||||
MADCTL_MV = 0x20 # Bit 5 Reverse Mode
|
||||
MADCTL_ML = 0x10 # Bit 4 LCD refresh Bottom to top
|
||||
MADCTL_RGB = 0x00 # Bit 3 Red-Green-Blue pixel order
|
||||
MADCTL_BGR = 0x08 # Bit 3 Blue-Green-Red pixel order
|
||||
MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left
|
||||
|
||||
# These bits are used instead of the above bits on some chips, where using MX and MY results in incorrect
|
||||
# partial updates.
|
||||
MADCTL_XFLIP = 0x02 # Mirror the display horizontally
|
||||
MADCTL_YFLIP = 0x01 # Mirror the display vertically
|
||||
|
||||
DELAY_FLAG = 0xFFF # Special flag to indicate a delay
|
||||
|
||||
|
||||
def delay(ms):
|
||||
return DELAY_FLAG, ms
|
||||
|
||||
|
||||
class DriverChip:
|
||||
models = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
modes=(TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL),
|
||||
initsequence=None,
|
||||
**defaults,
|
||||
):
|
||||
name = name.upper()
|
||||
self.name = name
|
||||
self.modes = modes
|
||||
self.initsequence = initsequence
|
||||
self.defaults = defaults
|
||||
DriverChip.models[name] = self
|
||||
|
||||
def extend(self, name, **kwargs):
|
||||
defaults = self.defaults.copy()
|
||||
if (
|
||||
CONF_WIDTH in defaults
|
||||
and CONF_OFFSET_WIDTH in kwargs
|
||||
and CONF_NATIVE_WIDTH not in defaults
|
||||
):
|
||||
defaults[CONF_NATIVE_WIDTH] = defaults[CONF_WIDTH]
|
||||
if (
|
||||
CONF_HEIGHT in defaults
|
||||
and CONF_OFFSET_HEIGHT in kwargs
|
||||
and CONF_NATIVE_HEIGHT not in defaults
|
||||
):
|
||||
defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT]
|
||||
defaults.update(kwargs)
|
||||
return DriverChip(name, self.modes, initsequence=self.initsequence, **defaults)
|
||||
|
||||
def get_default(self, key, fallback=False):
|
||||
return self.defaults.get(key, fallback)
|
||||
|
||||
def option(self, name, fallback=False):
|
||||
return cv.Optional(name, default=self.get_default(name, fallback))
|
||||
72
esphome/components/mipi_spi/models/amoled.py
Normal file
72
esphome/components/mipi_spi/models/amoled.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from esphome.components.spi import TYPE_QUAD
|
||||
|
||||
from .. import MODE_RGB
|
||||
from . import DriverChip, delay
|
||||
from .commands import MIPI, NORON, PAGESEL, PIXFMT, SLPOUT, SWIRE1, SWIRE2, TEON, WRAM
|
||||
|
||||
DriverChip(
|
||||
"T-DISPLAY-S3-AMOLED",
|
||||
width=240,
|
||||
height=536,
|
||||
cs_pin=6,
|
||||
reset_pin=17,
|
||||
enable_pin=38,
|
||||
bus_mode=TYPE_QUAD,
|
||||
brightness=0xD0,
|
||||
color_order=MODE_RGB,
|
||||
initsequence=(SLPOUT,), # Requires early SLPOUT
|
||||
)
|
||||
|
||||
DriverChip(
|
||||
name="T-DISPLAY-S3-AMOLED-PLUS",
|
||||
width=240,
|
||||
height=536,
|
||||
cs_pin=6,
|
||||
reset_pin=17,
|
||||
dc_pin=7,
|
||||
enable_pin=38,
|
||||
data_rate="40MHz",
|
||||
brightness=0xD0,
|
||||
color_order=MODE_RGB,
|
||||
initsequence=(
|
||||
(PAGESEL, 4),
|
||||
(0x6A, 0x00),
|
||||
(PAGESEL, 0x05),
|
||||
(PAGESEL, 0x07),
|
||||
(0x07, 0x4F),
|
||||
(PAGESEL, 0x01),
|
||||
(0x2A, 0x02),
|
||||
(0x2B, 0x73),
|
||||
(PAGESEL, 0x0A),
|
||||
(0x29, 0x10),
|
||||
(PAGESEL, 0x00),
|
||||
(0x53, 0x20),
|
||||
(TEON, 0x00),
|
||||
(PIXFMT, 0x75),
|
||||
(0xC4, 0x80),
|
||||
),
|
||||
)
|
||||
|
||||
RM690B0 = DriverChip(
|
||||
"RM690B0",
|
||||
brightness=0xD0,
|
||||
color_order=MODE_RGB,
|
||||
width=480,
|
||||
height=600,
|
||||
initsequence=(
|
||||
(PAGESEL, 0x20),
|
||||
(MIPI, 0x0A),
|
||||
(WRAM, 0x80),
|
||||
(SWIRE1, 0x51),
|
||||
(SWIRE2, 0x2E),
|
||||
(PAGESEL, 0x00),
|
||||
(0xC2, 0x00),
|
||||
delay(10),
|
||||
(TEON, 0x00),
|
||||
(NORON,),
|
||||
),
|
||||
)
|
||||
|
||||
T4_S3_AMOLED = RM690B0.extend("T4-S3", width=450, offset_width=16, bus_mode=TYPE_QUAD)
|
||||
|
||||
models = {}
|
||||
82
esphome/components/mipi_spi/models/commands.py
Normal file
82
esphome/components/mipi_spi/models/commands.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# MIPI DBI commands
|
||||
|
||||
NOP = 0x00
|
||||
SWRESET = 0x01
|
||||
RDDID = 0x04
|
||||
RDDST = 0x09
|
||||
RDMODE = 0x0A
|
||||
RDMADCTL = 0x0B
|
||||
RDPIXFMT = 0x0C
|
||||
RDIMGFMT = 0x0D
|
||||
RDSELFDIAG = 0x0F
|
||||
SLEEP_IN = 0x10
|
||||
SLPIN = 0x10
|
||||
SLEEP_OUT = 0x11
|
||||
SLPOUT = 0x11
|
||||
PTLON = 0x12
|
||||
NORON = 0x13
|
||||
INVERT_OFF = 0x20
|
||||
INVOFF = 0x20
|
||||
INVERT_ON = 0x21
|
||||
INVON = 0x21
|
||||
ALL_ON = 0x23
|
||||
WRAM = 0x24
|
||||
GAMMASET = 0x26
|
||||
MIPI = 0x26
|
||||
DISPOFF = 0x28
|
||||
DISPON = 0x29
|
||||
CASET = 0x2A
|
||||
PASET = 0x2B
|
||||
RASET = 0x2B
|
||||
RAMWR = 0x2C
|
||||
WDATA = 0x2C
|
||||
RAMRD = 0x2E
|
||||
PTLAR = 0x30
|
||||
VSCRDEF = 0x33
|
||||
TEON = 0x35
|
||||
MADCTL = 0x36
|
||||
MADCTL_CMD = 0x36
|
||||
VSCRSADD = 0x37
|
||||
IDMOFF = 0x38
|
||||
IDMON = 0x39
|
||||
COLMOD = 0x3A
|
||||
PIXFMT = 0x3A
|
||||
GETSCANLINE = 0x45
|
||||
BRIGHTNESS = 0x51
|
||||
WRDISBV = 0x51
|
||||
RDDISBV = 0x52
|
||||
WRCTRLD = 0x53
|
||||
SWIRE1 = 0x5A
|
||||
SWIRE2 = 0x5B
|
||||
IFMODE = 0xB0
|
||||
FRMCTR1 = 0xB1
|
||||
FRMCTR2 = 0xB2
|
||||
FRMCTR3 = 0xB3
|
||||
INVCTR = 0xB4
|
||||
DFUNCTR = 0xB6
|
||||
ETMOD = 0xB7
|
||||
PWCTR1 = 0xC0
|
||||
PWCTR2 = 0xC1
|
||||
PWCTR3 = 0xC2
|
||||
PWCTR4 = 0xC3
|
||||
PWCTR5 = 0xC4
|
||||
VMCTR1 = 0xC5
|
||||
IFCTR = 0xC6
|
||||
VMCTR2 = 0xC7
|
||||
GMCTR = 0xC8
|
||||
SETEXTC = 0xC8
|
||||
PWSET = 0xD0
|
||||
VMCTR = 0xD1
|
||||
PWSETN = 0xD2
|
||||
RDID4 = 0xD3
|
||||
RDINDEX = 0xD9
|
||||
RDID1 = 0xDA
|
||||
RDID2 = 0xDB
|
||||
RDID3 = 0xDC
|
||||
RDIDX = 0xDD
|
||||
GMCTRP1 = 0xE0
|
||||
GMCTRN1 = 0xE1
|
||||
CSCON = 0xF0
|
||||
PWCTR6 = 0xF6
|
||||
ADJCTL3 = 0xF7
|
||||
PAGESEL = 0xFE
|
||||
10
esphome/components/mipi_spi/models/cyd.py
Normal file
10
esphome/components/mipi_spi/models/cyd.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from .ili import ILI9341
|
||||
|
||||
ILI9341.extend(
|
||||
"ESP32-2432S028",
|
||||
data_rate="40MHz",
|
||||
cs_pin=15,
|
||||
dc_pin=2,
|
||||
)
|
||||
|
||||
models = {}
|
||||
749
esphome/components/mipi_spi/models/ili.py
Normal file
749
esphome/components/mipi_spi/models/ili.py
Normal file
@@ -0,0 +1,749 @@
|
||||
from esphome.components.spi import TYPE_OCTAL
|
||||
|
||||
from .. import MODE_RGB
|
||||
from . import DriverChip, delay
|
||||
from .commands import (
|
||||
ADJCTL3,
|
||||
CSCON,
|
||||
DFUNCTR,
|
||||
ETMOD,
|
||||
FRMCTR1,
|
||||
FRMCTR2,
|
||||
FRMCTR3,
|
||||
GAMMASET,
|
||||
GMCTR,
|
||||
GMCTRN1,
|
||||
GMCTRP1,
|
||||
IDMOFF,
|
||||
IFCTR,
|
||||
IFMODE,
|
||||
INVCTR,
|
||||
NORON,
|
||||
PWCTR1,
|
||||
PWCTR2,
|
||||
PWCTR3,
|
||||
PWCTR4,
|
||||
PWCTR5,
|
||||
PWSET,
|
||||
PWSETN,
|
||||
SETEXTC,
|
||||
SWRESET,
|
||||
VMCTR,
|
||||
VMCTR1,
|
||||
VMCTR2,
|
||||
VSCRSADD,
|
||||
)
|
||||
|
||||
DriverChip(
|
||||
"M5CORE",
|
||||
width=320,
|
||||
height=240,
|
||||
cs_pin=14,
|
||||
dc_pin=27,
|
||||
reset_pin=33,
|
||||
initsequence=(
|
||||
(SETEXTC, 0xFF, 0x93, 0x42),
|
||||
(PWCTR1, 0x12, 0x12),
|
||||
(PWCTR2, 0x03),
|
||||
(VMCTR1, 0xF2),
|
||||
(IFMODE, 0xE0),
|
||||
(0xF6, 0x01, 0x00, 0x00),
|
||||
(
|
||||
GMCTRP1,
|
||||
0x00,
|
||||
0x0C,
|
||||
0x11,
|
||||
0x04,
|
||||
0x11,
|
||||
0x08,
|
||||
0x37,
|
||||
0x89,
|
||||
0x4C,
|
||||
0x06,
|
||||
0x0C,
|
||||
0x0A,
|
||||
0x2E,
|
||||
0x34,
|
||||
0x0F,
|
||||
),
|
||||
(
|
||||
GMCTRN1,
|
||||
0x00,
|
||||
0x0B,
|
||||
0x11,
|
||||
0x05,
|
||||
0x13,
|
||||
0x09,
|
||||
0x33,
|
||||
0x67,
|
||||
0x48,
|
||||
0x07,
|
||||
0x0E,
|
||||
0x0B,
|
||||
0x2E,
|
||||
0x33,
|
||||
0x0F,
|
||||
),
|
||||
(DFUNCTR, 0x08, 0x82, 0x1D, 0x04),
|
||||
(IDMOFF,),
|
||||
),
|
||||
)
|
||||
ILI9341 = DriverChip(
|
||||
"ILI9341",
|
||||
mirror_x=True,
|
||||
width=240,
|
||||
height=320,
|
||||
initsequence=(
|
||||
(0xEF, 0x03, 0x80, 0x02),
|
||||
(0xCF, 0x00, 0xC1, 0x30),
|
||||
(0xED, 0x64, 0x03, 0x12, 0x81),
|
||||
(0xE8, 0x85, 0x00, 0x78),
|
||||
(0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02),
|
||||
(0xF7, 0x20),
|
||||
(0xEA, 0x00, 0x00),
|
||||
(PWCTR1, 0x23),
|
||||
(PWCTR2, 0x10),
|
||||
(VMCTR1, 0x3E, 0x28),
|
||||
(VMCTR2, 0x86),
|
||||
(VSCRSADD, 0x00),
|
||||
(FRMCTR1, 0x00, 0x18),
|
||||
(DFUNCTR, 0x08, 0x82, 0x27),
|
||||
(0xF2, 0x00),
|
||||
(GAMMASET, 0x01),
|
||||
(
|
||||
GMCTRP1,
|
||||
0x0F,
|
||||
0x31,
|
||||
0x2B,
|
||||
0x0C,
|
||||
0x0E,
|
||||
0x08,
|
||||
0x4E,
|
||||
0xF1,
|
||||
0x37,
|
||||
0x07,
|
||||
0x10,
|
||||
0x03,
|
||||
0x0E,
|
||||
0x09,
|
||||
0x00,
|
||||
),
|
||||
(
|
||||
GMCTRN1,
|
||||
0x00,
|
||||
0x0E,
|
||||
0x14,
|
||||
0x03,
|
||||
0x11,
|
||||
0x07,
|
||||
0x31,
|
||||
0xC1,
|
||||
0x48,
|
||||
0x08,
|
||||
0x0F,
|
||||
0x0C,
|
||||
0x31,
|
||||
0x36,
|
||||
0x0F,
|
||||
),
|
||||
),
|
||||
)
|
||||
DriverChip(
|
||||
"ILI9481",
|
||||
mirror_x=True,
|
||||
width=320,
|
||||
height=480,
|
||||
use_axis_flips=True,
|
||||
initsequence=(
|
||||
(PWSET, 0x07, 0x42, 0x18),
|
||||
(VMCTR, 0x00, 0x07, 0x10),
|
||||
(PWSETN, 0x01, 0x02),
|
||||
(PWCTR1, 0x10, 0x3B, 0x00, 0x02, 0x11),
|
||||
(VMCTR1, 0x03),
|
||||
(IFCTR, 0x83),
|
||||
(GMCTR, 0x32, 0x36, 0x45, 0x06, 0x16, 0x37, 0x75, 0x77, 0x54, 0x0C, 0x00),
|
||||
),
|
||||
)
|
||||
DriverChip(
|
||||
"ILI9486",
|
||||
mirror_x=True,
|
||||
width=320,
|
||||
height=480,
|
||||
initsequence=(
|
||||
(PWCTR3, 0x44),
|
||||
(VMCTR1, 0x00, 0x00, 0x00, 0x00),
|
||||
(
|
||||
GMCTRP1,
|
||||
0x0F,
|
||||
0x1F,
|
||||
0x1C,
|
||||
0x0C,
|
||||
0x0F,
|
||||
0x08,
|
||||
0x48,
|
||||
0x98,
|
||||
0x37,
|
||||
0x0A,
|
||||
0x13,
|
||||
0x04,
|
||||
0x11,
|
||||
0x0D,
|
||||
0x00,
|
||||
),
|
||||
(
|
||||
GMCTRN1,
|
||||
0x0F,
|
||||
0x32,
|
||||
0x2E,
|
||||
0x0B,
|
||||
0x0D,
|
||||
0x05,
|
||||
0x47,
|
||||
0x75,
|
||||
0x37,
|
||||
0x06,
|
||||
0x10,
|
||||
0x03,
|
||||
0x24,
|
||||
0x20,
|
||||
0x00,
|
||||
),
|
||||
),
|
||||
)
|
||||
DriverChip(
|
||||
"ILI9488",
|
||||
width=320,
|
||||
height=480,
|
||||
pixel_mode="18bit",
|
||||
initsequence=(
|
||||
(
|
||||
GMCTRP1,
|
||||
0x0F,
|
||||
0x24,
|
||||
0x1C,
|
||||
0x0A,
|
||||
0x0F,
|
||||
0x08,
|
||||
0x43,
|
||||
0x88,
|
||||
0x32,
|
||||
0x0F,
|
||||
0x10,
|
||||
0x06,
|
||||
0x0F,
|
||||
0x07,
|
||||
0x00,
|
||||
),
|
||||
(
|
||||
GMCTRN1,
|
||||
0x0F,
|
||||
0x38,
|
||||
0x30,
|
||||
0x09,
|
||||
0x0F,
|
||||
0x0F,
|
||||
0x4E,
|
||||
0x77,
|
||||
0x3C,
|
||||
0x07,
|
||||
0x10,
|
||||
0x05,
|
||||
0x23,
|
||||
0x1B,
|
||||
0x00,
|
||||
),
|
||||
(PWCTR1, 0x17, 0x15),
|
||||
(PWCTR2, 0x41),
|
||||
(VMCTR1, 0x00, 0x12, 0x80),
|
||||
(IFMODE, 0x00),
|
||||
(FRMCTR1, 0xA0),
|
||||
(INVCTR, 0x02),
|
||||
(0xE9, 0x00),
|
||||
(ADJCTL3, 0xA9, 0x51, 0x2C, 0x82),
|
||||
),
|
||||
)
|
||||
ILI9488_A = DriverChip(
|
||||
"ILI9488_A",
|
||||
width=320,
|
||||
height=480,
|
||||
invert_colors=False,
|
||||
pixel_mode="18bit",
|
||||
mirror_x=True,
|
||||
initsequence=(
|
||||
(
|
||||
GMCTRP1,
|
||||
0x00,
|
||||
0x03,
|
||||
0x09,
|
||||
0x08,
|
||||
0x16,
|
||||
0x0A,
|
||||
0x3F,
|
||||
0x78,
|
||||
0x4C,
|
||||
0x09,
|
||||
0x0A,
|
||||
0x08,
|
||||
0x16,
|
||||
0x1A,
|
||||
0x0F,
|
||||
),
|
||||
(
|
||||
GMCTRN1,
|
||||
0x00,
|
||||
0x16,
|
||||
0x19,
|
||||
0x03,
|
||||
0x0F,
|
||||
0x05,
|
||||
0x32,
|
||||
0x45,
|
||||
0x46,
|
||||
0x04,
|
||||
0x0E,
|
||||
0x0D,
|
||||
0x35,
|
||||
0x37,
|
||||
0x0F,
|
||||
),
|
||||
(PWCTR1, 0x17, 0x15),
|
||||
(PWCTR2, 0x41),
|
||||
(VMCTR1, 0x00, 0x12, 0x80),
|
||||
(IFMODE, 0x00),
|
||||
(FRMCTR1, 0xA0),
|
||||
(INVCTR, 0x02),
|
||||
(DFUNCTR, 0x02, 0x02),
|
||||
(0xE9, 0x00),
|
||||
(ADJCTL3, 0xA9, 0x51, 0x2C, 0x82),
|
||||
),
|
||||
)
|
||||
ST7796 = DriverChip(
|
||||
"ST7796",
|
||||
mirror_x=True,
|
||||
width=320,
|
||||
height=480,
|
||||
initsequence=(
|
||||
(SWRESET,),
|
||||
(CSCON, 0xC3),
|
||||
(CSCON, 0x96),
|
||||
(VMCTR1, 0x1C),
|
||||
(IFMODE, 0x80),
|
||||
(INVCTR, 0x01),
|
||||
(DFUNCTR, 0x80, 0x02, 0x3B),
|
||||
(ETMOD, 0xC6),
|
||||
(CSCON, 0x69),
|
||||
(CSCON, 0x3C),
|
||||
),
|
||||
)
|
||||
DriverChip(
|
||||
"S3BOX",
|
||||
width=320,
|
||||
height=240,
|
||||
mirror_x=True,
|
||||
mirror_y=True,
|
||||
invert_colors=False,
|
||||
data_rate="40MHz",
|
||||
dc_pin=4,
|
||||
cs_pin=5,
|
||||
# reset_pin={CONF_INVERTED: True, CONF_NUMBER: 48},
|
||||
initsequence=(
|
||||
(0xEF, 0x03, 0x80, 0x02),
|
||||
(0xCF, 0x00, 0xC1, 0x30),
|
||||
(0xED, 0x64, 0x03, 0x12, 0x81),
|
||||
(0xE8, 0x85, 0x00, 0x78),
|
||||
(0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02),
|
||||
(0xF7, 0x20),
|
||||
(0xEA, 0x00, 0x00),
|
||||
(PWCTR1, 0x23),
|
||||
(PWCTR2, 0x10),
|
||||
(VMCTR1, 0x3E, 0x28),
|
||||
(VMCTR2, 0x86),
|
||||
(VSCRSADD, 0x00),
|
||||
(FRMCTR1, 0x00, 0x18),
|
||||
(DFUNCTR, 0x08, 0x82, 0x27),
|
||||
(0xF2, 0x00),
|
||||
(GAMMASET, 0x01),
|
||||
(
|
||||
GMCTRP1,
|
||||
0x0F,
|
||||
0x31,
|
||||
0x2B,
|
||||
0x0C,
|
||||
0x0E,
|
||||
0x08,
|
||||
0x4E,
|
||||
0xF1,
|
||||
0x37,
|
||||
0x07,
|
||||
0x10,
|
||||
0x03,
|
||||
0x0E,
|
||||
0x09,
|
||||
0x00,
|
||||
),
|
||||
(
|
||||
GMCTRN1,
|
||||
0x00,
|
||||
0x0E,
|
||||
0x14,
|
||||
0x03,
|
||||
0x11,
|
||||
0x07,
|
||||
0x31,
|
||||
0xC1,
|
||||
0x48,
|
||||
0x08,
|
||||
0x0F,
|
||||
0x0C,
|
||||
0x31,
|
||||
0x36,
|
||||
0x0F,
|
||||
),
|
||||
),
|
||||
)
|
||||
DriverChip(
|
||||
"S3BOXLITE",
|
||||
mirror_x=True,
|
||||
color_order=MODE_RGB,
|
||||
width=320,
|
||||
height=240,
|
||||
cs_pin=5,
|
||||
dc_pin=4,
|
||||
reset_pin=48,
|
||||
initsequence=(
|
||||
(0xEF, 0x03, 0x80, 0x02),
|
||||
(0xCF, 0x00, 0xC1, 0x30),
|
||||
(0xED, 0x64, 0x03, 0x12, 0x81),
|
||||
(0xE8, 0x85, 0x00, 0x78),
|
||||
(0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02),
|
||||
(0xF7, 0x20),
|
||||
(0xEA, 0x00, 0x00),
|
||||
(PWCTR1, 0x23),
|
||||
(PWCTR2, 0x10),
|
||||
(VMCTR1, 0x3E, 0x28),
|
||||
(VMCTR2, 0x86),
|
||||
(VSCRSADD, 0x00),
|
||||
(FRMCTR1, 0x00, 0x18),
|
||||
(DFUNCTR, 0x08, 0x82, 0x27),
|
||||
(0xF2, 0x00),
|
||||
(GAMMASET, 0x01),
|
||||
(
|
||||
GMCTRP1,
|
||||
0xF0,
|
||||
0x09,
|
||||
0x0B,
|
||||
0x06,
|
||||
0x04,
|
||||
0x15,
|
||||
0x2F,
|
||||
0x54,
|
||||
0x42,
|
||||
0x3C,
|
||||
0x17,
|
||||
0x14,
|
||||
0x18,
|
||||
0x1B,
|
||||
),
|
||||
(
|
||||
GMCTRN1,
|
||||
0xE0,
|
||||
0x09,
|
||||
0x0B,
|
||||
0x06,
|
||||
0x04,
|
||||
0x03,
|
||||
0x2B,
|
||||
0x43,
|
||||
0x42,
|
||||
0x3B,
|
||||
0x16,
|
||||
0x14,
|
||||
0x17,
|
||||
0x1B,
|
||||
),
|
||||
),
|
||||
)
|
||||
ST7789V = DriverChip(
|
||||
"ST7789V",
|
||||
width=240,
|
||||
height=320,
|
||||
initsequence=(
|
||||
(DFUNCTR, 0x0A, 0x82),
|
||||
(FRMCTR2, 0x0C, 0x0C, 0x00, 0x33, 0x33),
|
||||
(ETMOD, 0x35),
|
||||
(0xBB, 0x28),
|
||||
(PWCTR1, 0x0C),
|
||||
(PWCTR3, 0x01, 0xFF),
|
||||
(PWCTR4, 0x10),
|
||||
(PWCTR5, 0x20),
|
||||
(IFCTR, 0x0F),
|
||||
(PWSET, 0xA4, 0xA1),
|
||||
(
|
||||
GMCTRP1,
|
||||
0xD0,
|
||||
0x00,
|
||||
0x02,
|
||||
0x07,
|
||||
0x0A,
|
||||
0x28,
|
||||
0x32,
|
||||
0x44,
|
||||
0x42,
|
||||
0x06,
|
||||
0x0E,
|
||||
0x12,
|
||||
0x14,
|
||||
0x17,
|
||||
),
|
||||
(
|
||||
GMCTRN1,
|
||||
0xD0,
|
||||
0x00,
|
||||
0x02,
|
||||
0x07,
|
||||
0x0A,
|
||||
0x28,
|
||||
0x31,
|
||||
0x54,
|
||||
0x47,
|
||||
0x0E,
|
||||
0x1C,
|
||||
0x17,
|
||||
0x1B,
|
||||
0x1E,
|
||||
),
|
||||
),
|
||||
)
|
||||
DriverChip(
|
||||
"GC9A01A",
|
||||
mirror_x=True,
|
||||
width=240,
|
||||
height=240,
|
||||
initsequence=(
|
||||
(0xEF,),
|
||||
(0xEB, 0x14),
|
||||
(0xFE,),
|
||||
(0xEF,),
|
||||
(0xEB, 0x14),
|
||||
(0x84, 0x40),
|
||||
(0x85, 0xFF),
|
||||
(0x86, 0xFF),
|
||||
(0x87, 0xFF),
|
||||
(0x88, 0x0A),
|
||||
(0x89, 0x21),
|
||||
(0x8A, 0x00),
|
||||
(0x8B, 0x80),
|
||||
(0x8C, 0x01),
|
||||
(0x8D, 0x01),
|
||||
(0x8E, 0xFF),
|
||||
(0x8F, 0xFF),
|
||||
(0xB6, 0x00, 0x00),
|
||||
(0x90, 0x08, 0x08, 0x08, 0x08),
|
||||
(0xBD, 0x06),
|
||||
(0xBC, 0x00),
|
||||
(0xFF, 0x60, 0x01, 0x04),
|
||||
(0xC3, 0x13),
|
||||
(0xC4, 0x13),
|
||||
(0xF9, 0x22),
|
||||
(0xBE, 0x11),
|
||||
(0xE1, 0x10, 0x0E),
|
||||
(0xDF, 0x21, 0x0C, 0x02),
|
||||
(0xF0, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A),
|
||||
(0xF1, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F),
|
||||
(0xF2, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A),
|
||||
(0xF3, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F),
|
||||
(0xED, 0x1B, 0x0B),
|
||||
(0xAE, 0x77),
|
||||
(0xCD, 0x63),
|
||||
(0xE8, 0x34),
|
||||
(
|
||||
0x62,
|
||||
0x18,
|
||||
0x0D,
|
||||
0x71,
|
||||
0xED,
|
||||
0x70,
|
||||
0x70,
|
||||
0x18,
|
||||
0x0F,
|
||||
0x71,
|
||||
0xEF,
|
||||
0x70,
|
||||
0x70,
|
||||
),
|
||||
(
|
||||
0x63,
|
||||
0x18,
|
||||
0x11,
|
||||
0x71,
|
||||
0xF1,
|
||||
0x70,
|
||||
0x70,
|
||||
0x18,
|
||||
0x13,
|
||||
0x71,
|
||||
0xF3,
|
||||
0x70,
|
||||
0x70,
|
||||
),
|
||||
(0x64, 0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07),
|
||||
(0x66, 0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00),
|
||||
(0x67, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98),
|
||||
(0x74, 0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00),
|
||||
(0x98, 0x3E, 0x07),
|
||||
(0x35,),
|
||||
),
|
||||
)
|
||||
DriverChip(
|
||||
"GC9D01N",
|
||||
width=160,
|
||||
height=160,
|
||||
initsequence=(
|
||||
(0xFE,),
|
||||
(0xEF,),
|
||||
(0x80, 0xFF),
|
||||
(0x81, 0xFF),
|
||||
(0x82, 0xFF),
|
||||
(0x83, 0xFF),
|
||||
(0x84, 0xFF),
|
||||
(0x85, 0xFF),
|
||||
(0x86, 0xFF),
|
||||
(0x87, 0xFF),
|
||||
(0x88, 0xFF),
|
||||
(0x89, 0xFF),
|
||||
(0x8A, 0xFF),
|
||||
(0x8B, 0xFF),
|
||||
(0x8C, 0xFF),
|
||||
(0x8D, 0xFF),
|
||||
(0x8E, 0xFF),
|
||||
(0x8F, 0xFF),
|
||||
(0x3A, 0x05),
|
||||
(0xEC, 0x01),
|
||||
(0x74, 0x02, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00),
|
||||
(0x98, 0x3E),
|
||||
(0x99, 0x3E),
|
||||
(0xB5, 0x0D, 0x0D),
|
||||
(0x60, 0x38, 0x0F, 0x79, 0x67),
|
||||
(0x61, 0x38, 0x11, 0x79, 0x67),
|
||||
(0x64, 0x38, 0x17, 0x71, 0x5F, 0x79, 0x67),
|
||||
(0x65, 0x38, 0x13, 0x71, 0x5B, 0x79, 0x67),
|
||||
(0x6A, 0x00, 0x00),
|
||||
(0x6C, 0x22, 0x02, 0x22, 0x02, 0x22, 0x22, 0x50),
|
||||
(
|
||||
0x6E,
|
||||
0x03,
|
||||
0x03,
|
||||
0x01,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0F,
|
||||
0x0F,
|
||||
0x0D,
|
||||
0x0D,
|
||||
0x0B,
|
||||
0x0B,
|
||||
0x09,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0A,
|
||||
0x0A,
|
||||
0x0C,
|
||||
0x0C,
|
||||
0x0E,
|
||||
0x0E,
|
||||
0x10,
|
||||
0x10,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x02,
|
||||
0x04,
|
||||
0x04,
|
||||
),
|
||||
(0xBF, 0x01),
|
||||
(0xF9, 0x40),
|
||||
(0x9B, 0x3B, 0x93, 0x33, 0x7F, 0x00),
|
||||
(0x7E, 0x30),
|
||||
(0x70, 0x0D, 0x02, 0x08, 0x0D, 0x02, 0x08),
|
||||
(0x71, 0x0D, 0x02, 0x08),
|
||||
(0x91, 0x0E, 0x09),
|
||||
(0xC3, 0x19, 0xC4, 0x19, 0xC9, 0x3C),
|
||||
(0xF0, 0x53, 0x15, 0x0A, 0x04, 0x00, 0x3E),
|
||||
(0xF1, 0x56, 0xA8, 0x7F, 0x33, 0x34, 0x5F),
|
||||
(0xF2, 0x53, 0x15, 0x0A, 0x04, 0x00, 0x3A),
|
||||
(0xF3, 0x52, 0xA4, 0x7F, 0x33, 0x34, 0xDF),
|
||||
),
|
||||
)
|
||||
DriverChip(
|
||||
"ST7735",
|
||||
color_order=MODE_RGB,
|
||||
width=128,
|
||||
height=160,
|
||||
initsequence=(
|
||||
SWRESET,
|
||||
delay(10),
|
||||
(FRMCTR1, 0x01, 0x2C, 0x2D),
|
||||
(FRMCTR2, 0x01, 0x2C, 0x2D),
|
||||
(FRMCTR3, 0x01, 0x2C, 0x2D, 0x01, 0x2C, 0x2D),
|
||||
(INVCTR, 0x07),
|
||||
(PWCTR1, 0xA2, 0x02, 0x84),
|
||||
(PWCTR2, 0xC5),
|
||||
(PWCTR3, 0x0A, 0x00),
|
||||
(PWCTR4, 0x8A, 0x2A),
|
||||
(PWCTR5, 0x8A, 0xEE),
|
||||
(VMCTR1, 0x0E),
|
||||
(
|
||||
GMCTRP1,
|
||||
0x02,
|
||||
0x1C,
|
||||
0x07,
|
||||
0x12,
|
||||
0x37,
|
||||
0x32,
|
||||
0x29,
|
||||
0x2D,
|
||||
0x29,
|
||||
0x25,
|
||||
0x2B,
|
||||
0x39,
|
||||
0x00,
|
||||
0x01,
|
||||
0x03,
|
||||
0x10,
|
||||
),
|
||||
(
|
||||
GMCTRN1,
|
||||
0x03,
|
||||
0x1D,
|
||||
0x07,
|
||||
0x06,
|
||||
0x2E,
|
||||
0x2C,
|
||||
0x29,
|
||||
0x2D,
|
||||
0x2E,
|
||||
0x2E,
|
||||
0x37,
|
||||
0x3F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x10,
|
||||
),
|
||||
NORON,
|
||||
),
|
||||
)
|
||||
ST7796.extend(
|
||||
"WT32-SC01-PLUS",
|
||||
bus_mode=TYPE_OCTAL,
|
||||
mirror_x=True,
|
||||
reset_pin=4,
|
||||
dc_pin=0,
|
||||
invert_colors=True,
|
||||
)
|
||||
|
||||
models = {}
|
||||
260
esphome/components/mipi_spi/models/jc.py
Normal file
260
esphome/components/mipi_spi/models/jc.py
Normal file
@@ -0,0 +1,260 @@
|
||||
from esphome.components.spi import TYPE_QUAD
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER
|
||||
|
||||
from .. import MODE_RGB
|
||||
from . import DriverChip
|
||||
|
||||
AXS15231 = DriverChip(
|
||||
"AXS15231",
|
||||
draw_rounding=8,
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order=MODE_RGB,
|
||||
bus_mode=TYPE_QUAD,
|
||||
initsequence=(
|
||||
(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5),
|
||||
(0xC1, 0x33),
|
||||
(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
|
||||
),
|
||||
)
|
||||
|
||||
AXS15231.extend(
|
||||
"JC3248W535",
|
||||
width=320,
|
||||
height=480,
|
||||
cs_pin={CONF_NUMBER: 45, CONF_IGNORE_STRAPPING_WARNING: True},
|
||||
data_rate="40MHz",
|
||||
)
|
||||
|
||||
DriverChip(
|
||||
"JC3636W518",
|
||||
height=360,
|
||||
width=360,
|
||||
offset_height=1,
|
||||
draw_rounding=1,
|
||||
cs_pin=10,
|
||||
reset_pin=47,
|
||||
invert_colors=True,
|
||||
color_order=MODE_RGB,
|
||||
bus_mode=TYPE_QUAD,
|
||||
data_rate="40MHz",
|
||||
initsequence=(
|
||||
(0xF0, 0x08),
|
||||
(0xF2, 0x08),
|
||||
(0x9B, 0x51),
|
||||
(0x86, 0x53),
|
||||
(0xF2, 0x80),
|
||||
(0xF0, 0x00),
|
||||
(0xF0, 0x01),
|
||||
(0xF1, 0x01),
|
||||
(0xB0, 0x54),
|
||||
(0xB1, 0x3F),
|
||||
(0xB2, 0x2A),
|
||||
(0xB4, 0x46),
|
||||
(0xB5, 0x34),
|
||||
(0xB6, 0xD5),
|
||||
(0xB7, 0x30),
|
||||
(0xBA, 0x00),
|
||||
(0xBB, 0x08),
|
||||
(0xBC, 0x08),
|
||||
(0xBD, 0x00),
|
||||
(0xC0, 0x80),
|
||||
(0xC1, 0x10),
|
||||
(0xC2, 0x37),
|
||||
(0xC3, 0x80),
|
||||
(0xC4, 0x10),
|
||||
(0xC5, 0x37),
|
||||
(0xC6, 0xA9),
|
||||
(0xC7, 0x41),
|
||||
(0xC8, 0x51),
|
||||
(0xC9, 0xA9),
|
||||
(0xCA, 0x41),
|
||||
(0xCB, 0x51),
|
||||
(0xD0, 0x91),
|
||||
(0xD1, 0x68),
|
||||
(0xD2, 0x69),
|
||||
(0xF5, 0x00, 0xA5),
|
||||
(0xDD, 0x3F),
|
||||
(0xDE, 0x3F),
|
||||
(0xF1, 0x10),
|
||||
(0xF0, 0x00),
|
||||
(0xF0, 0x02),
|
||||
(
|
||||
0xE0,
|
||||
0x70,
|
||||
0x09,
|
||||
0x12,
|
||||
0x0C,
|
||||
0x0B,
|
||||
0x27,
|
||||
0x38,
|
||||
0x54,
|
||||
0x4E,
|
||||
0x19,
|
||||
0x15,
|
||||
0x15,
|
||||
0x2C,
|
||||
0x2F,
|
||||
),
|
||||
(
|
||||
0xE1,
|
||||
0x70,
|
||||
0x08,
|
||||
0x11,
|
||||
0x0C,
|
||||
0x0B,
|
||||
0x27,
|
||||
0x38,
|
||||
0x43,
|
||||
0x4C,
|
||||
0x18,
|
||||
0x14,
|
||||
0x14,
|
||||
0x2B,
|
||||
0x2D,
|
||||
),
|
||||
(0xF0, 0x10),
|
||||
(0xF3, 0x10),
|
||||
(0xE0, 0x08),
|
||||
(0xE1, 0x00),
|
||||
(0xE2, 0x00),
|
||||
(0xE3, 0x00),
|
||||
(0xE4, 0xE0),
|
||||
(0xE5, 0x06),
|
||||
(0xE6, 0x21),
|
||||
(0xE7, 0x00),
|
||||
(0xE8, 0x05),
|
||||
(0xE9, 0x82),
|
||||
(0xEA, 0xDF),
|
||||
(0xEB, 0x89),
|
||||
(0xEC, 0x20),
|
||||
(0xED, 0x14),
|
||||
(0xEE, 0xFF),
|
||||
(0xEF, 0x00),
|
||||
(0xF8, 0xFF),
|
||||
(0xF9, 0x00),
|
||||
(0xFA, 0x00),
|
||||
(0xFB, 0x30),
|
||||
(0xFC, 0x00),
|
||||
(0xFD, 0x00),
|
||||
(0xFE, 0x00),
|
||||
(0xFF, 0x00),
|
||||
(0x60, 0x42),
|
||||
(0x61, 0xE0),
|
||||
(0x62, 0x40),
|
||||
(0x63, 0x40),
|
||||
(0x64, 0x02),
|
||||
(0x65, 0x00),
|
||||
(0x66, 0x40),
|
||||
(0x67, 0x03),
|
||||
(0x68, 0x00),
|
||||
(0x69, 0x00),
|
||||
(0x6A, 0x00),
|
||||
(0x6B, 0x00),
|
||||
(0x70, 0x42),
|
||||
(0x71, 0xE0),
|
||||
(0x72, 0x40),
|
||||
(0x73, 0x40),
|
||||
(0x74, 0x02),
|
||||
(0x75, 0x00),
|
||||
(0x76, 0x40),
|
||||
(0x77, 0x03),
|
||||
(0x78, 0x00),
|
||||
(0x79, 0x00),
|
||||
(0x7A, 0x00),
|
||||
(0x7B, 0x00),
|
||||
(0x80, 0x48),
|
||||
(0x81, 0x00),
|
||||
(0x82, 0x05),
|
||||
(0x83, 0x02),
|
||||
(0x84, 0xDD),
|
||||
(0x85, 0x00),
|
||||
(0x86, 0x00),
|
||||
(0x87, 0x00),
|
||||
(0x88, 0x48),
|
||||
(0x89, 0x00),
|
||||
(0x8A, 0x07),
|
||||
(0x8B, 0x02),
|
||||
(0x8C, 0xDF),
|
||||
(0x8D, 0x00),
|
||||
(0x8E, 0x00),
|
||||
(0x8F, 0x00),
|
||||
(0x90, 0x48),
|
||||
(0x91, 0x00),
|
||||
(0x92, 0x09),
|
||||
(0x93, 0x02),
|
||||
(0x94, 0xE1),
|
||||
(0x95, 0x00),
|
||||
(0x96, 0x00),
|
||||
(0x97, 0x00),
|
||||
(0x98, 0x48),
|
||||
(0x99, 0x00),
|
||||
(0x9A, 0x0B),
|
||||
(0x9B, 0x02),
|
||||
(0x9C, 0xE3),
|
||||
(0x9D, 0x00),
|
||||
(0x9E, 0x00),
|
||||
(0x9F, 0x00),
|
||||
(0xA0, 0x48),
|
||||
(0xA1, 0x00),
|
||||
(0xA2, 0x04),
|
||||
(0xA3, 0x02),
|
||||
(0xA4, 0xDC),
|
||||
(0xA5, 0x00),
|
||||
(0xA6, 0x00),
|
||||
(0xA7, 0x00),
|
||||
(0xA8, 0x48),
|
||||
(0xA9, 0x00),
|
||||
(0xAA, 0x06),
|
||||
(0xAB, 0x02),
|
||||
(0xAC, 0xDE),
|
||||
(0xAD, 0x00),
|
||||
(0xAE, 0x00),
|
||||
(0xAF, 0x00),
|
||||
(0xB0, 0x48),
|
||||
(0xB1, 0x00),
|
||||
(0xB2, 0x08),
|
||||
(0xB3, 0x02),
|
||||
(0xB4, 0xE0),
|
||||
(0xB5, 0x00),
|
||||
(0xB6, 0x00),
|
||||
(0xB7, 0x00),
|
||||
(0xB8, 0x48),
|
||||
(0xB9, 0x00),
|
||||
(0xBA, 0x0A),
|
||||
(0xBB, 0x02),
|
||||
(0xBC, 0xE2),
|
||||
(0xBD, 0x00),
|
||||
(0xBE, 0x00),
|
||||
(0xBF, 0x00),
|
||||
(0xC0, 0x12),
|
||||
(0xC1, 0xAA),
|
||||
(0xC2, 0x65),
|
||||
(0xC3, 0x74),
|
||||
(0xC4, 0x47),
|
||||
(0xC5, 0x56),
|
||||
(0xC6, 0x00),
|
||||
(0xC7, 0x88),
|
||||
(0xC8, 0x99),
|
||||
(0xC9, 0x33),
|
||||
(0xD0, 0x21),
|
||||
(0xD1, 0xAA),
|
||||
(0xD2, 0x65),
|
||||
(0xD3, 0x74),
|
||||
(0xD4, 0x47),
|
||||
(0xD5, 0x56),
|
||||
(0xD6, 0x00),
|
||||
(0xD7, 0x88),
|
||||
(0xD8, 0x99),
|
||||
(0xD9, 0x33),
|
||||
(0xF3, 0x01),
|
||||
(0xF0, 0x00),
|
||||
(0xF0, 0x01),
|
||||
(0xF1, 0x01),
|
||||
(0xA0, 0x0B),
|
||||
(0xA3, 0x2A),
|
||||
(0xA5, 0xC3),
|
||||
),
|
||||
)
|
||||
|
||||
models = {}
|
||||
15
esphome/components/mipi_spi/models/lanbon.py
Normal file
15
esphome/components/mipi_spi/models/lanbon.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from .ili import ST7789V
|
||||
|
||||
ST7789V.extend(
|
||||
"LANBON-L8",
|
||||
width=240,
|
||||
height=320,
|
||||
mirror_x=True,
|
||||
mirror_y=True,
|
||||
data_rate="80MHz",
|
||||
cs_pin=22,
|
||||
dc_pin=21,
|
||||
reset_pin=18,
|
||||
)
|
||||
|
||||
models = {}
|
||||
60
esphome/components/mipi_spi/models/lilygo.py
Normal file
60
esphome/components/mipi_spi/models/lilygo.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from esphome.components.spi import TYPE_OCTAL
|
||||
|
||||
from .. import MODE_BGR
|
||||
from .ili import ST7789V, ST7796
|
||||
|
||||
ST7789V.extend(
|
||||
"T-EMBED",
|
||||
width=170,
|
||||
height=320,
|
||||
offset_width=35,
|
||||
color_order=MODE_BGR,
|
||||
invert_colors=True,
|
||||
draw_rounding=1,
|
||||
cs_pin=10,
|
||||
dc_pin=13,
|
||||
reset_pin=9,
|
||||
data_rate="80MHz",
|
||||
)
|
||||
|
||||
ST7789V.extend(
|
||||
"T-DISPLAY",
|
||||
height=240,
|
||||
width=135,
|
||||
offset_width=52,
|
||||
offset_height=40,
|
||||
draw_rounding=1,
|
||||
cs_pin=5,
|
||||
dc_pin=16,
|
||||
invert_colors=True,
|
||||
)
|
||||
ST7789V.extend(
|
||||
"T-DISPLAY-S3",
|
||||
height=320,
|
||||
width=170,
|
||||
offset_width=35,
|
||||
color_order=MODE_BGR,
|
||||
invert_colors=True,
|
||||
draw_rounding=1,
|
||||
dc_pin=7,
|
||||
cs_pin=6,
|
||||
reset_pin=5,
|
||||
enable_pin=[9, 15],
|
||||
data_rate="10MHz",
|
||||
bus_mode=TYPE_OCTAL,
|
||||
)
|
||||
|
||||
ST7796.extend(
|
||||
"T-DISPLAY-S3-PRO",
|
||||
width=222,
|
||||
height=480,
|
||||
offset_width=49,
|
||||
draw_rounding=1,
|
||||
cs_pin=39,
|
||||
reset_pin=47,
|
||||
dc_pin=9,
|
||||
backlight_pin=48,
|
||||
invert_colors=True,
|
||||
)
|
||||
|
||||
models = {}
|
||||
139
esphome/components/mipi_spi/models/waveshare.py
Normal file
139
esphome/components/mipi_spi/models/waveshare.py
Normal file
@@ -0,0 +1,139 @@
|
||||
from . import DriverChip
|
||||
from .ili import ILI9488_A
|
||||
|
||||
DriverChip(
|
||||
"WAVESHARE-4-TFT",
|
||||
width=320,
|
||||
height=480,
|
||||
invert_colors=True,
|
||||
spi_16=True,
|
||||
initsequence=(
|
||||
(
|
||||
0xF9,
|
||||
0x00,
|
||||
0x08,
|
||||
),
|
||||
(
|
||||
0xC0,
|
||||
0x19,
|
||||
0x1A,
|
||||
),
|
||||
(
|
||||
0xC1,
|
||||
0x45,
|
||||
0x00,
|
||||
),
|
||||
(
|
||||
0xC2,
|
||||
0x33,
|
||||
),
|
||||
(
|
||||
0xC5,
|
||||
0x00,
|
||||
0x28,
|
||||
),
|
||||
(
|
||||
0xB1,
|
||||
0xA0,
|
||||
0x11,
|
||||
),
|
||||
(
|
||||
0xB4,
|
||||
0x02,
|
||||
),
|
||||
(
|
||||
0xB6,
|
||||
0x00,
|
||||
0x42,
|
||||
0x3B,
|
||||
),
|
||||
(
|
||||
0xB7,
|
||||
0x07,
|
||||
),
|
||||
(
|
||||
0xE0,
|
||||
0x1F,
|
||||
0x25,
|
||||
0x22,
|
||||
0x0B,
|
||||
0x06,
|
||||
0x0A,
|
||||
0x4E,
|
||||
0xC6,
|
||||
0x39,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
),
|
||||
(
|
||||
0xE1,
|
||||
0x1F,
|
||||
0x3F,
|
||||
0x3F,
|
||||
0x0F,
|
||||
0x1F,
|
||||
0x0F,
|
||||
0x46,
|
||||
0x49,
|
||||
0x31,
|
||||
0x05,
|
||||
0x09,
|
||||
0x03,
|
||||
0x1C,
|
||||
0x1A,
|
||||
0x00,
|
||||
),
|
||||
(
|
||||
0xF1,
|
||||
0x36,
|
||||
0x04,
|
||||
0x00,
|
||||
0x3C,
|
||||
0x0F,
|
||||
0x0F,
|
||||
0xA4,
|
||||
0x02,
|
||||
),
|
||||
(
|
||||
0xF2,
|
||||
0x18,
|
||||
0xA3,
|
||||
0x12,
|
||||
0x02,
|
||||
0x32,
|
||||
0x12,
|
||||
0xFF,
|
||||
0x32,
|
||||
0x00,
|
||||
),
|
||||
(
|
||||
0xF4,
|
||||
0x40,
|
||||
0x00,
|
||||
0x08,
|
||||
0x91,
|
||||
0x04,
|
||||
),
|
||||
(
|
||||
0xF8,
|
||||
0x21,
|
||||
0x04,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
ILI9488_A.extend(
|
||||
"PICO-RESTOUCH-LCD-3.5",
|
||||
spi_16=True,
|
||||
pixel_mode="16bit",
|
||||
mirror_x=True,
|
||||
dc_pin=33,
|
||||
cs_pin=34,
|
||||
reset_pin=40,
|
||||
data_rate="20MHz",
|
||||
invert_colors=True,
|
||||
)
|
||||
@@ -64,6 +64,8 @@ constexpr const char *const MQTT_DEVICE_NAME = "name";
|
||||
constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "sa";
|
||||
constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw";
|
||||
constexpr const char *const MQTT_DEVICE_HW_VERSION = "hw";
|
||||
constexpr const char *const MQTT_DIRECTION_COMMAND_TOPIC = "dir_cmd_t";
|
||||
constexpr const char *const MQTT_DIRECTION_STATE_TOPIC = "dir_stat_t";
|
||||
constexpr const char *const MQTT_DOCKED_TEMPLATE = "dock_tpl";
|
||||
constexpr const char *const MQTT_DOCKED_TOPIC = "dock_t";
|
||||
constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "fx_cmd_t";
|
||||
@@ -328,6 +330,8 @@ constexpr const char *const MQTT_DEVICE_NAME = "name";
|
||||
constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "suggested_area";
|
||||
constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw_version";
|
||||
constexpr const char *const MQTT_DEVICE_HW_VERSION = "hw_version";
|
||||
constexpr const char *const MQTT_DIRECTION_COMMAND_TOPIC = "direction_command_topic";
|
||||
constexpr const char *const MQTT_DIRECTION_STATE_TOPIC = "direction_state_topic";
|
||||
constexpr const char *const MQTT_DOCKED_TEMPLATE = "docked_template";
|
||||
constexpr const char *const MQTT_DOCKED_TOPIC = "docked_topic";
|
||||
constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "effect_command_topic";
|
||||
|
||||
@@ -43,6 +43,32 @@ void MQTTFanComponent::setup() {
|
||||
}
|
||||
});
|
||||
|
||||
if (this->state_->get_traits().supports_direction()) {
|
||||
this->subscribe(this->get_direction_command_topic(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto val = parse_on_off(payload.c_str(), "forward", "reverse");
|
||||
switch (val) {
|
||||
case PARSE_ON:
|
||||
ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name().c_str());
|
||||
this->state_->make_call().set_direction(fan::FanDirection::FORWARD).perform();
|
||||
break;
|
||||
case PARSE_OFF:
|
||||
ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name().c_str());
|
||||
this->state_->make_call().set_direction(fan::FanDirection::REVERSE).perform();
|
||||
break;
|
||||
case PARSE_TOGGLE:
|
||||
this->state_->make_call()
|
||||
.set_direction(this->state_->direction == fan::FanDirection::FORWARD ? fan::FanDirection::REVERSE
|
||||
: fan::FanDirection::FORWARD)
|
||||
.perform();
|
||||
break;
|
||||
case PARSE_NONE:
|
||||
ESP_LOGW(TAG, "Unknown direction Payload %s", payload.c_str());
|
||||
this->status_momentary_warning("direction", 5000);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this->state_->get_traits().supports_oscillation()) {
|
||||
this->subscribe(this->get_oscillation_command_topic(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
@@ -94,6 +120,10 @@ void MQTTFanComponent::setup() {
|
||||
void MQTTFanComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Fan '%s': ", this->state_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
if (this->state_->get_traits().supports_direction()) {
|
||||
ESP_LOGCONFIG(TAG, " Direction State Topic: '%s'", this->get_direction_state_topic().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Direction Command Topic: '%s'", this->get_direction_command_topic().c_str());
|
||||
}
|
||||
if (this->state_->get_traits().supports_oscillation()) {
|
||||
ESP_LOGCONFIG(TAG, " Oscillation State Topic: '%s'", this->get_oscillation_state_topic().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Oscillation Command Topic: '%s'", this->get_oscillation_command_topic().c_str());
|
||||
@@ -107,6 +137,10 @@ void MQTTFanComponent::dump_config() {
|
||||
bool MQTTFanComponent::send_initial_state() { return this->publish_state(); }
|
||||
|
||||
void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
if (this->state_->get_traits().supports_direction()) {
|
||||
root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic();
|
||||
root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic();
|
||||
}
|
||||
if (this->state_->get_traits().supports_oscillation()) {
|
||||
root[MQTT_OSCILLATION_COMMAND_TOPIC] = this->get_oscillation_command_topic();
|
||||
root[MQTT_OSCILLATION_STATE_TOPIC] = this->get_oscillation_state_topic();
|
||||
@@ -122,6 +156,11 @@ bool MQTTFanComponent::publish_state() {
|
||||
ESP_LOGD(TAG, "'%s' Sending state %s.", this->state_->get_name().c_str(), state_s);
|
||||
this->publish(this->get_state_topic_(), state_s);
|
||||
bool failed = false;
|
||||
if (this->state_->get_traits().supports_direction()) {
|
||||
bool success = this->publish(this->get_direction_state_topic(),
|
||||
this->state_->direction == fan::FanDirection::FORWARD ? "forward" : "reverse");
|
||||
failed = failed || !success;
|
||||
}
|
||||
if (this->state_->get_traits().supports_oscillation()) {
|
||||
bool success = this->publish(this->get_oscillation_state_topic(),
|
||||
this->state_->oscillating ? "oscillate_on" : "oscillate_off");
|
||||
|
||||
@@ -15,6 +15,8 @@ class MQTTFanComponent : public mqtt::MQTTComponent {
|
||||
public:
|
||||
explicit MQTTFanComponent(fan::Fan *state);
|
||||
|
||||
MQTT_COMPONENT_CUSTOM_TOPIC(direction, command)
|
||||
MQTT_COMPONENT_CUSTOM_TOPIC(direction, state)
|
||||
MQTT_COMPONENT_CUSTOM_TOPIC(oscillation, command)
|
||||
MQTT_COMPONENT_CUSTOM_TOPIC(oscillation, state)
|
||||
MQTT_COMPONENT_CUSTOM_TOPIC(speed_level, command)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user