Merge branch 'dev' into gp8211

This commit is contained in:
haudamekki 2024-10-27 19:07:38 +01:00 committed by GitHub
commit fa5a027946
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
95 changed files with 1833 additions and 503 deletions

View file

@ -7,11 +7,16 @@
- [ ] Bugfix (non-breaking change which fixes an issue) - [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality) - [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Code quality improvements to existing code or addition of tests
- [ ] Other - [ ] Other
**Related issue or feature (if applicable):** fixes <link to issue> **Related issue or feature (if applicable):**
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs#<esphome-docs PR number goes here> - fixes <link to issue>
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
- esphome/esphome-docs#<esphome-docs PR number goes here>
## Test Environment ## Test Environment
@ -23,12 +28,6 @@
- [ ] RTL87xx - [ ] RTL87xx
## Example entry for `config.yaml`: ## Example entry for `config.yaml`:
<!--
Supplying a configuration snippet, makes it easier for a maintainer to test
your PR. Furthermore, for new integrations, it gives an impression of how
the configuration would look like.
Note: Remove this section if this PR does not have an example entry.
-->
```yaml ```yaml
# Example config.yaml # Example config.yaml

View file

@ -17,12 +17,12 @@ runs:
steps: steps:
- name: Set up Python ${{ inputs.python-version }} - name: Set up Python ${{ inputs.python-version }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.1.1 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length

View file

@ -23,7 +23,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: "3.11" python-version: "3.11"

View file

@ -42,7 +42,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: "3.9" python-version: "3.9"
- name: Set up Docker Buildx - name: Set up Docker Buildx

View file

@ -41,12 +41,12 @@ jobs:
run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.1.1 uses: actions/cache@v4.1.2
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@ -302,14 +302,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@v4.1.1 uses: actions/cache@v4.1.2
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }} key: platformio-${{ matrix.pio_cache_key }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@v4.1.1 uses: actions/cache/restore@v4.1.2
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }} key: platformio-${{ matrix.pio_cache_key }}

View file

@ -53,7 +53,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: "3.x" python-version: "3.x"
- name: Set up python environment - name: Set up python environment
@ -85,7 +85,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: "3.9" python-version: "3.9"

View file

@ -22,7 +22,7 @@ jobs:
path: lib/home-assistant path: lib/home-assistant
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: 3.12 python-version: 3.12

View file

