1
0
mirror of https://github.com/esphome/esphome.git synced 2025-03-15 15:18:16 +00:00

Merge branch 'bump_aioesphomeapi' into integration

This commit is contained in:
J. Nick Koston 2023-11-17 13:44:13 -06:00
commit 60a94032ff
No known key found for this signature in database
16 changed files with 445 additions and 123 deletions

97
.github/actions/build-image/action.yaml vendored Normal file
View File

@ -0,0 +1,97 @@
name: Build Image
inputs:
platform:
description: "Platform to build for"
required: true
example: "linux/amd64"
target:
description: "Target to build"
required: true
example: "docker"
baseimg:
description: "Base image type"
required: true
example: "docker"
suffix:
description: "Suffix to add to tags"
required: true
version:
description: "Version to build"
required: true
example: "2023.12.0"
runs:
using: "composite"
steps:
- name: Generate short tags
id: tags
shell: bash
run: |
output=$(docker/generate_tags.py \
--tag "${{ inputs.version }}" \
--suffix "${{ inputs.suffix }}")
echo $output
for l in $output; do
echo $l >> $GITHUB_OUTPUT
done
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@v5.0.0
with:
context: .
file: ./docker/Dockerfile
platforms: ${{ inputs.platform }}
target: ${{ inputs.target }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BASEIMGTYPE=${{ inputs.baseimg }}
BUILD_VERSION=${{ inputs.version }}
outputs: |
type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
- name: Export ghcr digests
shell: bash
run: |
mkdir -p /tmp/digests/${{ inputs.target }}/ghcr
digest="${{ steps.build-ghcr.outputs.digest }}"
touch "/tmp/digests/${{ inputs.target }}/ghcr/${digest#sha256:}"
- name: Upload ghcr digest
uses: actions/upload-artifact@v3.1.3
with:
name: digests-${{ inputs.target }}-ghcr
path: /tmp/digests/${{ inputs.target }}/ghcr/*
if-no-files-found: error
retention-days: 1
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@v5.0.0
with:
context: .
file: ./docker/Dockerfile
platforms: ${{ inputs.platform }}
target: ${{ inputs.target }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BASEIMGTYPE=${{ inputs.baseimg }}
BUILD_VERSION=${{ inputs.version }}
outputs: |
type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
- name: Export dockerhub digests
shell: bash
run: |
mkdir -p /tmp/digests/${{ inputs.target }}/dockerhub
digest="${{ steps.build-dockerhub.outputs.digest }}"
touch "/tmp/digests/${{ inputs.target }}/dockerhub/${digest#sha256:}"
- name: Upload dockerhub digest
uses: actions/upload-artifact@v3.1.3
with:
name: digests-${{ inputs.target }}-dockerhub
path: /tmp/digests/${{ inputs.target }}/dockerhub/*
if-no-files-found: error
retention-days: 1

View File

@ -63,30 +63,20 @@ jobs:
run: twine upload dist/* run: twine upload dist/*
deploy-docker: deploy-docker:
name: Build and publish ESPHome ${{ matrix.image.title}} name: Build ESPHome ${{ matrix.platform }}
if: github.repository == 'esphome/esphome' if: github.repository == 'esphome/esphome'
permissions: permissions:
contents: read contents: read
packages: write packages: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: ${{ matrix.image.title == 'lint' }}
needs: [init] needs: [init]
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
image: platform:
- title: "ha-addon" - linux/amd64
suffix: "hassio" - linux/arm/v7
target: "hassio" - linux/arm64
baseimg: "hassio"
- title: "docker"
suffix: ""
target: "docker"
baseimg: "docker"
- title: "lint"
suffix: "lint"
target: "lint"
baseimg: "docker"
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
- name: Set up Python - name: Set up Python
@ -97,6 +87,7 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0 uses: docker/setup-buildx-action@v3.0.0
- name: Set up QEMU - name: Set up QEMU
if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v3.0.0 uses: docker/setup-qemu-action@v3.0.0
- name: Log in to docker hub - name: Log in to docker hub
@ -111,34 +102,105 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build docker
uses: ./.github/actions/build-image
with:
platform: ${{ matrix.platform }}
target: docker
baseimg: docker
suffix: ""
version: ${{ needs.init.outputs.tag }}
- name: Build ha-addon
uses: ./.github/actions/build-image
with:
platform: ${{ matrix.platform }}
target: hassio
baseimg: hassio
suffix: "hassio"
version: ${{ needs.init.outputs.tag }}
- name: Build lint
uses: ./.github/actions/build-image
with:
platform: ${{ matrix.platform }}
target: lint
baseimg: docker
suffix: lint
version: ${{ needs.init.outputs.tag }}
deploy-manifest:
name: Publish ESPHome ${{ matrix.image.title }} to ${{ matrix.registry }}
runs-on: ubuntu-latest
needs:
- init
- deploy-docker
if: github.repository == 'esphome/esphome'
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
image:
- title: "ha-addon"
target: "hassio"
suffix: "hassio"
- title: "docker"
target: "docker"
suffix: ""
- title: "lint"
target: "lint"
suffix: "lint"
registry:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@v4.1.1
- name: Download digests
uses: actions/download-artifact@v3.0.2
with:
name: digests-${{ matrix.image.target }}-${{ matrix.registry }}
path: /tmp/digests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate short tags - name: Generate short tags
id: tags id: tags
run: | run: |
docker/generate_tags.py \ output=$(docker/generate_tags.py \
--tag "${{ needs.init.outputs.tag }}" \ --tag "${{ needs.init.outputs.tag }}" \
--suffix "${{ matrix.image.suffix }}" --suffix "${{ matrix.image.suffix }}" \
--registry "${{ matrix.registry }}")
echo $output
for l in $output; do
echo $l >> $GITHUB_OUTPUT
done
- name: Build and push - name: Create manifest list and push
uses: docker/build-push-action@v5.0.0 working-directory: /tmp/digests
with: run: |
context: . docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
file: ./docker/Dockerfile $(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)
platforms: linux/amd64,linux/arm/v7,linux/arm64
target: ${{ matrix.image.target }}
push: true
# yamllint disable rule:line-length
cache-from: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }}
cache-to: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }},mode=max
# yamllint enable rule:line-length
tags: ${{ steps.tags.outputs.tags }}
build-args: |
BASEIMGTYPE=${{ matrix.image.baseimg }}
BUILD_VERSION=${{ needs.init.outputs.tag }}
deploy-ha-addon-repo: deploy-ha-addon-repo:
if: github.repository == 'esphome/esphome' && github.event_name == 'release' if: github.repository == 'esphome/esphome' && github.event_name == 'release'
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [deploy-docker] needs: [deploy-manifest]
steps: steps:
- name: Trigger Workflow - name: Trigger Workflow
uses: actions/github-script@v6.4.1 uses: actions/github-script@v6.4.1

View File

@ -68,7 +68,7 @@ ENV \
# See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian # See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian
RUN \ RUN \
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3; \ ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \
fi fi
RUN \ RUN \

View File

@ -1,13 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import re import re
import os
import argparse import argparse
import json
CHANNEL_DEV = "dev" CHANNEL_DEV = "dev"
CHANNEL_BETA = "beta" CHANNEL_BETA = "beta"
CHANNEL_RELEASE = "release" CHANNEL_RELEASE = "release"
GHCR = "ghcr"
DOCKERHUB = "dockerhub"
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
"--tag", "--tag",
@ -21,21 +22,31 @@ parser.add_argument(
required=True, required=True,
help="The suffix of the tag.", help="The suffix of the tag.",
) )
parser.add_argument(
"--registry",
type=str,
choices=[GHCR, DOCKERHUB],
required=False,
action="append",
help="The registry to build tags for.",
)
def main(): def main():
args = parser.parse_args() args = parser.parse_args()
# detect channel from tag # detect channel from tag
match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag) match = re.match(r"^(\d+\.\d+)(?:\.\d+)(?:(b\d+)|(-dev\d+))?$", args.tag)
major_minor_version = None major_minor_version = None
if match is None: if match is None: # eg 2023.12.0-dev20231109-testbranch
channel = None # Ran with custom tag for a branch etc
elif match.group(3) is not None: # eg 2023.12.0-dev20231109
channel = CHANNEL_DEV channel = CHANNEL_DEV
elif match.group(2) is None: elif match.group(2) is not None: # eg 2023.12.0b1
channel = CHANNEL_BETA
else: # eg 2023.12.0
major_minor_version = match.group(1) major_minor_version = match.group(1)
channel = CHANNEL_RELEASE channel = CHANNEL_RELEASE
else:
channel = CHANNEL_BETA
tags_to_push = [args.tag] tags_to_push = [args.tag]
if channel == CHANNEL_DEV: if channel == CHANNEL_DEV:
@ -53,15 +64,28 @@ def main():
suffix = f"-{args.suffix}" if args.suffix else "" suffix = f"-{args.suffix}" if args.suffix else ""
with open(os.environ["GITHUB_OUTPUT"], "w") as f: image_name = f"esphome/esphome{suffix}"
print(f"channel={channel}", file=f)
print(f"image=esphome/esphome{suffix}", file=f) print(f"channel={channel}")
if args.registry is None:
args.registry = [GHCR, DOCKERHUB]
elif len(args.registry) == 1:
if GHCR in args.registry:
print(f"image=ghcr.io/{image_name}")
if DOCKERHUB in args.registry:
print(f"image=docker.io/{image_name}")
print(f"image_name={image_name}")
full_tags = [] full_tags = []
for tag in tags_to_push: for tag in tags_to_push:
full_tags += [f"ghcr.io/esphome/esphome{suffix}:{tag}"] if GHCR in args.registry:
full_tags += [f"esphome/esphome{suffix}:{tag}"] full_tags += [f"ghcr.io/{image_name}:{tag}"]
print(f"tags={','.join(full_tags)}", file=f) if DOCKERHUB in args.registry:
full_tags += [f"docker.io/{image_name}:{tag}"]
print(f"tags={','.join(full_tags)}")
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -18,9 +18,10 @@ from . import CONF_ENCRYPTION
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_run_logs(config, address): async def async_run_logs(config: dict[str, Any], address: str) -> None:
"""Run the logs command in the event loop.""" """Run the logs command in the event loop."""
conf = config["api"] conf = config["api"]
name = config["esphome"]["name"]
port: int = int(conf[CONF_PORT]) port: int = int(conf[CONF_PORT])
password: str = conf[CONF_PASSWORD] password: str = conf[CONF_PASSWORD]
noise_psk: str | None = None noise_psk: str | None = None
@ -28,7 +29,6 @@ async def async_run_logs(config, address):
noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
_LOGGER.info("Starting log output from %s using esphome API", address) _LOGGER.info("Starting log output from %s using esphome API", address)
aiozc = AsyncZeroconf() aiozc = AsyncZeroconf()
cli = APIClient( cli = APIClient(
address, address,
port, port,
@ -48,7 +48,7 @@ async def async_run_logs(config, address):
text = text.replace("\033", "\\033") text = text.replace("\033", "\\033")
print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}") print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}")
stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc) stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc, name=name)
try: try:
while True: while True:
await asyncio.sleep(60) await asyncio.sleep(60)

View File

@ -1,5 +1,6 @@
#include "my9231.h" #include "my9231.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome { namespace esphome {
namespace my9231 { namespace my9231 {
@ -51,7 +52,11 @@ void MY9231OutputComponent::setup() {
MY9231_CMD_SCATTER_APDM | MY9231_CMD_FREQUENCY_DIVIDE_1 | MY9231_CMD_REACTION_FAST | MY9231_CMD_ONE_SHOT_DISABLE; MY9231_CMD_SCATTER_APDM | MY9231_CMD_FREQUENCY_DIVIDE_1 | MY9231_CMD_REACTION_FAST | MY9231_CMD_ONE_SHOT_DISABLE;
ESP_LOGV(TAG, " Command: 0x%02X", command); ESP_LOGV(TAG, " Command: 0x%02X", command);
{
InterruptLock lock;
this->send_dcki_pulses_(32 * this->num_chips_);
this->init_chips_(command); this->init_chips_(command);
}
ESP_LOGV(TAG, " Chips initialized."); ESP_LOGV(TAG, " Chips initialized.");
} }
void MY9231OutputComponent::dump_config() { void MY9231OutputComponent::dump_config() {
@ -66,11 +71,14 @@ void MY9231OutputComponent::loop() {
if (!this->update_) if (!this->update_)
return; return;
{
InterruptLock lock;
for (auto pwm_amount : this->pwm_amounts_) { for (auto pwm_amount : this->pwm_amounts_) {
this->write_word_(pwm_amount, this->bit_depth_); this->write_word_(pwm_amount, this->bit_depth_);
} }
// Send 8 DI pulses. After 8 falling edges, the duty data are store. // Send 8 DI pulses. After 8 falling edges, the duty data are store.
this->send_di_pulses_(8); this->send_di_pulses_(8);
}
this->update_ = false; this->update_ = false;
} }
void MY9231OutputComponent::set_channel_value_(uint8_t channel, uint16_t value) { void MY9231OutputComponent::set_channel_value_(uint8_t channel, uint16_t value) {
@ -92,6 +100,7 @@ void MY9231OutputComponent::init_chips_(uint8_t command) {
// Send 16 DI pulse. After 14 falling edges, the command data are // Send 16 DI pulse. After 14 falling edges, the command data are
// stored and after 16 falling edges the duty mode is activated. // stored and after 16 falling edges the duty mode is activated.
this->send_di_pulses_(16); this->send_di_pulses_(16);
delayMicroseconds(12);
} }
void MY9231OutputComponent::write_word_(uint16_t value, uint8_t bits) { void MY9231OutputComponent::write_word_(uint16_t value, uint8_t bits) {
for (uint8_t i = bits; i > 0; i--) { for (uint8_t i = bits; i > 0; i--) {
@ -106,6 +115,13 @@ void MY9231OutputComponent::send_di_pulses_(uint8_t count) {
this->pin_di_->digital_write(false); this->pin_di_->digital_write(false);
} }
} }
void MY9231OutputComponent::send_dcki_pulses_(uint8_t count) {
delayMicroseconds(12);
for (uint8_t i = 0; i < count; i++) {
this->pin_dcki_->digital_write(true);
this->pin_dcki_->digital_write(false);
}
}
} // namespace my9231 } // namespace my9231
} // namespace esphome } // namespace esphome

View File

@ -49,6 +49,7 @@ class MY9231OutputComponent : public Component {
void init_chips_(uint8_t command); void init_chips_(uint8_t command);
void write_word_(uint16_t value, uint8_t bits); void write_word_(uint16_t value, uint8_t bits);
void send_di_pulses_(uint8_t count); void send_di_pulses_(uint8_t count);
void send_dcki_pulses_(uint8_t count);
GPIOPin *pin_di_; GPIOPin *pin_di_;
GPIOPin *pin_dcki_; GPIOPin *pin_dcki_;

View File

@ -33,6 +33,7 @@ MODELS = {
"SH1106_96X16": SSD1306Model.SH1106_MODEL_96_16, "SH1106_96X16": SSD1306Model.SH1106_MODEL_96_16,
"SH1106_64X48": SSD1306Model.SH1106_MODEL_64_48, "SH1106_64X48": SSD1306Model.SH1106_MODEL_64_48,
"SH1107_128X64": SSD1306Model.SH1107_MODEL_128_64, "SH1107_128X64": SSD1306Model.SH1107_MODEL_128_64,
"SH1107_128X128": SSD1306Model.SH1107_MODEL_128_128,
"SSD1305_128X32": SSD1306Model.SSD1305_MODEL_128_32, "SSD1305_128X32": SSD1306Model.SSD1305_MODEL_128_32,
"SSD1305_128X64": SSD1306Model.SSD1305_MODEL_128_64, "SSD1305_128X64": SSD1306Model.SSD1305_MODEL_128_64,
} }
@ -63,8 +64,10 @@ SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, cv.Optional(CONF_EXTERNAL_VCC): cv.boolean,
cv.Optional(CONF_FLIP_X, default=True): cv.boolean, cv.Optional(CONF_FLIP_X, default=True): cv.boolean,
cv.Optional(CONF_FLIP_Y, default=True): cv.boolean, cv.Optional(CONF_FLIP_Y, default=True): cv.boolean,
cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=-32, max=32), # Offsets determine shifts of memory location to LCD rows/columns,
cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=-32, max=32), # and this family of controllers supports up to 128x128 screens
cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=0, max=128),
cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=0, max=128),
cv.Optional(CONF_INVERT, default=False): cv.boolean, cv.Optional(CONF_INVERT, default=False): cv.boolean,
} }
).extend(cv.polling_component_schema("1s")) ).extend(cv.polling_component_schema("1s"))

View File

@ -35,16 +35,31 @@ static const uint8_t SSD1306_COMMAND_INVERSE_DISPLAY = 0xA7;
static const uint8_t SSD1305_COMMAND_SET_BRIGHTNESS = 0x82; static const uint8_t SSD1305_COMMAND_SET_BRIGHTNESS = 0x82;
static const uint8_t SSD1305_COMMAND_SET_AREA_COLOR = 0xD8; static const uint8_t SSD1305_COMMAND_SET_AREA_COLOR = 0xD8;
static const uint8_t SH1107_COMMAND_SET_START_LINE = 0xDC;
static const uint8_t SH1107_COMMAND_CHARGE_PUMP = 0xAD;
void SSD1306::setup() { void SSD1306::setup() {
this->init_internal_(this->get_buffer_length_()); this->init_internal_(this->get_buffer_length_());
// SH1107 resources
//
// Datasheet v2.3:
// www.displayfuture.com/Display/datasheet/controller/SH1107.pdf
// Adafruit C++ driver:
// github.com/adafruit/Adafruit_SH110x
// Adafruit CircuitPython driver:
// github.com/adafruit/Adafruit_CircuitPython_DisplayIO_SH1107
// Turn off display during initialization (0xAE) // Turn off display during initialization (0xAE)
this->command(SSD1306_COMMAND_DISPLAY_OFF); this->command(SSD1306_COMMAND_DISPLAY_OFF);
// If SH1107, use POR defaults (0x50) = divider 1, frequency +0%
if (!this->is_sh1107_()) {
// Set oscillator frequency to 4'b1000 with no clock division (0xD5) // Set oscillator frequency to 4'b1000 with no clock division (0xD5)
this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV); this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV);
// Oscillator frequency <= 4'b1000, no clock division // Oscillator frequency <= 4'b1000, no clock division
this->command(0x80); this->command(0x80);
}
// Enable low power display mode for SSD1305 (0xD8) // Enable low power display mode for SSD1305 (0xD8)
if (this->is_ssd1305_()) { if (this->is_ssd1305_()) {
@ -60,11 +75,26 @@ void SSD1306::setup() {
this->command(SSD1306_COMMAND_SET_DISPLAY_OFFSET_Y); this->command(SSD1306_COMMAND_SET_DISPLAY_OFFSET_Y);
this->command(0x00 + this->offset_y_); this->command(0x00 + this->offset_y_);
if (this->is_sh1107_()) {
// Set start line at line 0 (0xDC)
this->command(SH1107_COMMAND_SET_START_LINE);
this->command(0x00);
} else {
// Set start line at line 0 (0x40) // Set start line at line 0 (0x40)
this->command(SSD1306_COMMAND_SET_START_LINE | 0x00); this->command(SSD1306_COMMAND_SET_START_LINE | 0x00);
}
if (this->is_ssd1305_()) {
// SSD1305 does not have charge pump // SSD1305 does not have charge pump
if (!this->is_ssd1305_()) { } else if (this->is_sh1107_()) {
// Enable charge pump (0xAD)
this->command(SH1107_COMMAND_CHARGE_PUMP);
if (this->external_vcc_) {
this->command(0x8A);
} else {
this->command(0x8B);
}
} else {
// Enable charge pump (0x8D) // Enable charge pump (0x8D)
this->command(SSD1306_COMMAND_CHARGE_PUMP); this->command(SSD1306_COMMAND_CHARGE_PUMP);
if (this->external_vcc_) { if (this->external_vcc_) {
@ -76,14 +106,17 @@ void SSD1306::setup() {
// Set addressing mode to horizontal (0x20) // Set addressing mode to horizontal (0x20)
this->command(SSD1306_COMMAND_MEMORY_MODE); this->command(SSD1306_COMMAND_MEMORY_MODE);
if (!this->is_sh1107_()) {
// SH1107 memory mode is a 1 byte command
this->command(0x00); this->command(0x00);
}
// X flip mode (0xA0, 0xA1) // X flip mode (0xA0, 0xA1)
this->command(SSD1306_COMMAND_SEGRE_MAP | this->flip_x_); this->command(SSD1306_COMMAND_SEGRE_MAP | this->flip_x_);
// Y flip mode (0xC0, 0xC8) // Y flip mode (0xC0, 0xC8)
this->command(SSD1306_COMMAND_COM_SCAN_INC | (this->flip_y_ << 3)); this->command(SSD1306_COMMAND_COM_SCAN_INC | (this->flip_y_ << 3));
if (!this->is_sh1107_()) {
// Set pin configuration (0xDA) // Set pin configuration (0xDA)
this->command(SSD1306_COMMAND_SET_COM_PINS); this->command(SSD1306_COMMAND_SET_COM_PINS);
switch (this->model_) { switch (this->model_) {
@ -98,12 +131,16 @@ void SSD1306::setup() {
case SSD1306_MODEL_64_48: case SSD1306_MODEL_64_48:
case SSD1306_MODEL_64_32: case SSD1306_MODEL_64_32:
case SH1106_MODEL_64_48: case SH1106_MODEL_64_48:
case SH1107_MODEL_128_64:
case SSD1305_MODEL_128_32: case SSD1305_MODEL_128_32:
case SSD1305_MODEL_128_64: case SSD1305_MODEL_128_64:
case SSD1306_MODEL_72_40: case SSD1306_MODEL_72_40:
this->command(0x12); this->command(0x12);
break; break;
case SH1107_MODEL_128_64:
case SH1107_MODEL_128_128:
// Not used, but prevents build warning
break;
}
} }
// Pre-charge period (0xD9) // Pre-charge period (0xD9)
@ -118,6 +155,7 @@ void SSD1306::setup() {
this->command(SSD1306_COMMAND_SET_VCOM_DETECT); this->command(SSD1306_COMMAND_SET_VCOM_DETECT);
switch (this->model_) { switch (this->model_) {
case SH1107_MODEL_128_64: case SH1107_MODEL_128_64:
case SH1107_MODEL_128_128:
this->command(0x35); this->command(0x35);
break; break;
case SSD1306_MODEL_72_40: case SSD1306_MODEL_72_40:
@ -149,7 +187,7 @@ void SSD1306::setup() {
this->turn_on(); this->turn_on();
} }
void SSD1306::display() { void SSD1306::display() {
if (this->is_sh1106_()) { if (this->is_sh1106_() || this->is_sh1107_()) {
this->write_display_data(); this->write_display_data();
return; return;
} }
@ -183,6 +221,7 @@ bool SSD1306::is_sh1106_() const {
return this->model_ == SH1106_MODEL_96_16 || this->model_ == SH1106_MODEL_128_32 || return this->model_ == SH1106_MODEL_96_16 || this->model_ == SH1106_MODEL_128_32 ||
this->model_ == SH1106_MODEL_128_64; this->model_ == SH1106_MODEL_128_64;
} }
bool SSD1306::is_sh1107_() const { return this->model_ == SH1107_MODEL_128_64 || this->model_ == SH1107_MODEL_128_128; }
bool SSD1306::is_ssd1305_() const { bool SSD1306::is_ssd1305_() const {
return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64; return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64;
} }
@ -224,6 +263,7 @@ void SSD1306::turn_off() {
int SSD1306::get_height_internal() { int SSD1306::get_height_internal() {
switch (this->model_) { switch (this->model_) {
case SH1107_MODEL_128_64: case SH1107_MODEL_128_64:
case SH1107_MODEL_128_128:
return 128; return 128;
case SSD1306_MODEL_128_32: case SSD1306_MODEL_128_32:
case SSD1306_MODEL_64_32: case SSD1306_MODEL_64_32:
@ -254,6 +294,7 @@ int SSD1306::get_width_internal() {
case SH1106_MODEL_128_64: case SH1106_MODEL_128_64:
case SSD1305_MODEL_128_32: case SSD1305_MODEL_128_32:
case SSD1305_MODEL_128_64: case SSD1305_MODEL_128_64:
case SH1107_MODEL_128_128:
return 128; return 128;
case SSD1306_MODEL_96_16: case SSD1306_MODEL_96_16:
case SH1106_MODEL_96_16: case SH1106_MODEL_96_16:

View File

@ -19,6 +19,7 @@ enum SSD1306Model {
SH1106_MODEL_96_16, SH1106_MODEL_96_16,
SH1106_MODEL_64_48, SH1106_MODEL_64_48,
SH1107_MODEL_128_64, SH1107_MODEL_128_64,
SH1107_MODEL_128_128,
SSD1305_MODEL_128_32, SSD1305_MODEL_128_32,
SSD1305_MODEL_128_64, SSD1305_MODEL_128_64,
}; };
@ -58,6 +59,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer {
void init_reset_(); void init_reset_();
bool is_sh1106_() const; bool is_sh1106_() const;
bool is_sh1107_() const;
bool is_ssd1305_() const; bool is_ssd1305_() const;
void draw_absolute_pixel_internal(int x, int y, Color color) override; void draw_absolute_pixel_internal(int x, int y, Color color) override;

View File

@ -38,13 +38,19 @@ void I2CSSD1306::dump_config() {
} }
void I2CSSD1306::command(uint8_t value) { this->write_byte(0x00, value); } void I2CSSD1306::command(uint8_t value) { this->write_byte(0x00, value); }
void HOT I2CSSD1306::write_display_data() { void HOT I2CSSD1306::write_display_data() {
if (this->is_sh1106_()) { if (this->is_sh1106_() || this->is_sh1107_()) {
uint32_t i = 0; uint32_t i = 0;
for (uint8_t page = 0; page < (uint8_t) this->get_height_internal() / 8; page++) { for (uint8_t page = 0; page < (uint8_t) this->get_height_internal() / 8; page++) {
this->command(0xB0 + page); // row this->command(0xB0 + page); // row
this->command(0x02); // lower column if (this->is_sh1106_()) {
this->command(0x02); // lower column - 0x02 is historical SH1106 value
} else {
// Other SH1107 drivers use 0x00
// Column values dont change and it seems they can be set only once,
// but we follow SH1106 implementation and resend them
this->command(0x00);
}
this->command(0x10); // higher column this->command(0x10); // higher column
for (uint8_t x = 0; x < (uint8_t) this->get_width_internal() / 16; x++) { for (uint8_t x = 0; x < (uint8_t) this->get_width_internal() / 16; x++) {
uint8_t data[16]; uint8_t data[16];
for (uint8_t &j : data) for (uint8_t &j : data)

View File

@ -36,10 +36,14 @@ void SPISSD1306::command(uint8_t value) {
this->disable(); this->disable();
} }
void HOT SPISSD1306::write_display_data() { void HOT SPISSD1306::write_display_data() {
if (this->is_sh1106_()) { if (this->is_sh1106_() || this->is_sh1107_()) {
for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) { for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) {
this->command(0xB0 + y); this->command(0xB0 + y);
if (this->is_sh1106_()) {
this->command(0x02); this->command(0x02);
} else {
this->command(0x00);
}
this->command(0x10); this->command(0x10);
this->dc_pin_->digital_write(true); this->dc_pin_->digital_write(true);
for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x++) { for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x++) {

View File

@ -18,20 +18,25 @@ DEPENDENCIES = ["api", "microphone"]
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
CONF_SILENCE_DETECTION = "silence_detection"
CONF_ON_LISTENING = "on_listening"
CONF_ON_START = "on_start"
CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected"
CONF_ON_STT_END = "on_stt_end"
CONF_ON_TTS_START = "on_tts_start"
CONF_ON_TTS_END = "on_tts_end"
CONF_ON_END = "on_end" CONF_ON_END = "on_end"
CONF_ON_ERROR = "on_error" CONF_ON_ERROR = "on_error"
CONF_ON_INTENT_END = "on_intent_end"
CONF_ON_INTENT_START = "on_intent_start"
CONF_ON_LISTENING = "on_listening"
CONF_ON_START = "on_start"
CONF_ON_STT_END = "on_stt_end"
CONF_ON_STT_VAD_END = "on_stt_vad_end"
CONF_ON_STT_VAD_START = "on_stt_vad_start"
CONF_ON_TTS_END = "on_tts_end"
CONF_ON_TTS_START = "on_tts_start"
CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected"
CONF_SILENCE_DETECTION = "silence_detection"
CONF_USE_WAKE_WORD = "use_wake_word" CONF_USE_WAKE_WORD = "use_wake_word"
CONF_VAD_THRESHOLD = "vad_threshold" CONF_VAD_THRESHOLD = "vad_threshold"
CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level"
CONF_AUTO_GAIN = "auto_gain" CONF_AUTO_GAIN = "auto_gain"
CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level"
CONF_VOLUME_MULTIPLIER = "volume_multiplier" CONF_VOLUME_MULTIPLIER = "volume_multiplier"
@ -88,6 +93,18 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation(
single=True single=True
), ),
cv.Optional(CONF_ON_INTENT_START): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_INTENT_END): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_STT_VAD_START): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_STT_VAD_END): automation.validate_automation(
single=True
),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
) )
@ -177,6 +194,34 @@ async def to_code(config):
config[CONF_ON_CLIENT_DISCONNECTED], config[CONF_ON_CLIENT_DISCONNECTED],
) )
if CONF_ON_INTENT_START in config:
await automation.build_automation(
var.get_intent_start_trigger(),
[],
config[CONF_ON_INTENT_START],
)
if CONF_ON_INTENT_END in config:
await automation.build_automation(
var.get_intent_end_trigger(),
[],
config[CONF_ON_INTENT_END],
)
if CONF_ON_STT_VAD_START in config:
await automation.build_automation(
var.get_stt_vad_start_trigger(),
[],
config[CONF_ON_STT_VAD_START],
)
if CONF_ON_STT_VAD_END in config:
await automation.build_automation(
var.get_stt_vad_end_trigger(),
[],
config[CONF_ON_STT_VAD_END],
)
cg.add_define("USE_VOICE_ASSISTANT") cg.add_define("USE_VOICE_ASSISTANT")

View File

@ -31,7 +31,7 @@ void VoiceAssistant::setup() {
this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (socket_ == nullptr) { if (socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket."); ESP_LOGW(TAG, "Could not create socket");
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -69,7 +69,7 @@ void VoiceAssistant::setup() {
ExternalRAMAllocator<uint8_t> speaker_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); ExternalRAMAllocator<uint8_t> speaker_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE); this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE);
if (this->speaker_buffer_ == nullptr) { if (this->speaker_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate speaker buffer."); ESP_LOGW(TAG, "Could not allocate speaker buffer");
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -79,7 +79,7 @@ void VoiceAssistant::setup() {
ExternalRAMAllocator<int16_t> allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE); ExternalRAMAllocator<int16_t> allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE); this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE);
if (this->input_buffer_ == nullptr) { if (this->input_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate input buffer."); ESP_LOGW(TAG, "Could not allocate input buffer");
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -89,7 +89,7 @@ void VoiceAssistant::setup() {
this->ring_buffer_ = rb_create(BUFFER_SIZE, sizeof(int16_t)); this->ring_buffer_ = rb_create(BUFFER_SIZE, sizeof(int16_t));
if (this->ring_buffer_ == nullptr) { if (this->ring_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate ring buffer."); ESP_LOGW(TAG, "Could not allocate ring buffer");
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -98,7 +98,7 @@ void VoiceAssistant::setup() {
ExternalRAMAllocator<uint8_t> send_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); ExternalRAMAllocator<uint8_t> send_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE); this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE);
if (send_buffer_ == nullptr) { if (send_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate send buffer."); ESP_LOGW(TAG, "Could not allocate send buffer");
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -221,8 +221,8 @@ void VoiceAssistant::loop() {
msg.audio_settings = audio_settings; msg.audio_settings = audio_settings;
if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) { if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) {
ESP_LOGW(TAG, "Could not request start."); ESP_LOGW(TAG, "Could not request start");
this->error_trigger_->trigger("not-connected", "Could not request start."); this->error_trigger_->trigger("not-connected", "Could not request start");
this->continuous_ = false; this->continuous_ = false;
this->set_state_(State::IDLE, State::IDLE); this->set_state_(State::IDLE, State::IDLE);
break; break;
@ -280,7 +280,7 @@ void VoiceAssistant::loop() {
this->speaker_buffer_size_ += len; this->speaker_buffer_size_ += len;
} }
} else { } else {
ESP_LOGW(TAG, "Receive buffer full."); ESP_LOGW(TAG, "Receive buffer full");
} }
if (this->speaker_buffer_size_ > 0) { if (this->speaker_buffer_size_ > 0) {
size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_);
@ -290,7 +290,7 @@ void VoiceAssistant::loop() {
this->speaker_buffer_index_ -= written; this->speaker_buffer_index_ -= written;
this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); }); this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); });
} else { } else {
ESP_LOGW(TAG, "Speaker buffer full."); ESP_LOGW(TAG, "Speaker buffer full");
} }
} }
if (this->wait_for_stream_end_) { if (this->wait_for_stream_end_) {
@ -513,7 +513,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
break; break;
} }
case api::enums::VOICE_ASSISTANT_STT_START: case api::enums::VOICE_ASSISTANT_STT_START:
ESP_LOGD(TAG, "STT Started"); ESP_LOGD(TAG, "STT started");
this->listening_trigger_->trigger(); this->listening_trigger_->trigger();
break; break;
case api::enums::VOICE_ASSISTANT_STT_END: { case api::enums::VOICE_ASSISTANT_STT_END: {
@ -525,19 +525,24 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
} }
} }
if (text.empty()) { if (text.empty()) {
ESP_LOGW(TAG, "No text in STT_END event."); ESP_LOGW(TAG, "No text in STT_END event");
return; return;
} }
ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str());
this->stt_end_trigger_->trigger(text); this->stt_end_trigger_->trigger(text);
break; break;
} }
case api::enums::VOICE_ASSISTANT_INTENT_START:
ESP_LOGD(TAG, "Intent started");
this->intent_start_trigger_->trigger();
break;
case api::enums::VOICE_ASSISTANT_INTENT_END: { case api::enums::VOICE_ASSISTANT_INTENT_END: {
for (auto arg : msg.data) { for (auto arg : msg.data) {
if (arg.name == "conversation_id") { if (arg.name == "conversation_id") {
this->conversation_id_ = std::move(arg.value); this->conversation_id_ = std::move(arg.value);
} }
} }
this->intent_end_trigger_->trigger();
break; break;
} }
case api::enums::VOICE_ASSISTANT_TTS_START: { case api::enums::VOICE_ASSISTANT_TTS_START: {
@ -548,7 +553,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
} }
} }
if (text.empty()) { if (text.empty()) {
ESP_LOGW(TAG, "No text in TTS_START event."); ESP_LOGW(TAG, "No text in TTS_START event");
return; return;
} }
ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); ESP_LOGD(TAG, "Response: \"%s\"", text.c_str());
@ -566,7 +571,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
} }
} }
if (url.empty()) { if (url.empty()) {
ESP_LOGW(TAG, "No url in TTS_END event."); ESP_LOGW(TAG, "No url in TTS_END event");
return; return;
} }
ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str());
@ -634,6 +639,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
this->set_state_(State::RESPONSE_FINISHED, State::IDLE); this->set_state_(State::RESPONSE_FINISHED, State::IDLE);
break; break;
} }
case api::enums::VOICE_ASSISTANT_STT_VAD_START:
ESP_LOGD(TAG, "Starting STT by VAD");
this->stt_vad_start_trigger_->trigger();
break;
case api::enums::VOICE_ASSISTANT_STT_VAD_END:
ESP_LOGD(TAG, "STT by VAD end");
this->stt_vad_end_trigger_->trigger();
break;
default: default:
ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type); ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type);
break; break;

View File

@ -100,13 +100,17 @@ class VoiceAssistant : public Component {
void set_auto_gain(uint8_t auto_gain) { this->auto_gain_ = auto_gain; } void set_auto_gain(uint8_t auto_gain) { this->auto_gain_ = auto_gain; }
void set_volume_multiplier(float volume_multiplier) { this->volume_multiplier_ = volume_multiplier; } void set_volume_multiplier(float volume_multiplier) { this->volume_multiplier_ = volume_multiplier; }
Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; }
Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; }
Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } Trigger<> *get_listening_trigger() const { return this->listening_trigger_; }
Trigger<> *get_end_trigger() const { return this->end_trigger_; }
Trigger<> *get_start_trigger() const { return this->start_trigger_; } Trigger<> *get_start_trigger() const { return this->start_trigger_; }
Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; }
Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; }
Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; }
Trigger<std::string> *get_stt_end_trigger() const { return this->stt_end_trigger_; } Trigger<std::string> *get_stt_end_trigger() const { return this->stt_end_trigger_; }
Trigger<std::string> *get_tts_start_trigger() const { return this->tts_start_trigger_; }
Trigger<std::string> *get_tts_end_trigger() const { return this->tts_end_trigger_; } Trigger<std::string> *get_tts_end_trigger() const { return this->tts_end_trigger_; }
Trigger<> *get_end_trigger() const { return this->end_trigger_; } Trigger<std::string> *get_tts_start_trigger() const { return this->tts_start_trigger_; }
Trigger<std::string, std::string> *get_error_trigger() const { return this->error_trigger_; } Trigger<std::string, std::string> *get_error_trigger() const { return this->error_trigger_; }
Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; } Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
@ -124,13 +128,17 @@ class VoiceAssistant : public Component {
std::unique_ptr<socket::Socket> socket_ = nullptr; std::unique_ptr<socket::Socket> socket_ = nullptr;
struct sockaddr_storage dest_addr_; struct sockaddr_storage dest_addr_;
Trigger<> *intent_end_trigger_ = new Trigger<>();
Trigger<> *intent_start_trigger_ = new Trigger<>();
Trigger<> *listening_trigger_ = new Trigger<>(); Trigger<> *listening_trigger_ = new Trigger<>();
Trigger<> *end_trigger_ = new Trigger<>();
Trigger<> *start_trigger_ = new Trigger<>(); Trigger<> *start_trigger_ = new Trigger<>();
Trigger<> *stt_vad_start_trigger_ = new Trigger<>();
Trigger<> *stt_vad_end_trigger_ = new Trigger<>();
Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger<> *wake_word_detected_trigger_ = new Trigger<>();
Trigger<std::string> *stt_end_trigger_ = new Trigger<std::string>(); Trigger<std::string> *stt_end_trigger_ = new Trigger<std::string>();
Trigger<std::string> *tts_start_trigger_ = new Trigger<std::string>();
Trigger<std::string> *tts_end_trigger_ = new Trigger<std::string>(); Trigger<std::string> *tts_end_trigger_ = new Trigger<std::string>();
Trigger<> *end_trigger_ = new Trigger<>(); Trigger<std::string> *tts_start_trigger_ = new Trigger<std::string>();
Trigger<std::string, std::string> *error_trigger_ = new Trigger<std::string, std::string>(); Trigger<std::string, std::string> *error_trigger_ = new Trigger<std::string, std::string>();
Trigger<> *client_connected_trigger_ = new Trigger<>(); Trigger<> *client_connected_trigger_ = new Trigger<>();

View File

@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile
esptool==4.6.2 esptool==4.6.2
click==8.1.7 click==8.1.7
esphome-dashboard==20231107.0 esphome-dashboard==20231107.0
aioesphomeapi==18.4.1 aioesphomeapi==18.5.3
zeroconf==0.127.0 zeroconf==0.127.0
# esp-idf requires this, but doesn't bundle it by default # esp-idf requires this, but doesn't bundle it by default