diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml new file mode 100644 index 0000000000..4c98a47ecd --- /dev/null +++ b/.github/actions/build-image/action.yaml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 14dbeee7b7..0e23db521a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,30 +63,20 @@ jobs: run: twine upload dist/* deploy-docker: - name: Build and publish ESPHome ${{ matrix.image.title}} + name: Build ESPHome ${{ matrix.platform }} if: github.repository == 'esphome/esphome' permissions: contents: read packages: write runs-on: ubuntu-latest - continue-on-error: ${{ matrix.image.title == 'lint' }} needs: [init] strategy: fail-fast: false matrix: - image: - - title: "ha-addon" - suffix: "hassio" - target: "hassio" - baseimg: "hassio" - - title: "docker" - suffix: "" - target: "docker" - baseimg: "docker" - - title: "lint" - suffix: "lint" - target: "lint" - baseimg: "docker" + platform: + - linux/amd64 + - linux/arm/v7 + - linux/arm64 steps: - uses: actions/checkout@v4.1.1 - name: Set up Python @@ -97,6 +87,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.0.0 - name: Set up QEMU + if: matrix.platform != 'linux/amd64' uses: docker/setup-qemu-action@v3.0.0 - name: Log in to docker hub @@ -111,34 +102,105 @@ jobs: username: ${{ github.actor }} 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 id: tags run: | - docker/generate_tags.py \ + output=$(docker/generate_tags.py \ --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 - uses: docker/build-push-action@v5.0.0 - with: - context: . - file: ./docker/Dockerfile - 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 }} + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \ + $(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *) deploy-ha-addon-repo: if: github.repository == 'esphome/esphome' && github.event_name == 'release' runs-on: ubuntu-latest - needs: [deploy-docker] + needs: [deploy-manifest] steps: - name: Trigger Workflow uses: actions/github-script@v6.4.1 diff --git a/docker/Dockerfile b/docker/Dockerfile index 7ca633a982..a892e1df38 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -68,7 +68,7 @@ ENV \ # See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian RUN \ 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 RUN \ diff --git a/docker/generate_tags.py b/docker/generate_tags.py index 71d0735526..3fc787d485 100755 --- a/docker/generate_tags.py +++ b/docker/generate_tags.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 import re -import os import argparse -import json CHANNEL_DEV = "dev" CHANNEL_BETA = "beta" CHANNEL_RELEASE = "release" +GHCR = "ghcr" +DOCKERHUB = "dockerhub" + parser = argparse.ArgumentParser() parser.add_argument( "--tag", @@ -21,21 +22,31 @@ parser.add_argument( required=True, 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(): args = parser.parse_args() # 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 - 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 - 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) channel = CHANNEL_RELEASE - else: - channel = CHANNEL_BETA tags_to_push = [args.tag] if channel == CHANNEL_DEV: @@ -53,15 +64,28 @@ def main(): suffix = f"-{args.suffix}" if args.suffix else "" - with open(os.environ["GITHUB_OUTPUT"], "w") as f: - print(f"channel={channel}", file=f) - print(f"image=esphome/esphome{suffix}", file=f) - full_tags = [] + image_name = f"esphome/esphome{suffix}" - for tag in tags_to_push: - full_tags += [f"ghcr.io/esphome/esphome{suffix}:{tag}"] - full_tags += [f"esphome/esphome{suffix}:{tag}"] - print(f"tags={','.join(full_tags)}", 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 = [] + + for tag in tags_to_push: + if GHCR in args.registry: + full_tags += [f"ghcr.io/{image_name}:{tag}"] + if DOCKERHUB in args.registry: + full_tags += [f"docker.io/{image_name}:{tag}"] + print(f"tags={','.join(full_tags)}") if __name__ == "__main__": diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 2c43eca70c..701848b1f1 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -18,9 +18,10 @@ from . import CONF_ENCRYPTION _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.""" conf = config["api"] + name = config["esphome"]["name"] port: int = int(conf[CONF_PORT]) password: str = conf[CONF_PASSWORD] noise_psk: str | None = None @@ -28,7 +29,6 @@ async def async_run_logs(config, address): noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] _LOGGER.info("Starting log output from %s using esphome API", address) aiozc = AsyncZeroconf() - cli = APIClient( address, port, @@ -48,7 +48,7 @@ async def async_run_logs(config, address): text = text.replace("\033", "\\033") 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: while True: await asyncio.sleep(60) diff --git a/esphome/components/my9231/my9231.cpp b/esphome/components/my9231/my9231.cpp index a97587b7be..c511591856 100644 --- a/esphome/components/my9231/my9231.cpp +++ b/esphome/components/my9231/my9231.cpp @@ -1,5 +1,6 @@ #include "my9231.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { 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; ESP_LOGV(TAG, " Command: 0x%02X", command); - this->init_chips_(command); + { + InterruptLock lock; + this->send_dcki_pulses_(32 * this->num_chips_); + this->init_chips_(command); + } ESP_LOGV(TAG, " Chips initialized."); } void MY9231OutputComponent::dump_config() { @@ -66,11 +71,14 @@ void MY9231OutputComponent::loop() { if (!this->update_) return; - for (auto pwm_amount : this->pwm_amounts_) { - this->write_word_(pwm_amount, this->bit_depth_); + { + InterruptLock lock; + for (auto pwm_amount : this->pwm_amounts_) { + this->write_word_(pwm_amount, this->bit_depth_); + } + // Send 8 DI pulses. After 8 falling edges, the duty data are store. + this->send_di_pulses_(8); } - // Send 8 DI pulses. After 8 falling edges, the duty data are store. - this->send_di_pulses_(8); this->update_ = false; } 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 // stored and after 16 falling edges the duty mode is activated. this->send_di_pulses_(16); + delayMicroseconds(12); } void MY9231OutputComponent::write_word_(uint16_t value, uint8_t bits) { 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); } } +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 esphome diff --git a/esphome/components/my9231/my9231.h b/esphome/components/my9231/my9231.h index a777dcc960..77c1259853 100644 --- a/esphome/components/my9231/my9231.h +++ b/esphome/components/my9231/my9231.h @@ -49,6 +49,7 @@ class MY9231OutputComponent : public Component { void init_chips_(uint8_t command); void write_word_(uint16_t value, uint8_t bits); void send_di_pulses_(uint8_t count); + void send_dcki_pulses_(uint8_t count); GPIOPin *pin_di_; GPIOPin *pin_dcki_; diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index f4abd845c8..55239dfcb8 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -33,6 +33,7 @@ MODELS = { "SH1106_96X16": SSD1306Model.SH1106_MODEL_96_16, "SH1106_64X48": SSD1306Model.SH1106_MODEL_64_48, "SH1107_128X64": SSD1306Model.SH1107_MODEL_128_64, + "SH1107_128X128": SSD1306Model.SH1107_MODEL_128_128, "SSD1305_128X32": SSD1306Model.SSD1305_MODEL_128_32, "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_FLIP_X, 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), - cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=-32, max=32), + # Offsets determine shifts of memory location to LCD rows/columns, + # 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, } ).extend(cv.polling_component_schema("1s")) diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 3cacd473d1..00b5c2d5a2 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -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_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() { 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) this->command(SSD1306_COMMAND_DISPLAY_OFF); - // Set oscillator frequency to 4'b1000 with no clock division (0xD5) - this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV); - // Oscillator frequency <= 4'b1000, no clock division - this->command(0x80); + // 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) + this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV); + // Oscillator frequency <= 4'b1000, no clock division + this->command(0x80); + } // Enable low power display mode for SSD1305 (0xD8) if (this->is_ssd1305_()) { @@ -60,11 +75,26 @@ void SSD1306::setup() { this->command(SSD1306_COMMAND_SET_DISPLAY_OFFSET_Y); this->command(0x00 + this->offset_y_); - // Set start line at line 0 (0x40) - this->command(SSD1306_COMMAND_SET_START_LINE | 0x00); + 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) + this->command(SSD1306_COMMAND_SET_START_LINE | 0x00); + } - // SSD1305 does not have charge pump - if (!this->is_ssd1305_()) { + if (this->is_ssd1305_()) { + // SSD1305 does not have charge pump + } 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) this->command(SSD1306_COMMAND_CHARGE_PUMP); if (this->external_vcc_) { @@ -76,34 +106,41 @@ void SSD1306::setup() { // Set addressing mode to horizontal (0x20) this->command(SSD1306_COMMAND_MEMORY_MODE); - this->command(0x00); - + if (!this->is_sh1107_()) { + // SH1107 memory mode is a 1 byte command + this->command(0x00); + } // X flip mode (0xA0, 0xA1) this->command(SSD1306_COMMAND_SEGRE_MAP | this->flip_x_); // Y flip mode (0xC0, 0xC8) this->command(SSD1306_COMMAND_COM_SCAN_INC | (this->flip_y_ << 3)); - // Set pin configuration (0xDA) - this->command(SSD1306_COMMAND_SET_COM_PINS); - switch (this->model_) { - case SSD1306_MODEL_128_32: - case SH1106_MODEL_128_32: - case SSD1306_MODEL_96_16: - case SH1106_MODEL_96_16: - this->command(0x02); - break; - case SSD1306_MODEL_128_64: - case SH1106_MODEL_128_64: - case SSD1306_MODEL_64_48: - case SSD1306_MODEL_64_32: - case SH1106_MODEL_64_48: - case SH1107_MODEL_128_64: - case SSD1305_MODEL_128_32: - case SSD1305_MODEL_128_64: - case SSD1306_MODEL_72_40: - this->command(0x12); - break; + if (!this->is_sh1107_()) { + // Set pin configuration (0xDA) + this->command(SSD1306_COMMAND_SET_COM_PINS); + switch (this->model_) { + case SSD1306_MODEL_128_32: + case SH1106_MODEL_128_32: + case SSD1306_MODEL_96_16: + case SH1106_MODEL_96_16: + this->command(0x02); + break; + case SSD1306_MODEL_128_64: + case SH1106_MODEL_128_64: + case SSD1306_MODEL_64_48: + case SSD1306_MODEL_64_32: + case SH1106_MODEL_64_48: + case SSD1305_MODEL_128_32: + case SSD1305_MODEL_128_64: + case SSD1306_MODEL_72_40: + this->command(0x12); + break; + case SH1107_MODEL_128_64: + case SH1107_MODEL_128_128: + // Not used, but prevents build warning + break; + } } // Pre-charge period (0xD9) @@ -118,6 +155,7 @@ void SSD1306::setup() { this->command(SSD1306_COMMAND_SET_VCOM_DETECT); switch (this->model_) { case SH1107_MODEL_128_64: + case SH1107_MODEL_128_128: this->command(0x35); break; case SSD1306_MODEL_72_40: @@ -149,7 +187,7 @@ void SSD1306::setup() { this->turn_on(); } void SSD1306::display() { - if (this->is_sh1106_()) { + if (this->is_sh1106_() || this->is_sh1107_()) { this->write_display_data(); return; } @@ -183,6 +221,7 @@ bool SSD1306::is_sh1106_() const { return this->model_ == SH1106_MODEL_96_16 || this->model_ == SH1106_MODEL_128_32 || 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 { 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() { switch (this->model_) { case SH1107_MODEL_128_64: + case SH1107_MODEL_128_128: return 128; case SSD1306_MODEL_128_32: case SSD1306_MODEL_64_32: @@ -254,6 +294,7 @@ int SSD1306::get_width_internal() { case SH1106_MODEL_128_64: case SSD1305_MODEL_128_32: case SSD1305_MODEL_128_64: + case SH1107_MODEL_128_128: return 128; case SSD1306_MODEL_96_16: case SH1106_MODEL_96_16: diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 4b0e9bb80e..34b76d284d 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -19,6 +19,7 @@ enum SSD1306Model { SH1106_MODEL_96_16, SH1106_MODEL_64_48, SH1107_MODEL_128_64, + SH1107_MODEL_128_128, SSD1305_MODEL_128_32, SSD1305_MODEL_128_64, }; @@ -58,6 +59,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void init_reset_(); bool is_sh1106_() const; + bool is_sh1107_() const; bool is_ssd1305_() const; void draw_absolute_pixel_internal(int x, int y, Color color) override; diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index 96734eb618..ed7cf102ee 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -38,13 +38,19 @@ void I2CSSD1306::dump_config() { } void I2CSSD1306::command(uint8_t value) { this->write_byte(0x00, value); } void HOT I2CSSD1306::write_display_data() { - if (this->is_sh1106_()) { + if (this->is_sh1106_() || this->is_sh1107_()) { uint32_t i = 0; for (uint8_t page = 0; page < (uint8_t) this->get_height_internal() / 8; page++) { this->command(0xB0 + page); // row - this->command(0x02); // lower column - this->command(0x10); // higher 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 for (uint8_t x = 0; x < (uint8_t) this->get_width_internal() / 16; x++) { uint8_t data[16]; for (uint8_t &j : data) diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index 7f025d77cd..0a0debfd65 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -36,10 +36,14 @@ void SPISSD1306::command(uint8_t value) { this->disable(); } 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++) { this->command(0xB0 + y); - this->command(0x02); + if (this->is_sh1106_()) { + this->command(0x02); + } else { + this->command(0x00); + } this->command(0x10); this->dc_pin_->digital_write(true); for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x++) { diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 3270b9f370..5715604605 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -18,20 +18,25 @@ DEPENDENCIES = ["api", "microphone"] 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_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_VAD_THRESHOLD = "vad_threshold" -CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level" CONF_AUTO_GAIN = "auto_gain" +CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level" CONF_VOLUME_MULTIPLIER = "volume_multiplier" @@ -88,6 +93,18 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( 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), ) @@ -177,6 +194,34 @@ async def to_code(config): 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") diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index fc5dd6e4e4..7ebbe762b3 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -31,7 +31,7 @@ void VoiceAssistant::setup() { this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (socket_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket."); + ESP_LOGW(TAG, "Could not create socket"); this->mark_failed(); return; } @@ -69,7 +69,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator speaker_allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE); if (this->speaker_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate speaker buffer."); + ESP_LOGW(TAG, "Could not allocate speaker buffer"); this->mark_failed(); return; } @@ -79,7 +79,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE); if (this->input_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate input buffer."); + ESP_LOGW(TAG, "Could not allocate input buffer"); this->mark_failed(); return; } @@ -89,7 +89,7 @@ void VoiceAssistant::setup() { this->ring_buffer_ = rb_create(BUFFER_SIZE, sizeof(int16_t)); if (this->ring_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate ring buffer."); + ESP_LOGW(TAG, "Could not allocate ring buffer"); this->mark_failed(); return; } @@ -98,7 +98,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator send_allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE); if (send_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate send buffer."); + ESP_LOGW(TAG, "Could not allocate send buffer"); this->mark_failed(); return; } @@ -221,8 +221,8 @@ void VoiceAssistant::loop() { msg.audio_settings = audio_settings; if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) { - ESP_LOGW(TAG, "Could not request start."); - this->error_trigger_->trigger("not-connected", "Could not request start."); + ESP_LOGW(TAG, "Could not request start"); + this->error_trigger_->trigger("not-connected", "Could not request start"); this->continuous_ = false; this->set_state_(State::IDLE, State::IDLE); break; @@ -280,7 +280,7 @@ void VoiceAssistant::loop() { this->speaker_buffer_size_ += len; } } else { - ESP_LOGW(TAG, "Receive buffer full."); + ESP_LOGW(TAG, "Receive buffer full"); } if (this->speaker_buffer_size_ > 0) { 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->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); }); } else { - ESP_LOGW(TAG, "Speaker buffer full."); + ESP_LOGW(TAG, "Speaker buffer full"); } } if (this->wait_for_stream_end_) { @@ -513,7 +513,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { break; } case api::enums::VOICE_ASSISTANT_STT_START: - ESP_LOGD(TAG, "STT Started"); + ESP_LOGD(TAG, "STT started"); this->listening_trigger_->trigger(); break; case api::enums::VOICE_ASSISTANT_STT_END: { @@ -525,19 +525,24 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (text.empty()) { - ESP_LOGW(TAG, "No text in STT_END event."); + ESP_LOGW(TAG, "No text in STT_END event"); return; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->stt_end_trigger_->trigger(text); 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: { for (auto arg : msg.data) { if (arg.name == "conversation_id") { this->conversation_id_ = std::move(arg.value); } } + this->intent_end_trigger_->trigger(); break; } case api::enums::VOICE_ASSISTANT_TTS_START: { @@ -548,7 +553,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (text.empty()) { - ESP_LOGW(TAG, "No text in TTS_START event."); + ESP_LOGW(TAG, "No text in TTS_START event"); return; } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); @@ -566,7 +571,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (url.empty()) { - ESP_LOGW(TAG, "No url in TTS_END event."); + ESP_LOGW(TAG, "No url in TTS_END event"); return; } 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); 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: ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type); break; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index a265522bca..a985bc4678 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -100,13 +100,17 @@ class VoiceAssistant : public Component { 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; } + 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_end_trigger() const { return this->end_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_stt_end_trigger() const { return this->stt_end_trigger_; } - Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } - Trigger<> *get_end_trigger() const { return this->end_trigger_; } + Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } Trigger *get_error_trigger() const { return this->error_trigger_; } Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; } @@ -124,13 +128,17 @@ class VoiceAssistant : public Component { std::unique_ptr socket_ = nullptr; struct sockaddr_storage dest_addr_; + Trigger<> *intent_end_trigger_ = new Trigger<>(); + Trigger<> *intent_start_trigger_ = new Trigger<>(); Trigger<> *listening_trigger_ = new Trigger<>(); + Trigger<> *end_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 *stt_end_trigger_ = new Trigger(); - Trigger *tts_start_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); - Trigger<> *end_trigger_ = new Trigger<>(); + Trigger *tts_start_trigger_ = new Trigger(); Trigger *error_trigger_ = new Trigger(); Trigger<> *client_connected_trigger_ = new Trigger<>(); diff --git a/requirements.txt b/requirements.txt index 9afe7064c2..1866d33ab2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.4.1 +aioesphomeapi==18.5.3 zeroconf==0.127.0 # esp-idf requires this, but doesn't bundle it by default