@ -199,10 +199,11 @@ esphome/components/htu31d/* @betterengineering
esphome/components/hydreon_rgxx/* @functionpointer esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/hyt271/* @Philippe12 esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core esphome/components/i2c/* @esphome/core
esphome/components/i2c_device/* @gabest11
esphome/components/i2s_audio/* @jesserockz esphome/components/i2s_audio/* @jesserockz
esphome/components/i2s_audio/media_player/* @jesserockz esphome/components/i2s_audio/media_player/* @jesserockz
esphome/components/i2s_audio/microphone/* @jesserockz esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
esphome/components/iaqcore/* @yozik04 esphome/components/iaqcore/* @yozik04
esphome/components/ili9xxx/* @clydebarrow @nielsnl68 esphome/components/ili9xxx/* @clydebarrow @nielsnl68
esphome/components/improv_base/* @esphome/core esphome/components/improv_base/* @esphome/core
@ -377,7 +378,7 @@ esphome/components/smt100/* @piechade
esphome/components/sn74hc165/* @jesserockz esphome/components/sn74hc165/* @jesserockz
esphome/components/socket/* @esphome/core esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/speaker/* @jesserockz esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi/* @clydebarrow @esphome/core
esphome/components/spi_device/* @clydebarrow esphome/components/spi_device/* @clydebarrow
esphome/components/spi_led_strip/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow

View file

@ -271,7 +271,8 @@ async def to_code(config):
pos += 1 pos += 1
elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]: elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]:
data = [0 for _ in range(height * width * 2 * frames)] bytes_per_pixel = 3 if transparent else 2
data = [0 for _ in range(height * width * bytes_per_pixel * frames)]
pos = 0 pos = 0
for frameIndex in range(frames): for frameIndex in range(frames):
image.seek(frameIndex) image.seek(frameIndex)
@ -288,17 +289,13 @@ async def to_code(config):
G = g >> 2 G = g >> 2
B = b >> 3 B = b >> 3
rgb = (R << 11) | (G << 5) | B rgb = (R << 11) | (G << 5) | B
if transparent:
if rgb == 0x0020:
rgb = 0
if a < 0x80:
rgb = 0x0020
data[pos] = rgb >> 8 data[pos] = rgb >> 8
pos += 1 pos += 1
data[pos] = rgb & 0xFF data[pos] = rgb & 0xFF
pos += 1 pos += 1
if transparent:
data[pos] = a
pos += 1
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
width8 = ((width + 7) // 8) * 8 width8 = ((width + 7) // 8) * 8

View file

@ -62,7 +62,7 @@ void Animation::set_frame(int frame) {
} }
void Animation::update_data_start_() { void Animation::update_data_start_() {
const uint32_t image_size = image_type_to_width_stride(this->width_, this->type_) * this->height_; const uint32_t image_size = this->get_width_stride() * this->height_;
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_; this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
} }

View file

@ -45,7 +45,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t, cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t,
cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t, cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t,
cv.Optional(CONF_IBEACON_UUID): cv.uuid, cv.Optional(CONF_IBEACON_UUID): esp32_ble_tracker.bt_uuid,
} }
) )
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
@ -79,7 +79,7 @@ async def to_code(config):
cg.add(var.set_service_uuid128(uuid128)) cg.add(var.set_service_uuid128(uuid128))
if ibeacon_uuid := config.get(CONF_IBEACON_UUID): if ibeacon_uuid := config.get(CONF_IBEACON_UUID):
ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(ibeacon_uuid)) ibeacon_uuid = esp32_ble_tracker.as_reversed_hex_array(ibeacon_uuid)
cg.add(var.set_ibeacon_uuid(ibeacon_uuid)) cg.add(var.set_ibeacon_uuid(ibeacon_uuid))
if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None: if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None:

View file

@ -395,6 +395,13 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All(
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.string_strict, cv.Optional(CONF_SOURCE): cv.string_strict,
cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
{
cv.Optional(
CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False
): cv.boolean,
}
),
} }
), ),
_arduino_check_versions, _arduino_check_versions,
@ -494,6 +501,9 @@ async def to_code(config):
conf = config[CONF_FRAMEWORK] conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
if CONF_ADVANCED in conf and conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
add_extra_script( add_extra_script(
"post", "post",
"post_build.py", "post_build.py",
@ -540,8 +550,6 @@ async def to_code(config):
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
if conf[CONF_ADVANCED].get(CONF_IGNORE_EFUSE_MAC_CRC): if conf[CONF_ADVANCED].get(CONF_IGNORE_EFUSE_MAC_CRC):
add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True) add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True)
if (framework_ver.major, framework_ver.minor) >= (4, 4): if (framework_ver.major, framework_ver.minor) >= (4, 4):

View file

@ -103,6 +103,173 @@ ESP32_BOARD_PINS = {
"LED": 13, "LED": 13,
"LED_BUILTIN": 13, "LED_BUILTIN": 13,
}, },
"adafruit_feather_esp32s3": {
"BUTTON": 0,
"A0": 18,
"A1": 17,
"A2": 16,
"A3": 15,
"A4": 14,
"A5": 8,
"SCK": 36,
"MOSI": 35,
"MISO": 37,
"RX": 38,
"TX": 39,
"SCL": 4,
"SDA": 3,
"NEOPIXEL": 33,
"PIN_NEOPIXEL": 33,
"NEOPIXEL_POWER": 21,
"I2C_POWER": 7,
"LED": 13,
"LED_BUILTIN": 13,
},
"adafruit_feather_esp32s3_nopsram": {
"BUTTON": 0,
"A0": 18,
"A1": 17,
"A2": 16,
"A3": 15,
"A4": 14,
"A5": 8,
"SCK": 36,
"MOSI": 35,
"MISO": 37,
"RX": 38,
"TX": 39,
"SCL": 4,
"SDA": 3,
"NEOPIXEL": 33,
"PIN_NEOPIXEL": 33,
"NEOPIXEL_POWER": 21,
"I2C_POWER": 7,
"LED": 13,
"LED_BUILTIN": 13,
},
"adafruit_feather_esp32s3_tft": {
"BUTTON": 0,
"A0": 18,
"A1": 17,
"A2": 16,
"A3": 15,
"A4": 14,
"A5": 8,
"SCK": 36,
"MOSI": 35,
"MISO": 37,
"RX": 2,
"TX": 1,
"SCL": 41,
"SDA": 42,
"NEOPIXEL": 33,
"PIN_NEOPIXEL": 33,
"NEOPIXEL_POWER": 34,
"TFT_I2C_POWER": 21,
"TFT_CS": 7,
"TFT_DC": 39,
"TFT_RESET": 40,
"TFT_BACKLIGHT": 45,
"LED": 13,
"LED_BUILTIN": 13,
},
"adafruit_funhouse_esp32s2": {
"BUTTON_UP": 5,
"BUTTON_DOWN": 3,
"BUTTON_SELECT": 4,
"DOTSTAR_DATA": 14,
"DOTSTAR_CLOCK": 15,
"PIR_SENSE": 16,
"A0": 17,
"A1": 2,
"A2": 1,
"CAP6": 6,
"CAP7": 7,
"CAP8": 8,
"CAP9": 9,
"CAP10": 10,
"CAP11": 11,
"CAP12": 12,
"CAP13": 13,
"SPEAKER": 42,
"LED": 37,
"LIGHT": 18,
"TFT_MOSI": 35,
"TFT_SCK": 36,
"TFT_CS": 40,
"TFT_DC": 39,
"TFT_RESET": 41,
"TFT_BACKLIGHT": 21,
"RED_LED": 31,
"BUTTON": 0,
},
"adafruit_itsybitsy_esp32": {
"A0": 25,
"A1": 26,
"A2": 4,
"A3": 38,
"A4": 37,
"A5": 36,
"SCK": 19,
"MOSI": 21,
"MISO": 22,
"SCL": 27,
"SDA": 15,
"TX": 20,
"RX": 8,
"NEOPIXEL": 0,
"PIN_NEOPIXEL": 0,
"NEOPIXEL_POWER": 2,
"BUTTON": 35,
},
"adafruit_magtag29_esp32s2": {
"A1": 18,
"BUTTON_A": 15,
"BUTTON_B": 14,
"BUTTON_C": 12,
"BUTTON_D": 11,
"SDA": 33,
"SCL": 34,
"SPEAKER": 17,
"SPEAKER_ENABLE": 16,
"VOLTAGE_MONITOR": 4,
"ACCELEROMETER_INT": 9,
"ACCELEROMETER_INTERRUPT": 9,
"LIGHT": 3,
"NEOPIXEL": 1,
"PIN_NEOPIXEL": 1,
"NEOPIXEL_POWER": 21,
"EPD_BUSY": 5,
"EPD_RESET": 6,
"EPD_DC": 7,
"EPD_CS": 8,
"EPD_MOSI": 35,
"EPD_SCK": 36,
"EPD_MISO": 37,
"BUTTON": 0,
"LED": 13,
"LED_BUILTIN": 13,
},
"adafruit_metro_esp32s2": {
"A0": 17,
"A1": 18,
"A2": 1,
"A3": 2,
"A4": 3,
"A5": 4,
"RX": 38,
"TX": 37,
"SCL": 34,
"SDA": 33,
"MISO": 37,
"SCK": 36,
"MOSI": 35,
"NEOPIXEL": 45,
"PIN_NEOPIXEL": 45,
"LED": 42,
"LED_BUILTIN": 42,
"BUTTON": 0,
},
"adafruit_qtpy_esp32c3": { "adafruit_qtpy_esp32c3": {
"A0": 4, "A0": 4,
"A1": 3, "A1": 3,
@ -141,6 +308,26 @@ ESP32_BOARD_PINS = {
"BUTTON": 0, "BUTTON": 0,
"SWITCH": 0, "SWITCH": 0,
}, },
"adafruit_qtpy_esp32s3_nopsram": {
"A0": 18,
"A1": 17,
"A2": 9,
"A3": 8,
"SDA": 7,
"SCL": 6,
"MOSI": 35,
"MISO": 37,
"SCK": 36,
"RX": 16,
"TX": 5,
"SDA1": 41,
"SCL1": 40,
"NEOPIXEL": 39,
"PIN_NEOPIXEL": 39,
"NEOPIXEL_POWER": 38,
"BUTTON": 0,
"SWITCH": 0,
},
"adafruit_qtpy_esp32": { "adafruit_qtpy_esp32": {
"A0": 26, "A0": 26,
"A1": 25, "A1": 25,
@ -1068,7 +1255,18 @@ ESP32_BOARD_PINS = {
"_VBAT": 35, "_VBAT": 35,
}, },
"wemosbat": {"LED": 16}, "wemosbat": {"LED": 16},
"wesp32": {"MISO": 32, "SCL": 4, "SDA": 15}, "wesp32": {
"MISO": 32,
"MOSI": 23,
"SCK": 18,
"SCL": 4,
"SDA": 15,
"MISO1": 12,
"MOSI1": 13,
"SCK1": 14,
"SCL1": 5,
"SDA1": 33,
},
"widora-air": { "widora-air": {
"A1": 39, "A1": 39,
"A2": 35, "A2": 35,

View file

@ -16,7 +16,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{ {
cv.GenerateID(): cv.declare_id(GP8403Output), cv.GenerateID(): cv.declare_id(GP8403Output),
cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403), cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403),
cv.Required(CONF_CHANNEL): cv.one_of(0, 1), cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=1),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)

View file

@ -16,7 +16,7 @@ from .const import KEY_HOST
from .gpio import host_pin_to_code # noqa from .gpio import host_pin_to_code # noqa
CODEOWNERS = ["@esphome/core", "@clydebarrow"] CODEOWNERS = ["@esphome/core", "@clydebarrow"]
AUTO_LOAD = ["network"] AUTO_LOAD = ["network", "preferences"]
def set_core_data(config): def set_core_data(config):

View file

@ -0,0 +1,26 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import CONF_ID
DEPENDENCIES = ["i2c"]
CODEOWNERS = ["@gabest11"]
MULTI_CONF = True
i2c_device_ns = cg.esphome_ns.namespace("i2c_device")
I2CDeviceComponent = i2c_device_ns.class_(
"I2CDeviceComponent", cg.Component, i2c.I2CDevice
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(I2CDeviceComponent),
}
).extend(i2c.i2c_device_schema(None))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

View file

@ -0,0 +1,17 @@
#include "i2c_device.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cinttypes>
namespace esphome {
namespace i2c_device {
static const char *const TAG = "i2c_device";
void I2CDeviceComponent::dump_config() {
ESP_LOGCONFIG(TAG, "I2CDevice");
LOG_I2C_DEVICE(this);
}
} // namespace i2c_device
} // namespace esphome

View file

@ -0,0 +1,18 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace i2c_device {
class I2CDeviceComponent : public Component, public i2c::I2CDevice {
public:
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
};
} // namespace i2c_device
} // namespace esphome

View file

@ -17,7 +17,7 @@ from .. import (
) )
AUTO_LOAD = ["audio"] AUTO_LOAD = ["audio"]
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz", "@kahrendt"]
DEPENDENCIES = ["i2s_audio"] DEPENDENCIES = ["i2s_audio"]
I2SAudioSpeaker = i2s_audio_ns.class_( I2SAudioSpeaker = i2s_audio_ns.class_(

View file

@ -32,6 +32,7 @@ enum SpeakerEventGroupBits : uint32_t {
STATE_RUNNING = (1 << 11), STATE_RUNNING = (1 << 11),
STATE_STOPPING = (1 << 12), STATE_STOPPING = (1 << 12),
STATE_STOPPED = (1 << 13), STATE_STOPPED = (1 << 13),
ERR_INVALID_FORMAT = (1 << 14),
ERR_TASK_FAILED_TO_START = (1 << 15), ERR_TASK_FAILED_TO_START = (1 << 15),
ERR_ESP_INVALID_STATE = (1 << 16), ERR_ESP_INVALID_STATE = (1 << 16),
ERR_ESP_INVALID_ARG = (1 << 17), ERR_ESP_INVALID_ARG = (1 << 17),
@ -104,16 +105,6 @@ void I2SAudioSpeaker::setup() {
void I2SAudioSpeaker::loop() { void I2SAudioSpeaker::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) {
this->status_set_error("Failed to start speaker task");
}
if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) {
uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS;
ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits)));
this->status_set_warning();
}
if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) { if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) {
ESP_LOGD(TAG, "Starting Speaker"); ESP_LOGD(TAG, "Starting Speaker");
this->state_ = speaker::STATE_STARTING; this->state_ = speaker::STATE_STARTING;
@ -139,12 +130,64 @@ void I2SAudioSpeaker::loop() {
this->speaker_task_handle_ = nullptr; this->speaker_task_handle_ = nullptr;
} }
} }
if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) {
this->status_set_error("Failed to start speaker task");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START);
}
if (event_group_bits & SpeakerEventGroupBits::ERR_INVALID_FORMAT) {
this->status_set_error("Failed to adjust I2S bus to match the incoming audio");
ESP_LOGE(TAG,
"Incompatible audio format: sample rate = %" PRIu32 ", channels = %" PRIu8 ", bits per sample = %" PRIu8,
this->audio_stream_info_.sample_rate, this->audio_stream_info_.channels,
this->audio_stream_info_.bits_per_sample);
}
if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) {
uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS;
ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits)));
this->status_set_warning();
}
} }
void I2SAudioSpeaker::set_volume(float volume) { void I2SAudioSpeaker::set_volume(float volume) {
this->volume_ = volume; this->volume_ = volume;
ssize_t decibel_index = remap<ssize_t, float>(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1); #ifdef USE_AUDIO_DAC
this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index]; if (this->audio_dac_ != nullptr) {
if (volume > 0.0) {
this->audio_dac_->set_mute_off();
}
this->audio_dac_->set_volume(volume);
} else
#endif
{
// Fallback to software volume control by using a Q15 fixed point scaling factor
ssize_t decibel_index = remap<ssize_t, float>(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1);
this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index];
}
}
void I2SAudioSpeaker::set_mute_state(bool mute_state) {
this->mute_state_ = mute_state;
#ifdef USE_AUDIO_DAC
if (this->audio_dac_) {
if (mute_state) {
this->audio_dac_->set_mute_on();
} else {
this->audio_dac_->set_mute_off();
}
} else
#endif
{
if (mute_state) {
// Fallback to software volume control and scale by 0
this->q15_volume_factor_ = 0;
} else {
// Revert to previous volume when unmuting
this->set_volume(this->volume_);
}
}
} }
size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
@ -275,6 +318,9 @@ void I2SAudioSpeaker::speaker_task(void *params) {
i2s_zero_dma_buffer(this_speaker->parent_->get_port()); i2s_zero_dma_buffer(this_speaker->parent_->get_port());
} }
} }
} else {
// Couldn't configure the I2S port to be compatible with the incoming audio
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_INVALID_FORMAT);
} }
i2s_zero_dma_buffer(this_speaker->parent_->get_port()); i2s_zero_dma_buffer(this_speaker->parent_->get_port());
@ -288,7 +334,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
} }
void I2SAudioSpeaker::start() { void I2SAudioSpeaker::start() {
if (this->is_failed()) if (this->is_failed() || this->status_has_error())
return; return;
if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING))
return; return;

View file

@ -49,11 +49,17 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
bool has_buffered_data() const override; bool has_buffered_data() const override;
/// @brief Sets the volume of the speaker. It is implemented as a software volume control. /// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailble, it is
/// Overrides the default setter to convert the floating point volume to a Q15 fixed-point factor. /// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a
/// @param volume /// Q15 fixed-point factor.
/// @param volume between 0.0 and 1.0
void set_volume(float volume) override; void set_volume(float volume) override;
float get_volume() override { return this->volume_; }
/// @brief Mutes or unmute the speaker. Uses the speaker's configured audio dac component. If unavailble, it is
/// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a
/// Q15 fixed-point factor.
/// @param mute_state true for muting, false for unmuting
void set_mute_state(bool mute_state) override;
protected: protected:
/// @brief Function for the FreeRTOS task handling audio output. /// @brief Function for the FreeRTOS task handling audio output.

View file

@ -361,24 +361,21 @@ async def to_code(config):
elif config[CONF_TYPE] in ["RGB565"]: elif config[CONF_TYPE] in ["RGB565"]:
image = image.convert("RGBA") image = image.convert("RGBA")
pixels = list(image.getdata()) pixels = list(image.getdata())
data = [0 for _ in range(height * width * 2)] bytes_per_pixel = 3 if transparent else 2
data = [0 for _ in range(height * width * bytes_per_pixel)]
pos = 0 pos = 0
for r, g, b, a in pixels: for r, g, b, a in pixels:
R = r >> 3 R = r >> 3
G = g >> 2 G = g >> 2
B = b >> 3 B = b >> 3
rgb = (R << 11) | (G << 5) | B rgb = (R << 11) | (G << 5) | B
if transparent:
if rgb == 0x0020:
rgb = 0
if a < 0x80:
rgb = 0x0020
data[pos] = rgb >> 8 data[pos] = rgb >> 8
pos += 1 pos += 1
data[pos] = rgb & 0xFF data[pos] = rgb & 0xFF
pos += 1 pos += 1
if transparent:
data[pos] = a
pos += 1
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
if transparent: if transparent:

View file

@ -88,7 +88,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
this->dsc_.header.reserved = 0; this->dsc_.header.reserved = 0;
this->dsc_.header.w = this->width_; this->dsc_.header.w = this->width_;
this->dsc_.header.h = this->height_; this->dsc_.header.h = this->height_;
this->dsc_.data_size = image_type_to_width_stride(this->dsc_.header.w * this->dsc_.header.h, this->get_type()); this->dsc_.data_size = this->get_width_stride() * this->get_height();
switch (this->get_type()) { switch (this->get_type()) {
case IMAGE_TYPE_BINARY: case IMAGE_TYPE_BINARY:
this->dsc_.header.cf = LV_IMG_CF_ALPHA_1BIT; this->dsc_.header.cf = LV_IMG_CF_ALPHA_1BIT;
@ -104,17 +104,17 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
case IMAGE_TYPE_RGB565: case IMAGE_TYPE_RGB565:
#if LV_COLOR_DEPTH == 16 #if LV_COLOR_DEPTH == 16
this->dsc_.header.cf = this->has_transparency() ? LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED : LV_IMG_CF_TRUE_COLOR; this->dsc_.header.cf = this->has_transparency() ? LV_IMG_CF_TRUE_COLOR_ALPHA : LV_IMG_CF_TRUE_COLOR;
#else #else
this->dsc_.header.cf = LV_IMG_CF_RGB565; this->dsc_.header.cf = LV_IMG_CF_RGB565;
#endif #endif
break; break;
case image::IMAGE_TYPE_RGBA: case IMAGE_TYPE_RGBA:
#if LV_COLOR_DEPTH == 32 #if LV_COLOR_DEPTH == 32
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR; this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
#else #else
this->dsc_.header.cf = LV_IMG_CF_RGBA8888; this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
#endif #endif
break; break;
} }
@ -147,21 +147,21 @@ Color Image::get_rgb24_pixel_(int x, int y) const {
return color; return color;
} }
Color Image::get_rgb565_pixel_(int x, int y) const { Color Image::get_rgb565_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 2; const uint8_t *pos = this->data_start_;
uint16_t rgb565 = if (this->transparent_) {
progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); pos += (x + y * this->width_) * 3;
} else {
pos += (x + y * this->width_) * 2;
}
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
auto r = (rgb565 & 0xF800) >> 11; auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5; auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F; auto b = rgb565 & 0x001F;
Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); auto a = this->transparent_ ? progmem_read_byte(pos + 2) : 0xFF;
if (rgb565 == 0x0020 && transparent_) { Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a);
// darkest green has been defined as transparent color for transparent RGB565 images.
color.w = 0;
} else {
color.w = 0xFF;
}
return color; return color;
} }
Color Image::get_grayscale_pixel_(int x, int y) const { Color Image::get_grayscale_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_); const uint32_t pos = (x + y * this->width_);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos); const uint8_t gray = progmem_read_byte(this->data_start_ + pos);

View file

@ -3,12 +3,7 @@
#include "esphome/components/display/display.h" #include "esphome/components/display/display.h"
#ifdef USE_LVGL #ifdef USE_LVGL
// required for clang-tidy #include "esphome/components/lvgl/lvgl_proxy.h"
#ifndef LV_CONF_H
#define LV_CONF_SKIP 1 // NOLINT
#endif // LV_CONF_H
#include <lvgl.h>
#endif // USE_LVGL #endif // USE_LVGL
namespace esphome { namespace esphome {
@ -22,24 +17,6 @@ enum ImageType {
IMAGE_TYPE_RGBA = 4, IMAGE_TYPE_RGBA = 4,
}; };
inline int image_type_to_bpp(ImageType type) {
switch (type) {
case IMAGE_TYPE_BINARY:
return 1;
case IMAGE_TYPE_GRAYSCALE:
return 8;
case IMAGE_TYPE_RGB565:
return 16;
case IMAGE_TYPE_RGB24:
return 24;
case IMAGE_TYPE_RGBA:
return 32;
}
return 0;
}
inline int image_type_to_width_stride(int width, ImageType type) { return (width * image_type_to_bpp(type) + 7u) / 8u; }
class Image : public display::BaseImage { class Image : public display::BaseImage {
public: public:
Image(const uint8_t *data_start, int width, int height, ImageType type); Image(const uint8_t *data_start, int width, int height, ImageType type);
@ -49,6 +26,25 @@ class Image : public display::BaseImage {
const uint8_t *get_data_start() const { return this->data_start_; } const uint8_t *get_data_start() const { return this->data_start_; }
ImageType get_type() const; ImageType get_type() const;
int get_bpp() const {
switch (this->type_) {
case IMAGE_TYPE_BINARY:
return 1;
case IMAGE_TYPE_GRAYSCALE:
return 8;
case IMAGE_TYPE_RGB565:
return this->transparent_ ? 24 : 16;
case IMAGE_TYPE_RGB24:
return 24;
case IMAGE_TYPE_RGBA:
return 32;
}
return 0;
}
/// Return the stride of the image in bytes, that is, the distance in bytes
/// between two consecutive rows of pixels.
uint32_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; }
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
void set_transparency(bool transparent) { transparent_ = transparent; } void set_transparency(bool transparent) { transparent_ = transparent; }

View file

@ -46,7 +46,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@kuba2k2"] CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = [] AUTO_LOAD = ["preferences"]
def _detect_variant(value): def _detect_variant(value):

View file

@ -33,7 +33,7 @@ from .schemas import (
FLEX_OBJ_SCHEMA, FLEX_OBJ_SCHEMA,
GRID_CELL_SCHEMA, GRID_CELL_SCHEMA,
LAYOUT_SCHEMAS, LAYOUT_SCHEMAS,
STATE_SCHEMA, STYLE_SCHEMA,
WIDGET_TYPES, WIDGET_TYPES,
any_widget_schema, any_widget_schema,
container_schema, container_schema,
@ -342,7 +342,7 @@ CONFIG_SCHEMA = (
), ),
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list( cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)}) cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)})
.extend(STATE_SCHEMA) .extend(STYLE_SCHEMA)
.extend( .extend(
{ {
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,

View file

@ -2,9 +2,9 @@ import esphome.codegen as cg
from esphome.components import light from esphome.components import light
from esphome.components.light import LightOutput from esphome.components.light import LightOutput
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_GAMMA_CORRECT, CONF_LED, CONF_OUTPUT_ID from esphome.const import CONF_GAMMA_CORRECT, CONF_OUTPUT_ID
from ..defines import CONF_LVGL_ID from ..defines import CONF_LVGL_ID, CONF_WIDGET
from ..lvcode import LvContext from ..lvcode import LvContext
from ..schemas import LVGL_SCHEMA from ..schemas import LVGL_SCHEMA
from ..types import LvType, lvgl_ns from ..types import LvType, lvgl_ns
@ -15,7 +15,7 @@ LVLight = lvgl_ns.class_("LVLight", LightOutput)
CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
{ {
cv.Optional(CONF_GAMMA_CORRECT, default=0.0): cv.positive_float, cv.Optional(CONF_GAMMA_CORRECT, default=0.0): cv.positive_float,
cv.Required(CONF_LED): cv.use_id(lv_led_t), cv.Required(CONF_WIDGET): cv.use_id(lv_led_t),
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LVLight), cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LVLight),
} }
).extend(LVGL_SCHEMA) ).extend(LVGL_SCHEMA)
@ -26,7 +26,7 @@ async def to_code(config):
await light.register_light(var, config) await light.register_light(var, config)
paren = await cg.get_variable(config[CONF_LVGL_ID]) paren = await cg.get_variable(config[CONF_LVGL_ID])
widget = await get_widgets(config, CONF_LED) widget = await get_widgets(config, CONF_WIDGET)
widget = widget[0] widget = widget[0]
await wait_for_widgets() await wait_for_widgets()
async with LvContext(paren) as ctx: async with LvContext(paren) as ctx:

View file

@ -267,6 +267,9 @@ def angle(value):
return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10) return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10)
lv_angle = LValidator(angle, uint32)
@schema_extractor("one_of") @schema_extractor("one_of")
def size_validator(value): def size_validator(value):
"""A size in one axis - one of "size_content", a number (pixels) or a percentage""" """A size in one axis - one of "size_content", a number (pixels) or a percentage"""
@ -274,10 +277,8 @@ def size_validator(value):
return ["SIZE_CONTENT", "number of pixels", "percentage"] return ["SIZE_CONTENT", "number of pixels", "percentage"]
if isinstance(value, str) and value.lower().endswith("px"): if isinstance(value, str) and value.lower().endswith("px"):
value = cv.int_(value[:-2]) value = cv.int_(value[:-2])
if isinstance(value, str) and not value.endswith("%"): if isinstance(value, str) and value.upper() == "SIZE_CONTENT":
if value.upper() == "SIZE_CONTENT": return "LV_SIZE_CONTENT"
return "LV_SIZE_CONTENT"
raise cv.Invalid("must be 'size_content', a percentage or an integer (pixels)")
return pixels_or_percent_validator(value) return pixels_or_percent_validator(value)
@ -403,6 +404,7 @@ class TextValidator(LValidator):
lv_text = TextValidator() lv_text = TextValidator()
lv_float = LValidator(cv.float_, cg.float_) lv_float = LValidator(cv.float_, cg.float_)
lv_int = LValidator(cv.int_, cg.int_) lv_int = LValidator(cv.int_, cg.int_)
lv_positive_int = LValidator(cv.positive_int, cg.int_)
lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255)) lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255))

View file

@ -84,6 +84,7 @@ lv_event_code_t lv_api_event; // NOLINT
lv_event_code_t lv_update_event; // NOLINT lv_event_code_t lv_update_event; // NOLINT
void LvglComponent::dump_config() { void LvglComponent::dump_config() {
ESP_LOGCONFIG(TAG, "LVGL:"); ESP_LOGCONFIG(TAG, "LVGL:");
ESP_LOGCONFIG(TAG, " Display width/height: %d x %d", this->disp_drv_.hor_res, this->disp_drv_.ver_res);
ESP_LOGCONFIG(TAG, " Rotation: %d", this->rotation); ESP_LOGCONFIG(TAG, " Rotation: %d", this->rotation);
ESP_LOGCONFIG(TAG, " Draw rounding: %d", (int) this->draw_rounding); ESP_LOGCONFIG(TAG, " Draw rounding: %d", (int) this->draw_rounding);
} }
@ -145,7 +146,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
lv_color_t *dst = this->rotate_buf_; lv_color_t *dst = this->rotate_buf_;
switch (this->rotation) { switch (this->rotation) {
case display::DISPLAY_ROTATION_90_DEGREES: case display::DISPLAY_ROTATION_90_DEGREES:
for (lv_coord_t x = height - 1; x-- != 0;) { for (lv_coord_t x = height; x-- != 0;) {
for (lv_coord_t y = 0; y != width; y++) { for (lv_coord_t y = 0; y != width; y++) {
dst[y * height + x] = *ptr++; dst[y * height + x] = *ptr++;
} }
@ -426,19 +427,8 @@ LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buf
this->disp_drv_.full_refresh = this->full_refresh_; this->disp_drv_.full_refresh = this->full_refresh_;
this->disp_drv_.flush_cb = static_flush_cb; this->disp_drv_.flush_cb = static_flush_cb;
this->disp_drv_.rounder_cb = rounder_cb; this->disp_drv_.rounder_cb = rounder_cb;
// reset the display rotation since we will handle all rotations this->disp_drv_.hor_res = (lv_coord_t) display->get_width();
display->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); this->disp_drv_.ver_res = (lv_coord_t) display->get_height();
switch (this->rotation) {
default:
this->disp_drv_.hor_res = (lv_coord_t) display->get_width();
this->disp_drv_.ver_res = (lv_coord_t) display->get_height();
break;
case display::DISPLAY_ROTATION_90_DEGREES:
case display::DISPLAY_ROTATION_270_DEGREES:
this->disp_drv_.ver_res = (lv_coord_t) display->get_width();
this->disp_drv_.hor_res = (lv_coord_t) display->get_height();
break;
}
this->disp_ = lv_disp_drv_register(&this->disp_drv_); this->disp_ = lv_disp_drv_register(&this->disp_drv_);
} }
@ -459,6 +449,9 @@ void LvglComponent::setup() {
esp_log_printf_(LVGL_LOG_LEVEL, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); esp_log_printf_(LVGL_LOG_LEVEL, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf);
}); });
#endif #endif
// Rotation will be handled by our drawing function, so reset the display rotation.
for (auto *display : this->displays_)
display->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);
this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0); this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0);
lv_disp_trig_activity(this->disp_); lv_disp_trig_activity(this->disp_);
ESP_LOGCONFIG(TAG, "LVGL Setup complete"); ESP_LOGCONFIG(TAG, "LVGL Setup complete");

View file

@ -0,0 +1,17 @@
#pragma once
/**
* This header is for use in components that might or might not use LVGL. There is a platformio bug where
the mere mention of a header file, even if ifdefed, causes the build to fail. This is a workaround, since if this
file is included in the build, LVGL is always included.
*/
#ifdef USE_LVGL
// required for clang-tidy
#ifndef LV_CONF_H
#define LV_CONF_SKIP 1 // NOLINT
#endif // LV_CONF_H
#include <lvgl.h>
namespace esphome {
namespace lvgl {} // namespace lvgl
} // namespace esphome
#endif // USE_LVGL

