1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-02 16:11:53 +00:00

Compare commits

...

73 Commits

Author SHA1 Message Date
Jesse Hills
1e20440c8e Merge pull request #8783 from esphome/bump-2025.5.0b1
2025.5.0b1
2025-05-14 15:54:44 +12:00
Jesse Hills
0630244195 Bump version to 2025.5.0b1 2025-05-14 09:54:26 +12:00
Clyde Stubbs
183659f527 [mipi_spi] New display driver for MIPI DBI devices (#8383)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-14 09:22:58 +12:00
Jesse Hills
4ea63af796 [online_image] Support 24 bit bmp images (#8612) 2025-05-14 09:21:19 +12:00
Samuel Sieb
0aa7911b1b [esp32][esp8266] use low-level pin control for ISR gpio (#8743)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-05-14 08:58:15 +12:00
Mischa Siekmann
032949bc77 [audio] Fix: Decoder stops unnecessarily after a potential failure is detected. (#8776) 2025-05-13 08:35:19 -04:00
Jesse Hills
6f8ee65919 [text_sensor] Fix schema generation (#8773) 2025-05-13 06:34:26 +00:00
Thomas Rupprecht
c5654b4cb2 [esp32] improve gpio (#8709) 2025-05-13 18:24:38 +12:00
Jesse Hills
410b6353fe [switch] Fix schema generation (#8774) 2025-05-13 06:17:54 +00:00
Jesse Hills
a36e1aab8e [cover] Update components to use `cover_schema(...)` (#8770) 2025-05-13 00:29:00 -05:00
Jesse Hills
864ae7a56c [template] Use alarm_control_panel_schema method (#8764) 2025-05-13 00:26:07 -05:00
Jesse Hills
2560d2b9d0 [demo] Clean up schema deprecations, add test (#8771) 2025-05-13 05:16:23 +00:00
Jesse Hills
0cf9b05afd [select] Tidy schema generation (#8775) 2025-05-13 05:07:57 +00:00
Cossid
8b65d1673a Tuya Select - Add int_datapoint option (#8393) 2025-05-13 16:44:51 +12:00
Jesse Hills
5e164b107a [climate] Fix climate_schema (#8772) 2025-05-13 16:35:56 +12:00
DanielV
a83959d738 In case of proto-diff show changes and archive generated (#8698)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-13 14:07:54 +12:00
realzoulou
0ccc5bf714 [gps] Add hdop sensor (#8680)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-13 14:05:34 +12:00
Jesse Hills
bc0956019b [config] Deprecate more *_SCHEMA constants (#8763) 2025-05-13 13:24:13 +12:00
Jesse Hills
49f631d6c5 [schema] Deploy schema after release workflow finished (#8767) 2025-05-13 13:18:23 +12:00
J. Nick Koston
a9d5eb8470 Fix missing recursion guard release on ESP8266 (#8766) 2025-05-13 13:17:37 +12:00
tomaszduda23
7c0546c9f0 [clang] clang tidy support with zephyr (#8352)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-05-12 23:36:34 +00:00
J. Nick Koston
f4eb75e4e0 Avoid iterating clients twice in the api_server loop (#8733) 2025-05-12 17:29:50 -05:00
dependabot[bot]
5b2c19bc86 Bump setuptools from 80.3.1 to 80.4.0 (#8753)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 09:32:13 +12:00
dependabot[bot]
185b84b8b2 Bump zeroconf from 0.146.5 to 0.147.0 (#8754)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 09:31:40 +12:00
tomaszduda23
facf94699e [udp, syslog] fix clang tidy (#8755) 2025-05-12 16:17:28 -05:00
Jesse Hills
58104229e2 [sml] Use text_sensor_schema method (#8762) 2025-05-12 21:16:56 +00:00
Jesse Hills
50c88b7aa7 [ble_client] Use text_sensor_schema method (#8761) 2025-05-12 16:15:57 -05:00
Jesse Hills
81bae96109 [airthings] Remove unnecessary schema (#8760) 2025-05-12 21:05:23 +00:00
Jesse Hills
a3ed090594 [tm1638] Use switch_schema method (#8758) 2025-05-12 20:59:59 +00:00
Jesse Hills
cff1820772 [sprinkler] Use number_schema method (#8759) 2025-05-12 20:59:42 +00:00
Jesse Hills
bdd2774544 [factory_reset] Use switch_schema method (#8757) 2025-05-12 20:58:05 +00:00
Jesse Hills
38790793dd [opentherm] Update to use schema methods (#8756) 2025-05-12 20:53:46 +00:00
Jesse Hills
dcd786d21c [config] Deprecate other *_SCHEMA constants (#8748) 2025-05-12 14:43:38 -05:00
Kevin Ahrendt
71e88fe9b2 [i2s_audio] Correct a microphone with a DC offset signal (#8751) 2025-05-13 07:30:58 +12:00
Guillermo Ruffino
11dcaf7383 [vscode] provide version to editor (#8752) 2025-05-13 07:27:07 +12:00
J. Nick Koston
dded81d622 Fix ESP32 API Disconnects Caused by Concurrent Logger Writes (#8736)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-13 07:03:34 +12:00
Jesse Hills
8324b3244c [config] Add entity schema consts with deprecation log (#8747) 2025-05-12 06:31:36 +00:00
Mateusz Bronk
401c090edd MQTT: fan direction control added (#8022)
Co-authored-by: Mateusz Bronk <mbronk@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-12 10:28:46 +12:00
Jesse Hills
8757957e17 Merge branch 'release' into dev 2025-05-12 10:19:16 +12:00
dependabot[bot]
e2c8a5b638 Bump ruff from 0.11.8 to 0.11.9 (#8735)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-12 10:19:10 +12:00
Jesse Hills
7bb899bfa1 Merge pull request #8746 from esphome/bump-2025.4.2
2025.4.2
2025-05-12 10:18:35 +12:00
dependabot[bot]
3e2359ddff Bump aioesphomeapi from 30.1.0 to 30.2.0 (#8734)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 10:05:24 +12:00
Samuel Sieb
04147a7f27 [one_wire][dallas_temp] adjust timings and reduce disabled interrupts (#8744)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-05-12 09:33:50 +12:00
Jesse Hills
cae3c030d2 Bump version to 2025.4.2 2025-05-12 08:52:13 +12:00
Clyde Stubbs
d7c615ec43 [lvgl] Fix image property processing (#8691) 2025-05-12 08:52:13 +12:00
Clyde Stubbs
1294e8ccd5 [lvgl] Allow padding to be negative (#8671) 2025-05-12 08:52:13 +12:00
Clyde Stubbs
37a2cb07d1 [as3935_i2c] Remove redundant includes (#8677) 2025-05-12 08:52:13 +12:00
Clyde Stubbs
2af3994f79 [display] Fix Rect::inside (#8679) 2025-05-12 08:52:12 +12:00
Jannik
0c0fe81814 Fix HLW8012 sensor not returning values if change_mode_every is set to never (#8456) 2025-05-12 08:52:12 +12:00
Ben Winslow
82c8614315 Fix typo preventing tt21100 from autosetting the touchscreen res. (#8662) 2025-05-12 08:52:12 +12:00
Jesse Hills
a85dc65038 [media_player] Fix actions with id as value (#8654) 2025-05-12 08:52:12 +12:00
Jesse Hills
290b8bdca0 [esp32_ble] Remove explicit and now incorrect ble override for esp32-c6 (#8643) 2025-05-12 08:52:12 +12:00
bdm310
a96ed0b70a [lvgl] Fix unexpected widget update behavior (#8260) 2025-05-12 08:52:12 +12:00
Kevin Ahrendt
cdc1a7c646 [sound_level] Add a new sound level sensor (#8737)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-12 08:51:49 +12:00
Kevin Ahrendt
7f59aff157 [voice_assistant] Bugfix: Properly detect states where mic is running (#8745) 2025-05-12 08:50:47 +12:00
Kevin Ahrendt
cdce59f7f9 [i2s_audio] Fix: Slot bit-width for ESP32 variant (#8738) 2025-05-11 00:27:50 -05:00
Kevin Ahrendt
ff1c3cb52e [audio] Bump esp-audio-libs to version 1.1.4 for speed optimizations (#8739) 2025-05-11 00:25:19 -05:00
Kevin Ahrendt
bec9d91419 [audio, microphone] - Allow MicrophoneSource to passively capture/optimization (#8732) 2025-05-09 16:54:33 -05:00
Jesse Hills
8399d894c1 [config] Use `cv.UNDEFINED instead of adhoc _UNDEF` objects (#8725) 2025-05-09 08:18:52 +00:00
Jesse Hills
e1732c4945 [lock] Move to use `lock_schema(..) instead of LOCK_SCHEMA` (#8728)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-05-09 07:45:32 +00:00
Jesse Hills
ca221d6cb2 [text] Move to use `text_schema(..) instead of TEXT_SCHEMA` (#8727) 2025-05-09 01:24:34 -05:00
Jesse Hills
8a90ce882a [update] Move to use `update_schema(..) instead of UPDATE_SCHEMA` (#8726) 2025-05-09 01:22:43 -05:00
Jesse Hills
b3400a1308 [lock] Tidy up template publish action and lockstate locations (#8729) 2025-05-09 01:19:03 -05:00
Jesse Hills
23fb1bed61 [valve] Move to use `valve_schema(..) instead of VALVE_SCHEMA` (#8730) 2025-05-09 01:14:13 -05:00
Jesse Hills
2b3757dff8 [valve] Tidy up template publish action location (#8731) 2025-05-09 01:05:26 -05:00
Jesse Hills
1da8e99d27 [api] Synchronise api.proto between repos (#8720) 2025-05-09 13:33:28 +12:00
John
e94e71ded8 ATM90E32 Semi-automatic calibration & Status fields (#8529)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-09 12:50:59 +12:00
J. Nick Koston
00f20c1e55 Optimize bluetooth_proxy memory copy and reduce reallocs (#8723) 2025-05-09 12:49:50 +12:00
J. Nick Koston
45d019a7e4 Improve BLE Connection Reliability by Enabling Software Coexistence (#8683)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-09 12:18:19 +12:00
J. Nick Koston
8465017db9 Consolidate write_raw_ implementation to reduce code duplication (#8717) 2025-05-09 12:10:44 +12:00
J. Nick Koston
782d748210 Increase zeroconf timeout to 10 seconds (#8670) 2025-05-09 12:05:59 +12:00
dependabot[bot]
b01d85a974 Bump puremagic from 1.28 to 1.29 (#8722)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-08 15:02:16 -05:00
dependabot[bot]
797a4c61f2 Bump ruff from 0.11.7 to 0.11.8 (#8721)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-08 15:01:52 -05:00
200 changed files with 8004 additions and 1695 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

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

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

View 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

View 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

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

View 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 = {}

View 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

View File

@@ -0,0 +1,10 @@
from .ili import ILI9341
ILI9341.extend(
"ESP32-2432S028",
data_rate="40MHz",
cs_pin=15,
dc_pin=2,
)
models = {}

View 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 = {}

View 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 = {}

View 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 = {}

View 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 = {}

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

View File

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

View File

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

View File

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