View file

@ -91,7 +91,7 @@ STYLE_PROPS = {
"arc_opa": lvalid.opacity, "arc_opa": lvalid.opacity,
"arc_color": lvalid.lv_color, "arc_color": lvalid.lv_color,
"arc_rounded": lvalid.lv_bool, "arc_rounded": lvalid.lv_bool,
"arc_width": cv.positive_int, "arc_width": lvalid.lv_positive_int,
"anim_time": lvalid.lv_milliseconds, "anim_time": lvalid.lv_milliseconds,
"bg_color": lvalid.lv_color, "bg_color": lvalid.lv_color,
"bg_grad": lv_gradient, "bg_grad": lv_gradient,
@ -111,7 +111,7 @@ STYLE_PROPS = {
"border_side": df.LvConstant( "border_side": df.LvConstant(
"LV_BORDER_SIDE_", "NONE", "TOP", "BOTTOM", "LEFT", "RIGHT", "INTERNAL" "LV_BORDER_SIDE_", "NONE", "TOP", "BOTTOM", "LEFT", "RIGHT", "INTERNAL"
).several_of, ).several_of,
"border_width": cv.positive_int, "border_width": lvalid.lv_positive_int,
"clip_corner": lvalid.lv_bool, "clip_corner": lvalid.lv_bool,
"color_filter_opa": lvalid.opacity, "color_filter_opa": lvalid.opacity,
"height": lvalid.size, "height": lvalid.size,
@ -134,11 +134,11 @@ STYLE_PROPS = {
"pad_right": lvalid.pixels, "pad_right": lvalid.pixels,
"pad_top": lvalid.pixels, "pad_top": lvalid.pixels,
"shadow_color": lvalid.lv_color, "shadow_color": lvalid.lv_color,
"shadow_ofs_x": cv.int_, "shadow_ofs_x": lvalid.lv_int,
"shadow_ofs_y": cv.int_, "shadow_ofs_y": lvalid.lv_int,
"shadow_opa": lvalid.opacity, "shadow_opa": lvalid.opacity,
"shadow_spread": cv.int_, "shadow_spread": lvalid.lv_int,
"shadow_width": cv.positive_int, "shadow_width": lvalid.lv_positive_int,
"text_align": df.LvConstant( "text_align": df.LvConstant(
"LV_TEXT_ALIGN_", "LEFT", "CENTER", "RIGHT", "AUTO" "LV_TEXT_ALIGN_", "LEFT", "CENTER", "RIGHT", "AUTO"
).one_of, ).one_of,
@ -150,7 +150,7 @@ STYLE_PROPS = {
"text_letter_space": cv.positive_int, "text_letter_space": cv.positive_int,
"text_line_space": cv.positive_int, "text_line_space": cv.positive_int,
"text_opa": lvalid.opacity, "text_opa": lvalid.opacity,
"transform_angle": lvalid.angle, "transform_angle": lvalid.lv_angle,
"transform_height": lvalid.pixels_or_percent, "transform_height": lvalid.pixels_or_percent,
"transform_pivot_x": lvalid.pixels_or_percent, "transform_pivot_x": lvalid.pixels_or_percent,
"transform_pivot_y": lvalid.pixels_or_percent, "transform_pivot_y": lvalid.pixels_or_percent,

View file

@ -21,6 +21,7 @@ media_player_ns = cg.esphome_ns.namespace("media_player")
MediaPlayer = media_player_ns.class_("MediaPlayer") MediaPlayer = media_player_ns.class_("MediaPlayer")
PlayAction = media_player_ns.class_( PlayAction = media_player_ns.class_(
"PlayAction", automation.Action, cg.Parented.template(MediaPlayer) "PlayAction", automation.Action, cg.Parented.template(MediaPlayer)
) )
@ -60,7 +61,11 @@ AnnoucementTrigger = media_player_ns.class_(
"AnnouncementTrigger", automation.Trigger.template() "AnnouncementTrigger", automation.Trigger.template()
) )
IsIdleCondition = media_player_ns.class_("IsIdleCondition", automation.Condition) IsIdleCondition = media_player_ns.class_("IsIdleCondition", automation.Condition)
IsPausedCondition = media_player_ns.class_("IsPausedCondition", automation.Condition)
IsPlayingCondition = media_player_ns.class_("IsPlayingCondition", automation.Condition) IsPlayingCondition = media_player_ns.class_("IsPlayingCondition", automation.Condition)
IsAnnouncingCondition = media_player_ns.class_(
"IsAnnouncingCondition", automation.Condition
)
async def setup_media_player_core_(var, config): async def setup_media_player_core_(var, config):
@ -159,9 +164,15 @@ async def media_player_play_media_action(config, action_id, template_arg, args):
@automation.register_condition( @automation.register_condition(
"media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_ACTION_SCHEMA "media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_ACTION_SCHEMA
) )
@automation.register_condition(
"media_player.is_paused", IsPausedCondition, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_condition( @automation.register_condition(
"media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_ACTION_SCHEMA "media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_ACTION_SCHEMA
) )
@automation.register_condition(
"media_player.is_announcing", IsAnnouncingCondition, MEDIA_PLAYER_ACTION_SCHEMA
)
async def media_player_action(config, action_id, template_arg, args): async def media_player_action(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID]) await cg.register_parented(var, config[CONF_ID])

View file

@ -68,5 +68,15 @@ template<typename... Ts> class IsPlayingCondition : public Condition<Ts...>, pub
bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; } bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; }
}; };
template<typename... Ts> class IsPausedCondition : public Condition<Ts...>, public Parented<MediaPlayer> {
public:
bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PAUSED; }
};
template<typename... Ts> class IsAnnouncingCondition : public Condition<Ts...>, public Parented<MediaPlayer> {
public:
bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; }
};
} // namespace media_player } // namespace media_player
} // namespace esphome } // namespace esphome

View file

@ -37,6 +37,10 @@ const char *media_player_command_to_string(MediaPlayerCommand command) {
return "UNMUTE"; return "UNMUTE";
case MEDIA_PLAYER_COMMAND_TOGGLE: case MEDIA_PLAYER_COMMAND_TOGGLE:
return "TOGGLE"; return "TOGGLE";
case MEDIA_PLAYER_COMMAND_VOLUME_UP:
return "VOLUME_UP";
case MEDIA_PLAYER_COMMAND_VOLUME_DOWN:
return "VOLUME_DOWN";
default: default:
return "UNKNOWN"; return "UNKNOWN";
} }

View file

@ -41,6 +41,7 @@ from esphome.const import (
CONF_SHUTDOWN_MESSAGE, CONF_SHUTDOWN_MESSAGE,
CONF_SSL_FINGERPRINTS, CONF_SSL_FINGERPRINTS,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
CONF_SUBSCRIBE_QOS,
CONF_TOPIC, CONF_TOPIC,
CONF_TOPIC_PREFIX, CONF_TOPIC_PREFIX,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
@ -518,6 +519,8 @@ async def register_mqtt_component(var, config):
cg.add(var.set_qos(config[CONF_QOS])) cg.add(var.set_qos(config[CONF_QOS]))
if CONF_RETAIN in config: if CONF_RETAIN in config:
cg.add(var.set_retain(config[CONF_RETAIN])) cg.add(var.set_retain(config[CONF_RETAIN]))
if CONF_SUBSCRIBE_QOS in config:
cg.add(var.set_subscribe_qos(config[CONF_SUBSCRIBE_QOS]))
if not config.get(CONF_DISCOVERY, True): if not config.get(CONF_DISCOVERY, True):
cg.add(var.disable_discovery()) cg.add(var.disable_discovery())
if CONF_STATE_TOPIC in config: if CONF_STATE_TOPIC in config:

View file

@ -16,6 +16,8 @@ static const char *const TAG = "mqtt.component";
void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; } void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; }
void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; }
void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; } void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; }
std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const { std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const {
@ -76,6 +78,10 @@ bool MQTTComponent::send_discovery_() {
config.command_topic = true; config.command_topic = true;
this->send_discovery(root, config); this->send_discovery(root, config);
// Set subscription QoS (default is 0)
if (this->subscribe_qos_ != 0) {
root[MQTT_QOS] = this->subscribe_qos_;
}
// Fields from EntityBase // Fields from EntityBase
if (this->get_entity()->has_own_name()) { if (this->get_entity()->has_own_name()) {

View file

@ -89,6 +89,9 @@ class MQTTComponent : public Component {
void disable_discovery(); void disable_discovery();
bool is_discovery_enabled() const; bool is_discovery_enabled() const;
/// Set the QOS for subscribe messages (used in discovery).
void set_subscribe_qos(uint8_t qos);
/// Override this method to return the component type (e.g. "light", "sensor", ...) /// Override this method to return the component type (e.g. "light", "sensor", ...)
virtual std::string component_type() const = 0; virtual std::string component_type() const = 0;
@ -204,6 +207,7 @@ class MQTTComponent : public Component {
bool command_retain_{false}; bool command_retain_{false};
bool retain_{true}; bool retain_{true};
uint8_t qos_{0}; uint8_t qos_{0};
uint8_t subscribe_qos_{0};
bool discovery_enabled_{true}; bool discovery_enabled_{true};
bool resend_state_{false}; bool resend_state_{false};
}; };

View file

@ -180,6 +180,7 @@ constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC = "pr_mode_cmd_t";
constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC = "pr_mode_stat_t"; constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC = "pr_mode_stat_t";
constexpr const char *const MQTT_PRESET_MODE_VALUE_TEMPLATE = "pr_mode_val_tpl"; constexpr const char *const MQTT_PRESET_MODE_VALUE_TEMPLATE = "pr_mode_val_tpl";
constexpr const char *const MQTT_PRESET_MODES = "pr_modes"; constexpr const char *const MQTT_PRESET_MODES = "pr_modes";
constexpr const char *const MQTT_QOS = "qos";
constexpr const char *const MQTT_RED_TEMPLATE = "r_tpl"; constexpr const char *const MQTT_RED_TEMPLATE = "r_tpl";
constexpr const char *const MQTT_RETAIN = "ret"; constexpr const char *const MQTT_RETAIN = "ret";
constexpr const char *const MQTT_RGB_COMMAND_TEMPLATE = "rgb_cmd_tpl"; constexpr const char *const MQTT_RGB_COMMAND_TEMPLATE = "rgb_cmd_tpl";
@ -441,6 +442,7 @@ constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC = "preset_mode_comman
constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic"; constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic";
constexpr const char *const MQTT_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template"; constexpr const char *const MQTT_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template";
constexpr const char *const MQTT_PRESET_MODES = "preset_modes"; constexpr const char *const MQTT_PRESET_MODES = "preset_modes";
constexpr const char *const MQTT_QOS = "qos";
constexpr const char *const MQTT_RED_TEMPLATE = "red_template"; constexpr const char *const MQTT_RED_TEMPLATE = "red_template";
constexpr const char *const MQTT_RETAIN = "retain"; constexpr const char *const MQTT_RETAIN = "retain";
constexpr const char *const MQTT_RGB_COMMAND_TEMPLATE = "rgb_command_template"; constexpr const char *const MQTT_RGB_COMMAND_TEMPLATE = "rgb_command_template";

View file

@ -215,16 +215,10 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
} }
case ImageType::IMAGE_TYPE_RGB565: { case ImageType::IMAGE_TYPE_RGB565: {
uint16_t col565 = display::ColorUtil::color_to_565(color); uint16_t col565 = display::ColorUtil::color_to_565(color);
if (this->has_transparency()) {
if (col565 == 0x0020) {
col565 = 0;
}
if (color.w < 0x80) {
col565 = 0x0020;
}
}
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF); this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF); this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
if (this->has_transparency())
this->buffer_[pos + 2] = color.w;
break; break;
} }
case ImageType::IMAGE_TYPE_RGBA: { case ImageType::IMAGE_TYPE_RGBA: {

View file

@ -86,13 +86,9 @@ class OnlineImage : public PollingComponent,
Allocator allocator_{Allocator::Flags::ALLOW_FAILURE}; Allocator allocator_{Allocator::Flags::ALLOW_FAILURE};
uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); } uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); }
int get_buffer_size_(int width, int height) const { int get_buffer_size_(int width, int height) const { return (this->get_bpp() * width + 7u) / 8u * height; }
return std::ceil(image::image_type_to_bpp(this->type_) * width * height / 8.0);
}
int get_position_(int x, int y) const { int get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; }
return ((x + y * this->buffer_width_) * image::image_type_to_bpp(this->type_)) / 8;
}
ESPHOME_ALWAYS_INLINE bool auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; } ESPHOME_ALWAYS_INLINE bool auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; }

View file

@ -1,9 +1,10 @@
from typing import Any from typing import Any
from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import pins
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266 from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
from . import generate
CODEOWNERS = ["@olegtarasov"] CODEOWNERS = ["@olegtarasov"]
MULTI_CONF = True MULTI_CONF = True
@ -15,15 +16,14 @@ CONF_DHW_ENABLE = "dhw_enable"
CONF_COOLING_ENABLE = "cooling_enable" CONF_COOLING_ENABLE = "cooling_enable"
CONF_OTC_ACTIVE = "otc_active" CONF_OTC_ACTIVE = "otc_active"
CONF_CH2_ACTIVE = "ch2_active" CONF_CH2_ACTIVE = "ch2_active"
CONF_SUMMER_MODE_ACTIVE = "summer_mode_active"
CONF_DHW_BLOCK = "dhw_block"
CONF_SYNC_MODE = "sync_mode" CONF_SYNC_MODE = "sync_mode"
opentherm_ns = cg.esphome_ns.namespace("opentherm")
OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(OpenthermHub), cv.GenerateID(): cv.declare_id(generate.OpenthermHub),
cv.Required(CONF_IN_PIN): pins.internal_gpio_input_pin_schema, cv.Required(CONF_IN_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_OUT_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_OUT_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_CH_ENABLE, True): cv.boolean, cv.Optional(CONF_CH_ENABLE, True): cv.boolean,
@ -31,6 +31,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_COOLING_ENABLE, False): cv.boolean, cv.Optional(CONF_COOLING_ENABLE, False): cv.boolean,
cv.Optional(CONF_OTC_ACTIVE, False): cv.boolean, cv.Optional(CONF_OTC_ACTIVE, False): cv.boolean,
cv.Optional(CONF_CH2_ACTIVE, False): cv.boolean, cv.Optional(CONF_CH2_ACTIVE, False): cv.boolean,
cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean,
cv.Optional(CONF_DHW_BLOCK, False): cv.boolean,
cv.Optional(CONF_SYNC_MODE, False): cv.boolean, cv.Optional(CONF_SYNC_MODE, False): cv.boolean,
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
@ -39,8 +41,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: dict[str, Any]) -> None: async def to_code(config: dict[str, Any]) -> None:
# Create the hub, passing the two callbacks defined below
# Since the hub is used in the callbacks, we need to define it first
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
@ -53,5 +53,7 @@ async def to_code(config: dict[str, Any]) -> None:
non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN} non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN}
for key, value in config.items(): for key, value in config.items():
if key not in non_sensors: if key in non_sensors:
cg.add(getattr(var, f"set_{key}")(value)) continue
cg.add(getattr(var, f"set_{key}")(value))

View file

@ -0,0 +1,5 @@
OPENTHERM = "opentherm"
CONF_OPENTHERM_ID = "opentherm_id"
SENSOR = "sensor"

View file

@ -0,0 +1,140 @@
from collections.abc import Awaitable
from typing import Any, Callable
import esphome.codegen as cg
from esphome.const import CONF_ID
from . import const
from .schema import TSchema
opentherm_ns = cg.esphome_ns.namespace("opentherm")
OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component)
def define_has_component(component_type: str, keys: list[str]) -> None:
cg.add_define(
f"OPENTHERM_{component_type.upper()}_LIST(F, sep)",
cg.RawExpression(
" sep ".join(map(lambda key: f"F({key}_{component_type.lower()})", keys))
),
)
for key in keys:
cg.add_define(f"OPENTHERM_HAS_{component_type.upper()}_{key}")
def define_message_handler(
component_type: str, keys: list[str], schemas: dict[str, TSchema]
) -> None:
# The macros defined here should be able to generate things like this:
# // Parsing a message and publishing to sensors
# case MessageId::Message:
# // Can have multiple sensors here, for example for a Status message with multiple flags
# this->thing_binary_sensor->publish_state(parse_flag8_lb_0(response));
# this->other_binary_sensor->publish_state(parse_flag8_lb_1(response));
# break;
# // Building a message for a write request
# case MessageId::Message: {
# unsigned int data = 0;
# data = write_flag8_lb_0(some_input_switch->state, data); // Where input_sensor can also be a number/output/switch
# data = write_u8_hb(some_number->state, data);
# return opentherm_->build_request_(MessageType::WriteData, MessageId::Message, data);
# }
messages: dict[str, list[tuple[str, str]]] = {}
for key in keys:
msg = schemas[key].message
if msg not in messages:
messages[msg] = []
messages[msg].append((key, schemas[key].message_data))
cg.add_define(
f"OPENTHERM_{component_type.upper()}_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)",
cg.RawExpression(
" msg_sep ".join(
[
f"MESSAGE({msg}) "
+ " entity_sep ".join(
[
f"ENTITY({key}_{component_type.lower()}, {msg_data})"
for key, msg_data in keys
]
)
+ " postscript"
for msg, keys in messages.items()
]
)
),
)
def define_readers(component_type: str, keys: list[str]) -> None:
for key in keys:
cg.add_define(
f"OPENTHERM_READ_{key}",
cg.RawExpression(f"this->{key}_{component_type.lower()}->state"),
)
def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]):
messages: set[tuple[str, bool]] = set()
for key in keys:
messages.add((schemas[key].message, schemas[key].keep_updated))
for msg, keep_updated in messages:
msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}")
if keep_updated:
cg.add(hub.add_repeating_message(msg_expr))
else:
cg.add(hub.add_initial_message(msg_expr))
def add_property_set(var: cg.MockObj, config_key: str, config: dict[str, Any]) -> None:
if config_key in config:
cg.add(getattr(var, f"set_{config_key}")(config[config_key]))
Create = Callable[[dict[str, Any], str, cg.MockObj], Awaitable[cg.Pvariable]]
def create_only_conf(
create: Callable[[dict[str, Any]], Awaitable[cg.Pvariable]]
) -> Create:
return lambda conf, _key, _hub: create(conf)
async def component_to_code(
component_type: str,
schemas: dict[str, TSchema],
type: cg.MockObjClass,
create: Create,
config: dict[str, Any],
) -> list[str]:
"""Generate the code for each configured component in the schema of a component type.
Parameters:
- component_type: The type of component, e.g. "sensor" or "binary_sensor"
- schema_: The schema for that component type, a list of available components
- type: The type of the component, e.g. sensor.Sensor or OpenthermOutput
- create: A constructor function for the component, which receives the config,
the key and the hub and should asynchronously return the new component
- config: The configuration for this component type
Returns: The list of keys for the created components
"""
cg.add_define(f"OPENTHERM_USE_{component_type.upper()}")
hub = await cg.get_variable(config[const.CONF_OPENTHERM_ID])
keys: list[str] = []
for key, conf in config.items():
if not isinstance(conf, dict):
continue
id = conf[CONF_ID]
if id and id.type == type:
entity = await create(conf, key, hub)
cg.add(getattr(hub, f"set_{key}_{component_type.lower()}")(entity))
keys.append(key)
define_has_component(component_type, keys)
define_message_handler(component_type, keys, schemas)
add_messages(hub, keys, schemas)
return keys

View file

@ -7,50 +7,114 @@ namespace esphome {
namespace opentherm { namespace opentherm {
static const char *const TAG = "opentherm"; static const char *const TAG = "opentherm";
namespace message_data {
bool parse_flag8_lb_0(OpenthermData &data) { return read_bit(data.valueLB, 0); }
bool parse_flag8_lb_1(OpenthermData &data) { return read_bit(data.valueLB, 1); }
bool parse_flag8_lb_2(OpenthermData &data) { return read_bit(data.valueLB, 2); }
bool parse_flag8_lb_3(OpenthermData &data) { return read_bit(data.valueLB, 3); }
bool parse_flag8_lb_4(OpenthermData &data) { return read_bit(data.valueLB, 4); }
bool parse_flag8_lb_5(OpenthermData &data) { return read_bit(data.valueLB, 5); }
bool parse_flag8_lb_6(OpenthermData &data) { return read_bit(data.valueLB, 6); }
bool parse_flag8_lb_7(OpenthermData &data) { return read_bit(data.valueLB, 7); }
bool parse_flag8_hb_0(OpenthermData &data) { return read_bit(data.valueHB, 0); }
bool parse_flag8_hb_1(OpenthermData &data) { return read_bit(data.valueHB, 1); }
bool parse_flag8_hb_2(OpenthermData &data) { return read_bit(data.valueHB, 2); }
bool parse_flag8_hb_3(OpenthermData &data) { return read_bit(data.valueHB, 3); }
bool parse_flag8_hb_4(OpenthermData &data) { return read_bit(data.valueHB, 4); }
bool parse_flag8_hb_5(OpenthermData &data) { return read_bit(data.valueHB, 5); }
bool parse_flag8_hb_6(OpenthermData &data) { return read_bit(data.valueHB, 6); }
bool parse_flag8_hb_7(OpenthermData &data) { return read_bit(data.valueHB, 7); }
uint8_t parse_u8_lb(OpenthermData &data) { return data.valueLB; }
uint8_t parse_u8_hb(OpenthermData &data) { return data.valueHB; }
int8_t parse_s8_lb(OpenthermData &data) { return (int8_t) data.valueLB; }
int8_t parse_s8_hb(OpenthermData &data) { return (int8_t) data.valueHB; }
uint16_t parse_u16(OpenthermData &data) { return data.u16(); }
int16_t parse_s16(OpenthermData &data) { return data.s16(); }
float parse_f88(OpenthermData &data) { return data.f88(); }
OpenthermData OpenthermHub::build_request_(MessageId request_id) { void write_flag8_lb_0(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 0, value); }
void write_flag8_lb_1(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 1, value); }
void write_flag8_lb_2(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 2, value); }
void write_flag8_lb_3(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 3, value); }
void write_flag8_lb_4(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 4, value); }
void write_flag8_lb_5(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 5, value); }
void write_flag8_lb_6(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 6, value); }
void write_flag8_lb_7(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 7, value); }
void write_flag8_hb_0(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 0, value); }
void write_flag8_hb_1(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 1, value); }
void write_flag8_hb_2(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 2, value); }
void write_flag8_hb_3(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 3, value); }
void write_flag8_hb_4(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 4, value); }
void write_flag8_hb_5(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 5, value); }
void write_flag8_hb_6(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 6, value); }
void write_flag8_hb_7(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 7, value); }
void write_u8_lb(const uint8_t value, OpenthermData &data) { data.valueLB = value; }
void write_u8_hb(const uint8_t value, OpenthermData &data) { data.valueHB = value; }
void write_s8_lb(const int8_t value, OpenthermData &data) { data.valueLB = (uint8_t) value; }
void write_s8_hb(const int8_t value, OpenthermData &data) { data.valueHB = (uint8_t) value; }
void write_u16(const uint16_t value, OpenthermData &data) { data.u16(value); }
void write_s16(const int16_t value, OpenthermData &data) { data.s16(value); }
void write_f88(const float value, OpenthermData &data) { data.f88(value); }
} // namespace message_data
OpenthermData OpenthermHub::build_request_(MessageId request_id) const {
OpenthermData data; OpenthermData data;
data.type = 0; data.type = 0;
data.id = 0; data.id = 0;
data.valueHB = 0; data.valueHB = 0;
data.valueLB = 0; data.valueLB = 0;
// First, handle the status request. This requires special logic, because we // We need this special logic for STATUS message because we have two options for specifying boiler modes:
// wouldn't want to inadvertently disable domestic hot water, for example. // with static config values in the hub, or with separate switches.
// It is also included in the macro-generated code below, but that will
// never be executed, because we short-circuit it here.
if (request_id == MessageId::STATUS) { if (request_id == MessageId::STATUS) {
bool const ch_enabled = this->ch_enable; // NOLINTBEGIN
bool dhw_enabled = this->dhw_enable; bool const ch_enabled = this->ch_enable && OPENTHERM_READ_ch_enable && OPENTHERM_READ_t_set > 0.0;
bool cooling_enabled = this->cooling_enable; bool const dhw_enabled = this->dhw_enable && OPENTHERM_READ_dhw_enable;
bool otc_enabled = this->otc_active; bool const cooling_enabled =
bool ch2_enabled = this->ch2_active; this->cooling_enable && OPENTHERM_READ_cooling_enable && OPENTHERM_READ_cooling_control > 0.0;
bool const otc_enabled = this->otc_active && OPENTHERM_READ_otc_active;
bool const ch2_enabled = this->ch2_active && OPENTHERM_READ_ch2_active && OPENTHERM_READ_t_set_ch2 > 0.0;
bool const summer_mode_is_active = this->summer_mode_active && OPENTHERM_READ_summer_mode_active;
bool const dhw_blocked = this->dhw_block && OPENTHERM_READ_dhw_block;
// NOLINTEND
data.type = MessageType::READ_DATA; data.type = MessageType::READ_DATA;
data.id = MessageId::STATUS; data.id = MessageId::STATUS;
data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4); data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4) |
(summer_mode_is_active << 5) | (dhw_blocked << 6);
return data;
}
// Disable incomplete switch statement warnings, because the cases in each // Disable incomplete switch statement warnings, because the cases in each
// switch are generated based on the configured sensors and inputs. // switch are generated based on the configured sensors and inputs.
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch" #pragma GCC diagnostic ignored "-Wswitch"
// TODO: This is a placeholder for an auto-generated switch statement which builds request structure based on switch (request_id) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) }
// which sensors are enabled in config.
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
return data; // And if we get here, a message was requested which somehow wasn't handled.
} // This shouldn't happen due to the way the defines are configured, so we
return OpenthermData(); // log an error and just return a 0 message.
ESP_LOGE(TAG, "Tried to create a request with unknown id %d. This should never happen, so please open an issue.",
request_id);
return {};
} }
OpenthermHub::OpenthermHub() : Component() {} OpenthermHub::OpenthermHub() : Component(), in_pin_{}, out_pin_{} {}
void OpenthermHub::process_response(OpenthermData &data) { void OpenthermHub::process_response(OpenthermData &data) {
ESP_LOGD(TAG, "Received OpenTherm response with id %d (%s)", data.id, ESP_LOGD(TAG, "Received OpenTherm response with id %d (%s)", data.id,
this->opentherm_->message_id_to_str((MessageId) data.id)); this->opentherm_->message_id_to_str((MessageId) data.id));
ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(data).c_str()); ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(data).c_str());
switch (data.id) {
OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_RESPONSE_MESSAGE, OPENTHERM_MESSAGE_RESPONSE_ENTITY, ,
OPENTHERM_MESSAGE_RESPONSE_POSTSCRIPT, )
}
} }
void OpenthermHub::setup() { void OpenthermHub::setup() {
@ -254,15 +318,17 @@ void OpenthermHub::handle_timeout_error_() {
this->stop_opentherm_(); this->stop_opentherm_();
} }
#define ID(x) x
#define SHOW2(x) #x
#define SHOW(x) SHOW2(x)
void OpenthermHub::dump_config() { void OpenthermHub::dump_config() {
ESP_LOGCONFIG(TAG, "OpenTherm:"); ESP_LOGCONFIG(TAG, "OpenTherm:");
LOG_PIN(" In: ", this->in_pin_); LOG_PIN(" In: ", this->in_pin_);
LOG_PIN(" Out: ", this->out_pin_); LOG_PIN(" Out: ", this->out_pin_);
ESP_LOGCONFIG(TAG, " Sync mode: %d", this->sync_mode_); ESP_LOGCONFIG(TAG, " Sync mode: %d", this->sync_mode_);
ESP_LOGCONFIG(TAG, " Sensors: %s", SHOW(OPENTHERM_SENSOR_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Binary sensors: %s", SHOW(OPENTHERM_BINARY_SENSOR_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Switches: %s", SHOW(OPENTHERM_SWITCH_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Input sensors: %s", SHOW(OPENTHERM_INPUT_SENSOR_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Outputs: %s", SHOW(OPENTHERM_OUTPUT_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Initial requests:"); ESP_LOGCONFIG(TAG, " Initial requests:");
for (auto type : this->initial_messages_) { for (auto type : this->initial_messages_) {
ESP_LOGCONFIG(TAG, " - %d", type); ESP_LOGCONFIG(TAG, " - %d", type);

View file

@ -7,11 +7,17 @@
#include "opentherm.h" #include "opentherm.h"
#ifdef OPENTHERM_USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#include <memory> #include <memory>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <functional> #include <functional>
#include "opentherm_macros.h"
namespace esphome { namespace esphome {
namespace opentherm { namespace opentherm {
@ -23,6 +29,8 @@ class OpenthermHub : public Component {
// The OpenTherm interface // The OpenTherm interface
std::unique_ptr<OpenTherm> opentherm_; std::unique_ptr<OpenTherm> opentherm_;
OPENTHERM_SENSOR_LIST(OPENTHERM_DECLARE_SENSOR, )
// The set of initial messages to send on starting communication with the boiler // The set of initial messages to send on starting communication with the boiler
std::unordered_set<MessageId> initial_messages_; std::unordered_set<MessageId> initial_messages_;
// and the repeating messages which are sent repeatedly to update various sensors // and the repeating messages which are sent repeatedly to update various sensors
@ -44,7 +52,7 @@ class OpenthermHub : public Component {
bool sync_mode_ = false; bool sync_mode_ = false;
// Create OpenTherm messages based on the message id // Create OpenTherm messages based on the message id
OpenthermData build_request_(MessageId request_id); OpenthermData build_request_(MessageId request_id) const;
void handle_protocol_write_error_(); void handle_protocol_write_error_();
void handle_protocol_read_error_(); void handle_protocol_read_error_();
void handle_timeout_error_(); void handle_timeout_error_();
@ -78,6 +86,8 @@ class OpenthermHub : public Component {
void set_in_pin(InternalGPIOPin *in_pin) { this->in_pin_ = in_pin; } void set_in_pin(InternalGPIOPin *in_pin) { this->in_pin_ = in_pin; }
void set_out_pin(InternalGPIOPin *out_pin) { this->out_pin_ = out_pin; } void set_out_pin(InternalGPIOPin *out_pin) { this->out_pin_ = out_pin; }
OPENTHERM_SENSOR_LIST(OPENTHERM_SET_SENSOR, )
// Add a request to the set of initial requests // Add a request to the set of initial requests
void add_initial_message(MessageId message_id) { this->initial_messages_.insert(message_id); } void add_initial_message(MessageId message_id) { this->initial_messages_.insert(message_id); }
// Add a request to the set of repeating requests. Note that a large number of repeating // Add a request to the set of repeating requests. Note that a large number of repeating
@ -86,9 +96,10 @@ class OpenthermHub : public Component {
// will be processed. // will be processed.
void add_repeating_message(MessageId message_id) { this->repeating_messages_.insert(message_id); } void add_repeating_message(MessageId message_id) { this->repeating_messages_.insert(message_id); }
// There are five status variables, which can either be set as a simple variable, // There are seven status variables, which can either be set as a simple variable,
// or using a switch. ch_enable and dhw_enable default to true, the others to false. // or using a switch. ch_enable and dhw_enable default to true, the others to false.
bool ch_enable = true, dhw_enable = true, cooling_enable = false, otc_active = false, ch2_active = false; bool ch_enable = true, dhw_enable = true, cooling_enable = false, otc_active = false, ch2_active = false,
summer_mode_active = false, dhw_block = false;
// Setters for the status variables // Setters for the status variables
void set_ch_enable(bool value) { this->ch_enable = value; } void set_ch_enable(bool value) { this->ch_enable = value; }
@ -96,6 +107,8 @@ class OpenthermHub : public Component {
void set_cooling_enable(bool value) { this->cooling_enable = value; } void set_cooling_enable(bool value) { this->cooling_enable = value; }
void set_otc_active(bool value) { this->otc_active = value; } void set_otc_active(bool value) { this->otc_active = value; }
void set_ch2_active(bool value) { this->ch2_active = value; } void set_ch2_active(bool value) { this->ch2_active = value; }
void set_summer_mode_active(bool value) { this->summer_mode_active = value; }
void set_dhw_block(bool value) { this->dhw_block = value; }
void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; } void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; }
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }

View file

@ -283,6 +283,9 @@ bool OpenTherm::init_esp32_timer_() {
.clk_src = TIMER_SRC_CLK_DEFAULT, .clk_src = TIMER_SRC_CLK_DEFAULT,
#endif #endif
.divider = 80, .divider = 80,
#if defined(SOC_TIMER_GROUP_SUPPORT_XTAL) && ESP_IDF_VERSION_MAJOR < 5
.clk_src = TIMER_SRC_CLK_APB
#endif
}; };
esp_err_t result; esp_err_t result;

View file

@ -20,7 +20,6 @@
namespace esphome { namespace esphome {
namespace opentherm { namespace opentherm {
// TODO: Account for immutable semantics change in hub.cpp when doing later installments of OpenTherm PR
template<class T> constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; } template<class T> constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; }
template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); } template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); }
@ -28,7 +27,7 @@ template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1
template<class T> constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); } template<class T> constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); }
template<class T> constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) { template<class T> constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) {
return bit_value ? setBit(value, bit) : clearBit(value, bit); return bit_value ? set_bit(value, bit) : clear_bit(value, bit);
} }
enum OperationMode { enum OperationMode {

View file

@ -0,0 +1,91 @@
#pragma once
namespace esphome {
namespace opentherm {
// ===== hub.h macros =====
// *_LIST macros will be generated in defines.h if at least one sensor from each platform is used.
// These lists will look like this:
// #define OPENTHERM_BINARY_SENSOR_LIST(F, sep) F(sensor_1) sep F(sensor_2)
// These lists will be used in hub.h to define sensor fields (passing macros like OPENTHERM_DECLARE_SENSOR as F)
// and setters (passing macros like OPENTHERM_SET_SENSOR as F) (see below)
// In order for things not to break, we define empty lists here in case some platforms are not used in config.
#ifndef OPENTHERM_SENSOR_LIST
#define OPENTHERM_SENSOR_LIST(F, sep)
#endif
// Use macros to create fields for every entity specified in the ESPHome configuration
#define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity;
// Setter macros
#define OPENTHERM_SET_SENSOR(entity) \
void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; }
// ===== hub.cpp macros =====
// *_MESSAGE_HANDLERS are generated in defines.h and look like this:
// OPENTHERM_NUMBER_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) MESSAGE(COOLING_CONTROL)
// ENTITY(cooling_control_number, f88) postscript msg_sep They contain placeholders for message part and entities parts,
// since one message can contain multiple entities. MESSAGE part is substituted with OPENTHERM_MESSAGE_WRITE_MESSAGE,
// OPENTHERM_MESSAGE_READ_MESSAGE or OPENTHERM_MESSAGE_RESPONSE_MESSAGE. ENTITY part is substituted with
// OPENTHERM_MESSAGE_WRITE_ENTITY or OPENTHERM_MESSAGE_RESPONSE_ENTITY. OPENTHERM_IGNORE is used for sensor read
// requests since no data needs to be sent or processed, just the data id.
// In order for things not to break, we define empty lists here in case some platforms are not used in config.
#ifndef OPENTHERM_SENSOR_MESSAGE_HANDLERS
#define OPENTHERM_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)
#endif
// Read data request builder
#define OPENTHERM_MESSAGE_READ_MESSAGE(msg) \
case MessageId::msg: \
data.type = MessageType::READ_DATA; \
data.id = request_id; \
return data;
// Data processing builders
#define OPENTHERM_MESSAGE_RESPONSE_MESSAGE(msg) case MessageId::msg:
#define OPENTHERM_MESSAGE_RESPONSE_ENTITY(key, msg_data) this->key->publish_state(message_data::parse_##msg_data(data));
#define OPENTHERM_MESSAGE_RESPONSE_POSTSCRIPT break;
#define OPENTHERM_IGNORE(x, y)
// Default macros for STATUS entities
#ifndef OPENTHERM_READ_ch_enable
#define OPENTHERM_READ_ch_enable true
#endif
#ifndef OPENTHERM_READ_dhw_enable
#define OPENTHERM_READ_dhw_enable true
#endif
#ifndef OPENTHERM_READ_t_set
#define OPENTHERM_READ_t_set 0.0
#endif
#ifndef OPENTHERM_READ_cooling_enable
#define OPENTHERM_READ_cooling_enable false
#endif
#ifndef OPENTHERM_READ_cooling_control
#define OPENTHERM_READ_cooling_control 0.0
#endif
#ifndef OPENTHERM_READ_otc_active
#define OPENTHERM_READ_otc_active false
#endif
#ifndef OPENTHERM_READ_ch2_active
#define OPENTHERM_READ_ch2_active false
#endif
#ifndef OPENTHERM_READ_t_set_ch2
#define OPENTHERM_READ_t_set_ch2 0.0
#endif
#ifndef OPENTHERM_READ_summer_mode_active
#define OPENTHERM_READ_summer_mode_active false
#endif
#ifndef OPENTHERM_READ_dhw_block
#define OPENTHERM_READ_dhw_block false
#endif
// These macros utilize the structure of *_LIST macros in order
#define ID(x) x
#define SHOW_INNER(x) #x
#define SHOW(x) SHOW_INNER(x)
} // namespace opentherm
} // namespace esphome

View file

@ -0,0 +1,438 @@
# This file contains a schema for all supported sensors, binary sensors and
# inputs of the OpenTherm component.
from dataclasses import dataclass
from typing import Optional, TypeVar
from esphome.const import (
UNIT_CELSIUS,
UNIT_EMPTY,
UNIT_KILOWATT,
UNIT_MICROAMP,
UNIT_PERCENT,
UNIT_REVOLUTIONS_PER_MINUTE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_NONE,
STATE_CLASS_TOTAL_INCREASING,
)
@dataclass
class EntitySchema:
description: str
"""Description of the item, based on the OpenTherm spec"""
message: str
"""OpenTherm message id used to read or write the value"""
keep_updated: bool
"""Whether the value should be read or write repeatedly (True) or only during
the initialization phase (False)
"""
message_data: str
"""Instructions on how to interpret the data in the message
- flag8_[hb|lb]_[0-7]: data is a byte of single bit flags,
this flag is set in the high (hb) or low byte (lb),
at position 0 to 7
- u8_[hb|lb]: data is an unsigned 8-bit integer,
in the high (hb) or low byte (lb)
- s8_[hb|lb]: data is an signed 8-bit integer,
in the high (hb) or low byte (lb)
- f88: data is a signed fixed point value with
1 sign bit, 7 integer bits, 8 fractional bits
- u16: data is an unsigned 16-bit integer
- s16: data is a signed 16-bit integer
"""
TSchema = TypeVar("TSchema", bound=EntitySchema)
@dataclass
class SensorSchema(EntitySchema):
accuracy_decimals: int
state_class: str
unit_of_measurement: Optional[str] = None
icon: Optional[str] = None
device_class: Optional[str] = None
disabled_by_default: bool = False
SENSORS: dict[str, SensorSchema] = {
"rel_mod_level": SensorSchema(
description="Relative modulation level",
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=2,
icon="mdi:percent",
state_class=STATE_CLASS_MEASUREMENT,
message="MODULATION_LEVEL",
keep_updated=True,
message_data="f88",
),
"ch_pressure": SensorSchema(
description="Water pressure in CH circuit",
unit_of_measurement="bar",
accuracy_decimals=2,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
message="CH_WATER_PRESSURE",
keep_updated=True,
message_data="f88",
),
"dhw_flow_rate": SensorSchema(
description="Water flow rate in DHW circuit",
unit_of_measurement="l/min",
accuracy_decimals=2,
icon="mdi:waves-arrow-right",
state_class=STATE_CLASS_MEASUREMENT,
message="DHW_FLOW_RATE",
keep_updated=True,
message_data="f88",
),
"t_boiler": SensorSchema(
description="Boiler water temperature",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="FEED_TEMP",
keep_updated=True,
message_data="f88",
),
"t_dhw": SensorSchema(
description="DHW temperature",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="DHW_TEMP",
keep_updated=True,
message_data="f88",
),
"t_outside": SensorSchema(
description="Outside temperature",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="OUTSIDE_TEMP",
keep_updated=True,
message_data="f88",
),
"t_ret": SensorSchema(
description="Return water temperature",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="RETURN_WATER_TEMP",
keep_updated=True,
message_data="f88",
),
"t_storage": SensorSchema(
description="Solar storage temperature",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="SOLAR_STORE_TEMP",
keep_updated=True,
message_data="f88",
),
"t_collector": SensorSchema(
description="Solar collector temperature",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="SOLAR_COLLECT_TEMP",
keep_updated=True,
message_data="s16",
),
"t_flow_ch2": SensorSchema(
description="Flow water temperature CH2 circuit",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="FEED_TEMP_CH2",
keep_updated=True,
message_data="f88",
),
"t_dhw2": SensorSchema(
description="Domestic hot water temperature 2",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="DHW2_TEMP",
keep_updated=True,
message_data="f88",
),
"t_exhaust": SensorSchema(
description="Boiler exhaust temperature",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="EXHAUST_TEMP",
keep_updated=True,
message_data="s16",
),
"fan_speed": SensorSchema(
description="Boiler fan speed",
unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
message="FAN_SPEED",
keep_updated=True,
message_data="u16",
),
"flame_current": SensorSchema(
description="Boiler flame current",
unit_of_measurement=UNIT_MICROAMP,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
message="FLAME_CURRENT",
keep_updated=True,
message_data="f88",
),
"burner_starts": SensorSchema(
description="Number of starts burner",
accuracy_decimals=0,
icon="mdi:gas-burner",
state_class=STATE_CLASS_TOTAL_INCREASING,
message="BURNER_STARTS",
keep_updated=True,
message_data="u16",
),
"ch_pump_starts": SensorSchema(
description="Number of starts CH pump",
accuracy_decimals=0,
icon="mdi:pump",
state_class=STATE_CLASS_TOTAL_INCREASING,
message="CH_PUMP_STARTS",
keep_updated=True,
message_data="u16",
),
"dhw_pump_valve_starts": SensorSchema(
description="Number of starts DHW pump/valve",
accuracy_decimals=0,
icon="mdi:water-pump",
state_class=STATE_CLASS_TOTAL_INCREASING,
message="DHW_PUMP_STARTS",
keep_updated=True,
message_data="u16",
),
"dhw_burner_starts": SensorSchema(
description="Number of starts burner during DHW mode",
accuracy_decimals=0,
icon="mdi:gas-burner",
state_class=STATE_CLASS_TOTAL_INCREASING,
message="DHW_BURNER_STARTS",
keep_updated=True,
message_data="u16",
),
"burner_operation_hours": SensorSchema(
description="Number of hours that burner is in operation",
accuracy_decimals=0,
icon="mdi:clock-outline",
state_class=STATE_CLASS_TOTAL_INCREASING,
message="BURNER_HOURS",
keep_updated=True,
message_data="u16",
),
"ch_pump_operation_hours": SensorSchema(
description="Number of hours that CH pump has been running",
accuracy_decimals=0,
icon="mdi:clock-outline",
state_class=STATE_CLASS_TOTAL_INCREASING,
message="CH_PUMP_HOURS",
keep_updated=True,
message_data="u16",
),
"dhw_pump_valve_operation_hours": SensorSchema(
description="Number of hours that DHW pump has been running or DHW valve has been opened",
accuracy_decimals=0,
icon="mdi:clock-outline",
state_class=STATE_CLASS_TOTAL_INCREASING,
message="DHW_PUMP_HOURS",
keep_updated=True,
message_data="u16",
),
"dhw_burner_operation_hours": SensorSchema(
description="Number of hours that burner is in operation during DHW mode",
accuracy_decimals=0,
icon="mdi:clock-outline",
state_class=STATE_CLASS_TOTAL_INCREASING,
message="DHW_BURNER_HOURS",
keep_updated=True,
message_data="u16",
),
"t_dhw_set_ub": SensorSchema(
description="Upper bound for adjustment of DHW setpoint",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="DHW_BOUNDS",
keep_updated=False,
message_data="s8_hb",
),
"t_dhw_set_lb": SensorSchema(
description="Lower bound for adjustment of DHW setpoint",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="DHW_BOUNDS",
keep_updated=False,
message_data="s8_lb",
),
"max_t_set_ub": SensorSchema(
description="Upper bound for adjustment of max CH setpoint",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="CH_BOUNDS",
keep_updated=False,
message_data="s8_hb",
),
"max_t_set_lb": SensorSchema(
description="Lower bound for adjustment of max CH setpoint",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="CH_BOUNDS",
keep_updated=False,
message_data="s8_lb",
),
"t_dhw_set": SensorSchema(
description="Domestic hot water temperature setpoint",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="DHW_SETPOINT",
keep_updated=True,
message_data="f88",
),
"max_t_set": SensorSchema(
description="Maximum allowable CH water setpoint",
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
message="MAX_CH_SETPOINT",
keep_updated=True,
message_data="f88",
),
"oem_fault_code": SensorSchema(
description="OEM fault code",
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
message="FAULT_FLAGS",
keep_updated=True,
message_data="u8_lb",
),
"oem_diagnostic_code": SensorSchema(
description="OEM diagnostic code",
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
message="OEM_DIAGNOSTIC",
keep_updated=True,
message_data="u16",
),
"max_capacity": SensorSchema(
description="Maximum boiler capacity (KW)",
unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
disabled_by_default=True,
message="MAX_BOILER_CAPACITY",
keep_updated=False,
message_data="u8_hb",
),
"min_mod_level": SensorSchema(
description="Minimum modulation level",
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=0,
icon="mdi:percent",
disabled_by_default=True,
state_class=STATE_CLASS_MEASUREMENT,
message="MAX_BOILER_CAPACITY",
keep_updated=False,
message_data="u8_lb",
),
"opentherm_version_device": SensorSchema(
description="Version of OpenTherm implemented by device",
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
disabled_by_default=True,
message="OT_VERSION_DEVICE",
keep_updated=False,
message_data="f88",
),
"device_type": SensorSchema(
description="Device product type",
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
disabled_by_default=True,
message="VERSION_DEVICE",
keep_updated=False,
message_data="u8_hb",
),
"device_version": SensorSchema(
description="Device product version",
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
disabled_by_default=True,
message="VERSION_DEVICE",
keep_updated=False,
message_data="u8_lb",
),
"device_id": SensorSchema(
description="Device ID code",
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
disabled_by_default=True,
message="DEVICE_CONFIG",
keep_updated=False,
message_data="u8_lb",
),
"otc_hc_ratio_ub": SensorSchema(
description="OTC heat curve ratio upper bound",
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
disabled_by_default=True,
message="OTC_CURVE_BOUNDS",
keep_updated=False,
message_data="u8_hb",
),
"otc_hc_ratio_lb": SensorSchema(
description="OTC heat curve ratio lower bound",
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
disabled_by_default=True,
message="OTC_CURVE_BOUNDS",
keep_updated=False,
message_data="u8_lb",
),
}

View file

@ -0,0 +1,35 @@
from typing import Any
import esphome.config_validation as cv
from esphome.components import sensor
from .. import const, schema, validate, generate
DEPENDENCIES = [const.OPENTHERM]
COMPONENT_TYPE = const.SENSOR
def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema:
return sensor.sensor_schema(
unit_of_measurement=entity.unit_of_measurement
or sensor._UNDEF, # pylint: disable=protected-access
accuracy_decimals=entity.accuracy_decimals,
device_class=entity.device_class
or sensor._UNDEF, # pylint: disable=protected-access
icon=entity.icon or sensor._UNDEF, # pylint: disable=protected-access
state_class=entity.state_class,
)
CONFIG_SCHEMA = validate.create_component_schema(
schema.SENSORS, get_entity_validation_schema
)
async def to_code(config: dict[str, Any]) -> None:
await generate.component_to_code(
COMPONENT_TYPE,
schema.SENSORS,
sensor.Sensor,
generate.create_only_conf(sensor.new_sensor),
config,
)

View file

@ -0,0 +1,31 @@
from typing import Callable
from voluptuous import Schema
import esphome.config_validation as cv
from . import const, schema, generate
from .schema import TSchema
def create_entities_schema(
entities: dict[str, schema.EntitySchema],
get_entity_validation_schema: Callable[[TSchema], cv.Schema],
) -> Schema:
entity_schema = {}
for key, entity in entities.items():
entity_schema[cv.Optional(key)] = get_entity_validation_schema(entity)
return cv.Schema(entity_schema)
def create_component_schema(
entities: dict[str, schema.EntitySchema],
get_entity_validation_schema: Callable[[TSchema], cv.Schema],
) -> Schema:
return (
cv.Schema(
{cv.GenerateID(const.CONF_OPENTHERM_ID): cv.use_id(generate.OpenthermHub)}
)
.extend(create_entities_schema(entities, get_entity_validation_schema))
.extend(cv.COMPONENT_SCHEMA)
)

View file

@ -17,7 +17,7 @@ from esphome.const import (
PLATFORM_RP2040, PLATFORM_RP2040,
) )
from esphome.core import CORE, EsphomeError, coroutine_with_priority from esphome.core import CORE, EsphomeError, coroutine_with_priority
from esphome.helpers import copy_file_if_changed, mkdir_p, write_file from esphome.helpers import copy_file_if_changed, mkdir_p, write_file, read_file
from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns
@ -26,7 +26,7 @@ from .gpio import rp2040_pin_to_code # noqa
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
AUTO_LOAD = [] AUTO_LOAD = ["preferences"]
def set_core_data(config): def set_core_data(config):
@ -230,11 +230,14 @@ def generate_pio_files() -> bool:
# Called by writer.py # Called by writer.py
def copy_files() -> bool: def copy_files():
dir = os.path.dirname(__file__) dir = os.path.dirname(__file__)
post_build_file = os.path.join(dir, "post_build.py.script") post_build_file = os.path.join(dir, "post_build.py.script")
copy_file_if_changed( copy_file_if_changed(
post_build_file, post_build_file,
CORE.relative_build_path("post_build.py"), CORE.relative_build_path("post_build.py"),
) )
return generate_pio_files() if generate_pio_files():
path = CORE.relative_src_path("esphome.h")
content = read_file(path).rstrip("\n")
write_file(path, content + '\n#include "pio_includes.h"\n')

View file

@ -84,6 +84,26 @@ void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uin
ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err));
} }
int RpiDpiRgb::get_width() {
switch (this->rotation_) {
case display::DISPLAY_ROTATION_90_DEGREES:
case display::DISPLAY_ROTATION_270_DEGREES:
return this->get_height_internal();
default:
return this->get_width_internal();
}
}
int RpiDpiRgb::get_height() {
switch (this->rotation_) {
case display::DISPLAY_ROTATION_90_DEGREES:
case display::DISPLAY_ROTATION_270_DEGREES:
return this->get_width_internal();
default:
return this->get_height_internal();
}
}
void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) { void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) {
if (!this->get_clipping().inside(x, y)) if (!this->get_clipping().inside(x, y))
return; // NOLINT return; // NOLINT

View file

@ -24,6 +24,7 @@ class RpiDpiRgb : public display::Display {
void update() override { this->do_update_(); } void update() override { this->do_update_(); }
void setup() override; void setup() override;
void loop() override; void loop() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, 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; display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
void draw_pixel_at(int x, int y, Color color) override; void draw_pixel_at(int x, int y, Color color) override;
@ -44,8 +45,8 @@ class RpiDpiRgb : public display::Display {
this->width_ = width; this->width_ = width;
this->height_ = height; this->height_ = height;
} }
int get_width() override { return this->width_; } int get_width() override;
int get_height() override { return this->height_; } int get_height() override;
void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; } void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; }
void set_hsync_front_porch(uint16_t hsync_front_porch) { this->hsync_front_porch_ = hsync_front_porch; } void set_hsync_front_porch(uint16_t hsync_front_porch) { this->hsync_front_porch_ = hsync_front_porch; }
void set_hsync_pulse_width(uint16_t hsync_pulse_width) { this->hsync_pulse_width_ = hsync_pulse_width; } void set_hsync_pulse_width(uint16_t hsync_pulse_width) { this->hsync_pulse_width_ = hsync_pulse_width; }

View file

@ -26,7 +26,10 @@ inline double deg2rad(double degrees) {
return degrees * PI_ON_180; return degrees * PI_ON_180;
} }
void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); } void Rtttl::dump_config() {
ESP_LOGCONFIG(TAG, "Rtttl:");
ESP_LOGCONFIG(TAG, " Gain: %f", gain_);
}
void Rtttl::play(std::string rtttl) { void Rtttl::play(std::string rtttl) {
if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) {

View file

@ -39,6 +39,7 @@ class Rtttl : public Component {
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; } void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; }
#endif #endif
float get_gain() { return gain_; }
void set_gain(float gain) { void set_gain(float gain) {
if (gain < 0.1f) if (gain < 0.1f)
gain = 0.1f; gain = 0.1f;

View file

@ -1,15 +1,18 @@
from esphome import automation from esphome import automation
from esphome.automation import maybe_simple_id from esphome.automation import maybe_simple_id
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import audio_dac
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME
from esphome.core import CORE from esphome.core import CORE
from esphome.coroutine import coroutine_with_priority from esphome.coroutine import coroutine_with_priority
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz", "@kahrendt"]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
CONF_AUDIO_DAC = "audio_dac"
speaker_ns = cg.esphome_ns.namespace("speaker") speaker_ns = cg.esphome_ns.namespace("speaker")
Speaker = speaker_ns.class_("Speaker") Speaker = speaker_ns.class_("Speaker")
@ -26,6 +29,12 @@ FinishAction = speaker_ns.class_(
VolumeSetAction = speaker_ns.class_( VolumeSetAction = speaker_ns.class_(
"VolumeSetAction", automation.Action, cg.Parented.template(Speaker) "VolumeSetAction", automation.Action, cg.Parented.template(Speaker)
) )
MuteOnAction = speaker_ns.class_(
"MuteOnAction", automation.Action, cg.Parented.template(Speaker)
)
MuteOffAction = speaker_ns.class_(
"MuteOffAction", automation.Action, cg.Parented.template(Speaker)
)
IsPlayingCondition = speaker_ns.class_("IsPlayingCondition", automation.Condition) IsPlayingCondition = speaker_ns.class_("IsPlayingCondition", automation.Condition)
@ -33,7 +42,9 @@ IsStoppedCondition = speaker_ns.class_("IsStoppedCondition", automation.Conditio
async def setup_speaker_core_(var, config): async def setup_speaker_core_(var, config):
pass if audio_dac_config := config.get(CONF_AUDIO_DAC):
aud_dac = await cg.get_variable(audio_dac_config)
cg.add(var.set_audio_dac(aud_dac))
async def register_speaker(var, config): async def register_speaker(var, config):
@ -42,8 +53,11 @@ async def register_speaker(var, config):
await setup_speaker_core_(var, config) await setup_speaker_core_(var, config)
SPEAKER_SCHEMA = cv.Schema({}) SPEAKER_SCHEMA = cv.Schema(
{
cv.Optional(CONF_AUDIO_DAC): cv.use_id(audio_dac.AudioDac),
}
)
SPEAKER_AUTOMATION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(Speaker)}) SPEAKER_AUTOMATION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(Speaker)})
@ -113,6 +127,15 @@ async def speaker_volume_set_action(config, action_id, template_arg, args):
return var return var
@automation.register_action(
"speaker.mute_off", MuteOffAction, SPEAKER_AUTOMATION_SCHEMA
)
@automation.register_action("speaker.mute_on", MuteOnAction, SPEAKER_AUTOMATION_SCHEMA)
async def speaker_mute_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@coroutine_with_priority(100.0) @coroutine_with_priority(100.0)
async def to_code(config): async def to_code(config):
cg.add_global(speaker_ns.using) cg.add_global(speaker_ns.using)

View file

@ -39,6 +39,26 @@ template<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Pa
void play(Ts... x) override { this->parent_->set_volume(this->volume_.value(x...)); } void play(Ts... x) override { this->parent_->set_volume(this->volume_.value(x...)); }
}; };
template<typename... Ts> class MuteOnAction : public Action<Ts...> {
public:
explicit MuteOnAction(Speaker *speaker) : speaker_(speaker) {}
void play(Ts... x) override { this->speaker_->set_mute_state(true); }
protected:
Speaker *speaker_;
};
template<typename... Ts> class MuteOffAction : public Action<Ts...> {
public:
explicit MuteOffAction(Speaker *speaker) : speaker_(speaker) {}
void play(Ts... x) override { this->speaker_->set_mute_state(false); }
protected:
Speaker *speaker_;
};
template<typename... Ts> class StopAction : public Action<Ts...>, public Parented<Speaker> { template<typename... Ts> class StopAction : public Action<Ts...>, public Parented<Speaker> {
public: public:
void play(Ts... x) override { this->parent_->stop(); } void play(Ts... x) override { this->parent_->stop(); }

View file

@ -8,7 +8,12 @@
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#endif #endif
#include "esphome/core/defines.h"
#include "esphome/components/audio/audio.h" #include "esphome/components/audio/audio.h"
#ifdef USE_AUDIO_DAC
#include "esphome/components/audio_dac/audio_dac.h"
#endif
namespace esphome { namespace esphome {
namespace speaker { namespace speaker {
@ -56,9 +61,35 @@ class Speaker {
bool is_running() const { return this->state_ == STATE_RUNNING; } bool is_running() const { return this->state_ == STATE_RUNNING; }
bool is_stopped() const { return this->state_ == STATE_STOPPED; } bool is_stopped() const { return this->state_ == STATE_STOPPED; }
// Volume control must be implemented by each speaker component, otherwise it will have no effect. // Volume control is handled by a configured audio dac component. Individual speaker components can
virtual void set_volume(float volume) { this->volume_ = volume; }; // override and implement in software if an audio dac isn't available.
virtual float get_volume() { return this->volume_; } virtual void set_volume(float volume) {
this->volume_ = volume;
#ifdef USE_AUDIO_DAC
if (this->audio_dac_ != nullptr) {
this->audio_dac_->set_volume(volume);
}
#endif
};
float get_volume() { return this->volume_; }
virtual void set_mute_state(bool mute_state) {
this->mute_state_ = mute_state;
#ifdef USE_AUDIO_DAC
if (this->audio_dac_) {
if (mute_state) {
this->audio_dac_->set_mute_on();
} else {
this->audio_dac_->set_mute_off();
}
}
#endif
}
bool get_mute_state() { return this->mute_state_; }
#ifdef USE_AUDIO_DAC
void set_audio_dac(audio_dac::AudioDac *audio_dac) { this->audio_dac_ = audio_dac; }
#endif
void set_audio_stream_info(const audio::AudioStreamInfo &audio_stream_info) { void set_audio_stream_info(const audio::AudioStreamInfo &audio_stream_info) {
this->audio_stream_info_ = audio_stream_info; this->audio_stream_info_ = audio_stream_info;
@ -68,6 +99,11 @@ class Speaker {
State state_{STATE_STOPPED}; State state_{STATE_STOPPED};
audio::AudioStreamInfo audio_stream_info_; audio::AudioStreamInfo audio_stream_info_;
float volume_{1.0f}; float volume_{1.0f};
bool mute_state_{false};
#ifdef USE_AUDIO_DAC
audio_dac::AudioDac *audio_dac_{nullptr};
#endif
}; };
} // namespace speaker } // namespace speaker

View file

@ -23,6 +23,8 @@ static const size_t SEND_BUFFER_SIZE = INPUT_BUFFER_SIZE * sizeof(int16_t);
static const size_t RECEIVE_SIZE = 1024; static const size_t RECEIVE_SIZE = 1024;
static const size_t SPEAKER_BUFFER_SIZE = 16 * RECEIVE_SIZE; static const size_t SPEAKER_BUFFER_SIZE = 16 * RECEIVE_SIZE;
VoiceAssistant::VoiceAssistant() { global_voice_assistant = this; }
float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
bool VoiceAssistant::start_udp_socket_() { bool VoiceAssistant::start_udp_socket_() {
@ -68,12 +70,6 @@ bool VoiceAssistant::start_udp_socket_() {
return true; return true;
} }
void VoiceAssistant::setup() {
ESP_LOGCONFIG(TAG, "Setting up Voice Assistant...");
global_voice_assistant = this;
}
bool VoiceAssistant::allocate_buffers_() { bool VoiceAssistant::allocate_buffers_() {
if (this->send_buffer_ != nullptr) { if (this->send_buffer_ != nullptr) {
return true; // Already allocated return true; // Already allocated
@ -437,16 +433,18 @@ void VoiceAssistant::loop() {
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
void VoiceAssistant::write_speaker_() { void VoiceAssistant::write_speaker_() {
if (this->speaker_buffer_size_ > 0) { if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) {
size_t write_chunk = std::min<size_t>(this->speaker_buffer_size_, 4 * 1024); if (this->speaker_buffer_size_ > 0) {
size_t written = this->speaker_->play(this->speaker_buffer_, write_chunk); size_t write_chunk = std::min<size_t>(this->speaker_buffer_size_, 4 * 1024);
if (written > 0) { size_t written = this->speaker_->play(this->speaker_buffer_, write_chunk);
memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); if (written > 0) {
this->speaker_buffer_size_ -= written; memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written);
this->speaker_buffer_index_ -= written; this->speaker_buffer_size_ -= written;
this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); }); this->speaker_buffer_index_ -= written;
} else { this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); });
ESP_LOGV(TAG, "Speaker buffer full, trying again next loop"); } else {
ESP_LOGV(TAG, "Speaker buffer full, trying again next loop");
}
} }
} }
} }
@ -776,16 +774,20 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
} }
case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: {
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
this->wait_for_stream_end_ = true; if (this->speaker_ != nullptr) {
ESP_LOGD(TAG, "TTS stream start"); this->wait_for_stream_end_ = true;
this->defer([this] { this->tts_stream_start_trigger_->trigger(); }); ESP_LOGD(TAG, "TTS stream start");
this->defer([this] { this->tts_stream_start_trigger_->trigger(); });
}
#endif #endif
break; break;
} }
case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: {
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
this->stream_ended_ = true; if (this->speaker_ != nullptr) {
ESP_LOGD(TAG, "TTS stream end"); this->stream_ended_ = true;
ESP_LOGD(TAG, "TTS stream end");
}
#endif #endif
break; break;
} }
@ -806,14 +808,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) { void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) {
#ifdef USE_SPEAKER // We should never get to this function if there is no speaker anyway #ifdef USE_SPEAKER // We should never get to this function if there is no speaker anyway
if (this->speaker_buffer_index_ + msg.data.length() < SPEAKER_BUFFER_SIZE) { if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) {
memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data.data(), msg.data.length()); if (this->speaker_buffer_index_ + msg.data.length() < SPEAKER_BUFFER_SIZE) {
this->speaker_buffer_index_ += msg.data.length(); memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data.data(), msg.data.length());
this->speaker_buffer_size_ += msg.data.length(); this->speaker_buffer_index_ += msg.data.length();
this->speaker_bytes_received_ += msg.data.length(); this->speaker_buffer_size_ += msg.data.length();
ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data.length()); this->speaker_bytes_received_ += msg.data.length();
} else { ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data.length());
ESP_LOGE(TAG, "Cannot receive audio, buffer is full"); } else {
ESP_LOGE(TAG, "Cannot receive audio, buffer is full");
}
} }
#endif #endif
} }

View file

@ -91,7 +91,8 @@ struct Configuration {
class VoiceAssistant : public Component { class VoiceAssistant : public Component {
public: public:
void setup() override; VoiceAssistant();
void loop() override; void loop() override;
float get_setup_priority() const override; float get_setup_priority() const override;
void start_streaming(); void start_streaming();
@ -249,7 +250,7 @@ class VoiceAssistant : public Component {
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
void write_speaker_(); void write_speaker_();
speaker::Speaker *speaker_{nullptr}; speaker::Speaker *speaker_{nullptr};
uint8_t *speaker_buffer_; uint8_t *speaker_buffer_{nullptr};
size_t speaker_buffer_index_{0}; size_t speaker_buffer_index_{0};
size_t speaker_buffer_size_{0}; size_t speaker_buffer_size_{0};
size_t speaker_bytes_received_{0}; size_t speaker_bytes_received_{0};
@ -281,8 +282,8 @@ class VoiceAssistant : public Component {
float volume_multiplier_; float volume_multiplier_;
uint32_t conversation_timeout_; uint32_t conversation_timeout_;
uint8_t *send_buffer_; uint8_t *send_buffer_{nullptr};
int16_t *input_buffer_; int16_t *input_buffer_{nullptr};
bool continuous_{false}; bool continuous_{false};
bool silence_detection_; bool silence_detection_;

View file

@ -209,7 +209,7 @@ class WeikaiComponent : public Component {
/// @brief store the name for the component /// @brief store the name for the component
/// @param name the name as defined by the python code generator /// @param name the name as defined by the python code generator
void set_name(std::string name) { this->name_ = std::move(name); } void set_name(std::string &&name) { this->name_ = std::move(name); }
/// @brief Get the name of the component /// @brief Get the name of the component
/// @return the name /// @return the name
@ -308,7 +308,7 @@ class WeikaiChannel : public uart::UARTComponent {
/// @brief The name as generated by the Python code generator /// @brief The name as generated by the Python code generator
/// @param name of the channel /// @param name of the channel
void set_channel_name(std::string name) { this->name_ = std::move(name); } void set_channel_name(std::string &&name) { this->name_ = std::move(name); }
/// @brief Get the channel name /// @brief Get the channel name
/// @return the name /// @return the name

View file

@ -34,6 +34,11 @@ static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non
static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void WiFiComponent::wifi_pre_setup_() { void WiFiComponent::wifi_pre_setup_() {
uint8_t mac[6];
if (has_custom_mac_address()) {
get_mac_address_raw(mac);
set_mac_address(mac);
}
auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2); auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2);
WiFi.onEvent(f); WiFi.onEvent(f);
WiFi.persistent(false); WiFi.persistent(false);

View file

@ -40,6 +40,7 @@ from esphome.const import (
CONF_SECOND, CONF_SECOND,
CONF_SETUP_PRIORITY, CONF_SETUP_PRIORITY,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
CONF_SUBSCRIBE_QOS,
CONF_TOPIC, CONF_TOPIC,
CONF_TYPE, CONF_TYPE,
CONF_TYPE_ID, CONF_TYPE_ID,
@ -1893,9 +1894,10 @@ MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema(
MQTT_COMPONENT_SCHEMA = Schema( MQTT_COMPONENT_SCHEMA = Schema(
{ {
Optional(CONF_QOS): All(requires_component("mqtt"), int_range(min=0, max=2)), Optional(CONF_QOS): All(requires_component("mqtt"), mqtt_qos),
Optional(CONF_RETAIN): All(requires_component("mqtt"), boolean), Optional(CONF_RETAIN): All(requires_component("mqtt"), boolean),
Optional(CONF_DISCOVERY): All(requires_component("mqtt"), boolean), Optional(CONF_DISCOVERY): All(requires_component("mqtt"), boolean),
Optional(CONF_SUBSCRIBE_QOS): All(requires_component("mqtt"), mqtt_qos),
Optional(CONF_STATE_TOPIC): All(requires_component("mqtt"), publish_topic), Optional(CONF_STATE_TOPIC): All(requires_component("mqtt"), publish_topic),
Optional(CONF_AVAILABILITY): All( Optional(CONF_AVAILABILITY): All(
requires_component("mqtt"), Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA) requires_component("mqtt"), Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA)

View file

@ -819,6 +819,7 @@ CONF_STOP = "stop"
CONF_STOP_ACTION = "stop_action" CONF_STOP_ACTION = "stop_action"
CONF_STORE_BASELINE = "store_baseline" CONF_STORE_BASELINE = "store_baseline"
CONF_SUBNET = "subnet" CONF_SUBNET = "subnet"
CONF_SUBSCRIBE_QOS = "subscribe_qos"
CONF_SUBSTITUTIONS = "substitutions" CONF_SUBSTITUTIONS = "substitutions"
CONF_SUM = "sum" CONF_SUM = "sum"
CONF_SUPPLEMENTAL_COOLING_ACTION = "supplemental_cooling_action" CONF_SUPPLEMENTAL_COOLING_ACTION = "supplemental_cooling_action"

View file

@ -318,6 +318,8 @@ async def add_includes(includes):
async def _add_platformio_options(pio_options): async def _add_platformio_options(pio_options):
# Add includes at the very end, so that they override everything # Add includes at the very end, so that they override everything
for key, val in pio_options.items(): for key, val in pio_options.items():
if key == "build_flags" and not isinstance(val, list):
val = [val]
cg.add_platformio_option(key, val) cg.add_platformio_option(key, val)

View file

@ -5,10 +5,14 @@ from collections.abc import Coroutine
import contextlib import contextlib
from dataclasses import dataclass from dataclasses import dataclass
from functools import partial from functools import partial
import json
import logging import logging
from pathlib import Path
import threading import threading
from typing import TYPE_CHECKING, Any, Callable from typing import TYPE_CHECKING, Any, Callable
from esphome.storage_json import ignored_devices_storage_path
from ..zeroconf import DiscoveredImport from ..zeroconf import DiscoveredImport
from .dns import DNSCache from .dns import DNSCache
from .entries import DashboardEntries from .entries import DashboardEntries
@ -20,6 +24,8 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
IGNORED_DEVICES_STORAGE_PATH = "ignored-devices.json"
@dataclass @dataclass
class Event: class Event:
@ -74,6 +80,7 @@ class ESPHomeDashboard:
"settings", "settings",
"dns_cache", "dns_cache",
"_background_tasks", "_background_tasks",
"ignored_devices",
) )
def __init__(self) -> None: def __init__(self) -> None:
@ -89,12 +96,30 @@ class ESPHomeDashboard:
self.settings = DashboardSettings() self.settings = DashboardSettings()
self.dns_cache = DNSCache() self.dns_cache = DNSCache()
self._background_tasks: set[asyncio.Task] = set() self._background_tasks: set[asyncio.Task] = set()
self.ignored_devices: set[str] = set()
async def async_setup(self) -> None: async def async_setup(self) -> None:
"""Setup the dashboard.""" """Setup the dashboard."""
self.loop = asyncio.get_running_loop() self.loop = asyncio.get_running_loop()
self.ping_request = asyncio.Event() self.ping_request = asyncio.Event()
self.entries = DashboardEntries(self) self.entries = DashboardEntries(self)
self.load_ignored_devices()
def load_ignored_devices(self) -> None:
storage_path = Path(ignored_devices_storage_path())
try:
with storage_path.open("r", encoding="utf-8") as f_handle:
data = json.load(f_handle)
self.ignored_devices = set(data.get("ignored_devices", set()))
except FileNotFoundError:
pass
def save_ignored_devices(self) -> None:
storage_path = Path(ignored_devices_storage_path())
with storage_path.open("w", encoding="utf-8") as f_handle:
json.dump(
{"ignored_devices": sorted(self.ignored_devices)}, indent=2, fp=f_handle
)
async def async_run(self) -> None: async def async_run(self) -> None:
"""Run the dashboard.""" """Run the dashboard."""

View file

@ -7,6 +7,7 @@ import datetime
import functools import functools
import gzip import gzip
import hashlib import hashlib
import importlib
import json import json
import logging import logging
import os import os
@ -541,6 +542,46 @@ class ImportRequestHandler(BaseHandler):
self.finish() self.finish()
class IgnoreDeviceRequestHandler(BaseHandler):
@authenticated
def post(self) -> None:
dashboard = DASHBOARD
try:
args = json.loads(self.request.body.decode())
device_name = args["name"]
ignore = args["ignore"]
except (json.JSONDecodeError, KeyError):
self.set_status(400)
self.set_header("content-type", "application/json")
self.write(json.dumps({"error": "Invalid payload"}))
return
ignored_device = next(
(
res
for res in dashboard.import_result.values()
if res.device_name == device_name
),
None,
)
if ignored_device is None:
self.set_status(404)
self.set_header("content-type", "application/json")
self.write(json.dumps({"error": "Device not found"}))
return
if ignore:
dashboard.ignored_devices.add(ignored_device.device_name)
else:
dashboard.ignored_devices.discard(ignored_device.device_name)
dashboard.save_ignored_devices()
self.set_status(204)
self.finish()
class DownloadListRequestHandler(BaseHandler): class DownloadListRequestHandler(BaseHandler):
@authenticated @authenticated
@bind_config @bind_config
@ -555,26 +596,18 @@ class DownloadListRequestHandler(BaseHandler):
downloads = [] downloads = []
platform: str = storage_json.target_platform.lower() platform: str = storage_json.target_platform.lower()
if platform == const.PLATFORM_RP2040:
from esphome.components.rp2040 import get_download_types as rp2040_types
downloads = rp2040_types(storage_json) if platform.upper() in ESP32_VARIANTS:
elif platform == const.PLATFORM_ESP8266: platform = "esp32"
from esphome.components.esp8266 import get_download_types as esp8266_types
downloads = esp8266_types(storage_json)
elif platform.upper() in ESP32_VARIANTS:
from esphome.components.esp32 import get_download_types as esp32_types
downloads = esp32_types(storage_json)
elif platform in (const.PLATFORM_RTL87XX, const.PLATFORM_BK72XX): elif platform in (const.PLATFORM_RTL87XX, const.PLATFORM_BK72XX):
from esphome.components.libretiny import ( platform = "libretiny"
get_download_types as libretiny_types,
)
downloads = libretiny_types(storage_json) try:
else: module = importlib.import_module(f"esphome.components.{platform}")
raise ValueError(f"Unknown platform {platform}") get_download_types = getattr(module, "get_download_types")
except AttributeError as exc:
raise ValueError(f"Unknown platform {platform}") from exc
downloads = get_download_types(storage_json)
self.set_status(200) self.set_status(200)
self.set_header("content-type", "application/json") self.set_header("content-type", "application/json")
@ -688,6 +721,7 @@ class ListDevicesHandler(BaseHandler):
"project_name": res.project_name, "project_name": res.project_name,
"project_version": res.project_version, "project_version": res.project_version,
"network": res.network, "network": res.network,
"ignored": res.device_name in dashboard.ignored_devices,
} }
for res in dashboard.import_result.values() for res in dashboard.import_result.values()
if res.device_name not in configured if res.device_name not in configured
@ -1156,6 +1190,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application:
(f"{rel}prometheus-sd", PrometheusServiceDiscoveryHandler), (f"{rel}prometheus-sd", PrometheusServiceDiscoveryHandler),
(f"{rel}boards/([a-z0-9]+)", BoardsRequestHandler), (f"{rel}boards/([a-z0-9]+)", BoardsRequestHandler),
(f"{rel}version", EsphomeVersionHandler), (f"{rel}version", EsphomeVersionHandler),
(f"{rel}ignore-device", IgnoreDeviceRequestHandler),
], ],
**app_settings, **app_settings,
) )

View file

@ -209,6 +209,12 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None):
elif CONF_MQTT in config: elif CONF_MQTT in config:
conf = config[CONF_MQTT] conf = config[CONF_MQTT]
if CONF_LOG_TOPIC in conf: if CONF_LOG_TOPIC in conf:
if config[CONF_MQTT][CONF_LOG_TOPIC] is None:
_LOGGER.error("MQTT log topic set to null, can't start MQTT logs")
return 1
if CONF_TOPIC not in config[CONF_MQTT][CONF_LOG_TOPIC]:
_LOGGER.error("MQTT log topic not available, can't start MQTT logs")
return 1
topic = config[CONF_MQTT][CONF_LOG_TOPIC][CONF_TOPIC] topic = config[CONF_MQTT][CONF_LOG_TOPIC][CONF_TOPIC]
elif CONF_TOPIC_PREFIX in config[CONF_MQTT]: elif CONF_TOPIC_PREFIX in config[CONF_MQTT]:
topic = f"{config[CONF_MQTT][CONF_TOPIC_PREFIX]}/debug" topic = f"{config[CONF_MQTT][CONF_TOPIC_PREFIX]}/debug"

View file

@ -28,6 +28,10 @@ def esphome_storage_path() -> str:
return os.path.join(CORE.data_dir, "esphome.json") return os.path.join(CORE.data_dir, "esphome.json")
def ignored_devices_storage_path() -> str:
return os.path.join(CORE.data_dir, "ignored-devices.json")
def trash_storage_path() -> str: def trash_storage_path() -> str:
return CORE.relative_config_path("trash") return CORE.relative_config_path("trash")

View file

@ -1,3 +1,4 @@
import importlib
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
@ -299,25 +300,13 @@ def copy_src_tree():
CORE.relative_src_path("esphome", "core", "version.h"), generate_version_h() CORE.relative_src_path("esphome", "core", "version.h"), generate_version_h()
) )
if CORE.is_esp32: platform = "esphome.components." + CORE.target_platform
from esphome.components.esp32 import copy_files try:
module = importlib.import_module(platform)
copy_files = getattr(module, "copy_files")
copy_files() copy_files()
except AttributeError:
elif CORE.is_esp8266: pass
from esphome.components.esp8266 import copy_files
copy_files()
elif CORE.is_rp2040:
from esphome.components.rp2040 import copy_files
(pio) = copy_files()
if pio:
write_file_if_changed(
CORE.relative_src_path("esphome.h"),
ESPHOME_H_FORMAT.format(include_s + '\n#include "pio_includes.h"'),
)
def generate_defines_h(): def generate_defines_h():

View file

@ -12,7 +12,7 @@ pyserial==3.5
platformio==6.1.16 # When updating platformio, also update Dockerfile platformio==6.1.16 # When updating platformio, also update Dockerfile
esptool==4.7.0 esptool==4.7.0
click==8.1.7 click==8.1.7
esphome-dashboard==20240620.0 esphome-dashboard==20241025.0
aioesphomeapi==24.6.2 aioesphomeapi==24.6.2
zeroconf==0.132.2 zeroconf==0.132.2
puremagic==1.27 puremagic==1.27

View file

@ -0,0 +1,8 @@
i2c:
- id: i2c_i2c
scl: 16
sda: 17
i2c_device:
id: i2cdev
address: 0x2C

View file

@ -0,0 +1,8 @@
i2c:
- id: i2c_i2c
scl: 5
sda: 4
i2c_device:
id: i2cdev
address: 0x2C

View file

@ -0,0 +1,8 @@
i2c:
- id: i2c_i2c
scl: 5
sda: 4
i2c_device:
id: i2cdev
address: 0x2C

View file

@ -0,0 +1,8 @@
i2c:
- id: i2c_i2c
scl: 16
sda: 17
i2c_device:
id: i2cdev
address: 0x2C

View file

@ -0,0 +1,8 @@
i2c:
- id: i2c_i2c
scl: 5
sda: 4
i2c_device:
id: i2cdev
address: 0x2C

View file

@ -0,0 +1,8 @@
i2c:
- id: i2c_i2c
scl: 5
sda: 4
i2c_device:
id: i2cdev
address: 0x2C

View file

@ -0,0 +1,38 @@
image:
- id: binary_image
file: ../../pnglogo.png
type: BINARY
dither: FloydSteinberg
- id: transparent_transparent_image
file: ../../pnglogo.png
type: TRANSPARENT_BINARY
- id: rgba_image
file: ../../pnglogo.png
type: RGBA
resize: 50x50
- id: rgb24_image
file: ../../pnglogo.png
type: RGB24
use_transparency: yes
- id: rgb565_image
file: ../../pnglogo.png
type: RGB565
use_transparency: no
- id: web_svg_image
file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
resize: 256x48
type: TRANSPARENT_BINARY
- id: web_tiff_image
file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
type: RGB24
resize: 48x48
- id: web_redirect_image
file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
type: RGB24
resize: 48x48
- id: mdi_alert
file: mdi:alert-circle-outline
resize: 50x50
- id: another_alert_icon
file: mdi:alert-outline
type: BINARY

View file

@ -13,41 +13,5 @@ display:
reset_pin: 21 reset_pin: 21
invert_colors: true invert_colors: true
image: <<: !include common.yaml
- id: binary_image
file: ../../pnglogo.png
type: BINARY
dither: FloydSteinberg
- id: transparent_transparent_image
file: ../../pnglogo.png
type: TRANSPARENT_BINARY
- id: rgba_image
file: ../../pnglogo.png
type: RGBA
resize: 50x50
- id: rgb24_image
file: ../../pnglogo.png
type: RGB24
use_transparency: yes
- id: rgb565_image
file: ../../pnglogo.png
type: RGB565
use_transparency: no
- id: web_svg_image
file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
resize: 256x48
type: TRANSPARENT_BINARY
- id: web_tiff_image
file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
type: RGB24
resize: 48x48
- id: web_redirect_image
file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
type: RGB24
resize: 48x48
- id: mdi_alert
file: mdi:alert-circle-outline
resize: 50x50
- id: another_alert_icon
file: mdi:alert-outline
type: BINARY

View file

@ -13,41 +13,4 @@ display:
reset_pin: 10 reset_pin: 10
invert_colors: true invert_colors: true
image: <<: !include common.yaml
- id: binary_image
file: ../../pnglogo.png
type: BINARY
dither: FloydSteinberg
- id: transparent_transparent_image
file: ../../pnglogo.png
type: TRANSPARENT_BINARY
- id: rgba_image
file: ../../pnglogo.png
type: RGBA
resize: 50x50
- id: rgb24_image
file: ../../pnglogo.png
type: RGB24
use_transparency: yes
- id: rgb565_image
file: ../../pnglogo.png
type: RGB565
use_transparency: no
- id: web_svg_image
file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
resize: 256x48
type: TRANSPARENT_BINARY
- id: web_tiff_image
file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
type: RGB24
resize: 48x48
- id: web_redirect_image
file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
type: RGB24
resize: 48x48
- id: mdi_alert
file: mdi:alert-circle-outline
resize: 50x50
- id: another_alert_icon
file: mdi:alert-outline
type: BINARY

View file

@ -13,41 +13,4 @@ display:
reset_pin: 10 reset_pin: 10
invert_colors: true invert_colors: true
image: <<: !include common.yaml
- id: binary_image
file: ../../pnglogo.png
type: BINARY
dither: FloydSteinberg
- id: transparent_transparent_image
file: ../../pnglogo.png
type: TRANSPARENT_BINARY
- id: rgba_image
file: ../../pnglogo.png
type: RGBA
resize: 50x50
- id: rgb24_image
file: ../../pnglogo.png
type: RGB24
use_transparency: yes
- id: rgb565_image
file: ../../pnglogo.png
type: RGB565
use_transparency: no
- id: web_svg_image
file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
resize: 256x48
type: TRANSPARENT_BINARY
- id: web_tiff_image
file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
type: RGB24
resize: 48x48
- id: web_redirect_image
file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
type: RGB24
resize: 48x48
- id: mdi_alert
file: mdi:alert-circle-outline
resize: 50x50
- id: another_alert_icon
file: mdi:alert-outline
type: BINARY

View file

@ -13,41 +13,4 @@ display:
reset_pin: 21 reset_pin: 21
invert_colors: true invert_colors: true
image: <<: !include common.yaml
- id: binary_image
file: ../../pnglogo.png
type: BINARY
dither: FloydSteinberg
- id: transparent_transparent_image
file: ../../pnglogo.png
type: TRANSPARENT_BINARY
- id: rgba_image
file: ../../pnglogo.png
type: RGBA
resize: 50x50
- id: rgb24_image
file: ../../pnglogo.png
type: RGB24
use_transparency: yes
- id: rgb565_image
file: ../../pnglogo.png
type: RGB565
use_transparency: no
- id: web_svg_image
file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
resize: 256x48
type: TRANSPARENT_BINARY
- id: web_tiff_image
file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
type: RGB24
resize: 48x48
- id: web_redirect_image
file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
type: RGB24
resize: 48x48
- id: mdi_alert
file: mdi:alert-circle-outline
resize: 50x50
- id: another_alert_icon
file: mdi:alert-outline
type: BINARY

View file

@ -13,41 +13,4 @@ display:
reset_pin: 16 reset_pin: 16
invert_colors: true invert_colors: true
image: <<: !include common.yaml
- id: binary_image
file: ../../pnglogo.png
type: BINARY
dither: FloydSteinberg
- id: transparent_transparent_image
file: ../../pnglogo.png
type: TRANSPARENT_BINARY
- id: rgba_image
file: ../../pnglogo.png
type: RGBA
resize: 50x50
- id: rgb24_image
file: ../../pnglogo.png
type: RGB24
use_transparency: yes
- id: rgb565_image
file: ../../pnglogo.png
type: RGB565
use_transparency: no
- id: web_svg_image
file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
resize: 256x48
type: TRANSPARENT_BINARY
- id: web_tiff_image
file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
type: RGB24
resize: 48x48
- id: web_redirect_image
file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
type: RGB24
resize: 48x48
- id: mdi_alert
file: mdi:alert-circle-outline
resize: 50x50
- id: another_alert_icon
file: mdi:alert-outline
type: BINARY

View file

@ -0,0 +1,8 @@
display:
- platform: sdl
auto_clear_enabled: false
dimensions:
width: 480
height: 480
<<: !include common.yaml

View file

@ -13,41 +13,4 @@ display:
reset_pin: 22 reset_pin: 22
invert_colors: true invert_colors: true
image: <<: !include common.yaml
- id: binary_image
file: ../../pnglogo.png
type: BINARY
dither: FloydSteinberg
- id: transparent_transparent_image
file: ../../pnglogo.png
type: TRANSPARENT_BINARY
- id: rgba_image
file: ../../pnglogo.png
type: RGBA
resize: 50x50
- id: rgb24_image
file: ../../pnglogo.png
type: RGB24
use_transparency: yes
- id: rgb565_image
file: ../../pnglogo.png
type: RGB565
use_transparency: no
- id: web_svg_image
file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
resize: 256x48
type: TRANSPARENT_BINARY
- id: web_tiff_image
file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
type: RGB24
resize: 48x48
- id: web_redirect_image
file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
type: RGB24
resize: 48x48
- id: mdi_alert
file: mdi:alert-circle-outline
resize: 50x50
- id: another_alert_icon
file: mdi:alert-outline
type: BINARY

View file

@ -93,7 +93,7 @@ light:
- platform: lvgl - platform: lvgl
name: LVGL LED name: LVGL LED
id: lv_light id: lv_light
led: lv_led widget: lv_led
binary_sensor: binary_sensor:
- platform: lvgl - platform: lvgl

View file

@ -323,6 +323,13 @@ lvgl:
id: button_button id: button_button
width: 20% width: 20%
height: 10% height: 10%
transform_angle: !lambda return 180*100;
arc_width: !lambda return 4;
border_width: !lambda return 6;
shadow_ofs_x: !lambda return 6;
shadow_ofs_y: !lambda return 6;
shadow_spread: !lambda return 6;
shadow_width: !lambda return 6;
pressed: pressed:
bg_color: light_blue bg_color: light_blue
checkable: true checkable: true
@ -422,7 +429,7 @@ lvgl:
id: lv_image id: lv_image
src: cat_image src: cat_image
align: top_left align: top_left
y: 50 y: "50"
- tileview: - tileview:
id: tileview_id id: tileview_id
scrollbar_mode: active scrollbar_mode: active
@ -461,7 +468,7 @@ lvgl:
bg_opa: transp bg_opa: transp
knob: knob:
radius: 1 radius: 1
width: 4 width: "4"
height: 10% height: 10%
bg_color: 0x000000 bg_color: 0x000000
width: 100% width: 100%

View file

@ -27,6 +27,10 @@ media_player:
media_player.is_idle: media_player.is_idle:
- wait_until: - wait_until:
media_player.is_playing: media_player.is_playing:
- wait_until:
media_player.is_announcing:
- wait_until:
media_player.is_paused:
- media_player.volume_up: - media_player.volume_up:
- media_player.volume_down: - media_player.volume_down:
- media_player.volume_set: 50% - media_player.volume_set: 50%

View file

@ -227,6 +227,7 @@ datetime:
type: date type: date
state_topic: some/topic/date state_topic: some/topic/date
qos: 2 qos: 2
subscribe_qos: 2
set_action: set_action:
- logger.log: "set_value" - logger.log: "set_value"
on_value: on_value:

View file

@ -1,3 +1,76 @@
api:
wifi:
ap:
ssid: "Thermostat"
password: "MySecretThemostat"
opentherm: opentherm:
in_pin: 1 in_pin: 4
out_pin: 2 out_pin: 5
ch_enable: true
dhw_enable: false
cooling_enable: false
otc_active: false
ch2_active: true
summer_mode_active: true
dhw_block: true
sync_mode: true
sensor:
- platform: opentherm
rel_mod_level:
name: "Boiler Relative modulation level"
ch_pressure:
name: "Boiler Water pressure in CH circuit"
dhw_flow_rate:
name: "Boiler Water flow rate in DHW circuit"
t_boiler:
name: "Boiler water temperature"
t_dhw:
name: "Boiler DHW temperature"
t_outside:
name: "Boiler Outside temperature"
t_ret:
name: "Boiler Return water temperature"
t_storage:
name: "Boiler Solar storage temperature"
t_collector:
name: "Boiler Solar collector temperature"
t_flow_ch2:
name: "Boiler Flow water temperature CH2 circuit"
t_dhw2:
name: "Boiler Domestic hot water temperature 2"
t_exhaust:
name: "Boiler Exhaust temperature"
burner_starts:
name: "Boiler Number of starts burner"
ch_pump_starts:
name: "Boiler Number of starts CH pump"
dhw_pump_valve_starts:
name: "Boiler Number of starts DHW pump/valve"
dhw_burner_starts:
name: "Boiler Number of starts burner during DHW mode"
burner_operation_hours:
name: "Boiler Number of hours that burner is in operation (i.e. flame on)"
ch_pump_operation_hours:
name: "Boiler Number of hours that CH pump has been running"
dhw_pump_valve_operation_hours:
name: "Boiler Number of hours that DHW pump has been running or DHW valve has been opened"
dhw_burner_operation_hours:
name: "Boiler Number of hours that burner is in operation during DHW mode"
t_dhw_set_ub:
name: "Boiler Upper bound for adjustement of DHW setpoint"
t_dhw_set_lb:
name: "Boiler Lower bound for adjustement of DHW setpoint"
max_t_set_ub:
name: "Boiler Upper bound for adjustement of max CH setpoint"
max_t_set_lb:
name: "Boiler Lower bound for adjustement of max CH setpoint"
t_dhw_set:
name: "Boiler Domestic hot water temperature setpoint"
max_t_set:
name: "Boiler Maximum allowable CH water setpoint"
otc_hc_ratio_ub:
name: "OTC heat curve ratio upper bound"
otc_hc_ratio_lb:
name: "OTC heat curve ratio lower bound"

View file

@ -1,6 +1,8 @@
esphome: esphome:
on_boot: on_boot:
then: then:
- speaker.mute_on:
- speaker.mute_off:
- if: - if:
condition: speaker.is_stopped condition: speaker.is_stopped
then: then:

View file

@ -1,6 +1,8 @@
esphome: esphome:
on_boot: on_boot:
then: then:
- speaker.mute_on:
- speaker.mute_off:
- if: - if:
condition: speaker.is_stopped condition: speaker.is_stopped
then: then:

View file

@ -1,6 +1,8 @@
esphome: esphome:
on_boot: on_boot:
then: then:
- speaker.mute_on:
- speaker.mute_off:
- if: - if:
condition: speaker.is_stopped condition: speaker.is_stopped
then: then:

View file

@ -1,6 +1,8 @@
esphome: esphome:
on_boot: on_boot:
then: then:
- speaker.mute_on:
- speaker.mute_off:
- if: - if:
condition: speaker.is_stopped condition: speaker.is_stopped
then: then:
@ -17,8 +19,17 @@ i2s_audio:
i2s_bclk_pin: 17 i2s_bclk_pin: 17
i2s_mclk_pin: 15 i2s_mclk_pin: 15
i2c:
scl: 12
sda: 10
audio_dac:
- platform: aic3204
id: internal_dac
speaker: speaker:
- platform: i2s_audio - platform: i2s_audio
id: speaker_id id: speaker_with_audio_dac_id
audio_dac: internal_dac
dac_type: external dac_type: external
i2s_dout_pin: 13 i2s_dout_pin: 14