Merge pull request #5923 from esphome/bump-2023.12.0b1

2023.12.0b1
This commit is contained in:
Jesse Hills 2023-12-14 09:00:48 +09:00 committed by GitHub
commit cc5611bd89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
347 changed files with 16521 additions and 4712 deletions

View file

@ -37,6 +37,7 @@
"!secret scalar", "!secret scalar",
"!lambda scalar", "!lambda scalar",
"!extend scalar", "!extend scalar",
"!remove scalar",
"!include_dir_named scalar", "!include_dir_named scalar",
"!include_dir_list scalar", "!include_dir_list scalar",
"!include_dir_merge_list scalar", "!include_dir_merge_list scalar",

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

@ -17,7 +17,7 @@ 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@v4.7.0 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment

View file

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

View file

@ -40,7 +40,7 @@ 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@v4.7.1 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment

View file

@ -18,7 +18,7 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v4.0.1 - uses: dessant/lock-threads@v5.0.1
with: with:
pr-inactive-days: "1" pr-inactive-days: "1"
pr-lock-reason: "" pr-lock-reason: ""

View file

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check for needs-docs label - name: Check for needs-docs label
uses: actions/github-script@v6.4.1 uses: actions/github-script@v7.0.1
with: with:
script: | script: |
const { data: labels } = await github.rest.issues.listLabelsOnIssue({ const { data: labels } = await github.rest.issues.listLabelsOnIssue({

View file

@ -45,7 +45,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4.7.1 uses: actions/setup-python@v5.0.0
with: with:
python-version: "3.x" python-version: "3.x"
- name: Set up python environment - name: Set up python environment
@ -63,40 +63,31 @@ 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
uses: actions/setup-python@v4.7.1 uses: actions/setup-python@v5.0.0
with: with:
python-version: "3.9" python-version: "3.9"
- 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,37 +102,108 @@ 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@v7.0.1
with: with:
github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
script: | script: |

View file

@ -18,7 +18,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v8.0.0 - uses: actions/stale@v9.0.0
with: with:
days-before-pr-stale: 90 days-before-pr-stale: 90
days-before-pr-close: 7 days-before-pr-close: 7
@ -38,7 +38,7 @@ jobs:
close-issues: close-issues:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v8.0.0 - uses: actions/stale@v9.0.0
with: with:
days-before-pr-stale: -1 days-before-pr-stale: -1
days-before-pr-close: -1 days-before-pr-close: -1

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@v4.7.1 uses: actions/setup-python@v5.0.0
with: with:
python-version: 3.11 python-version: 3.11

View file

@ -19,4 +19,4 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Run yamllint - name: Run yamllint
uses: frenck/action-yamllint@v1.4.1 uses: frenck/action-yamllint@v1.4.2

View file

@ -3,7 +3,7 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.10.1 rev: 23.12.0
hooks: hooks:
- id: black - id: black
args: args:

View file

@ -12,6 +12,7 @@ esphome/core/* @esphome/core
# Integrations # Integrations
esphome/components/a01nyub/* @MrSuicideParrot esphome/components/a01nyub/* @MrSuicideParrot
esphome/components/a02yyuw/* @TH-Braemer
esphome/components/absolute_humidity/* @DAVe3283 esphome/components/absolute_humidity/* @DAVe3283
esphome/components/ac_dimmer/* @glmnet esphome/components/ac_dimmer/* @glmnet
esphome/components/adc/* @esphome/core esphome/components/adc/* @esphome/core
@ -88,8 +89,9 @@ esphome/components/ds1307/* @badbadc0ffee
esphome/components/dsmr/* @glmnet @zuidwijk esphome/components/dsmr/* @glmnet @zuidwijk
esphome/components/duty_time/* @dudanov esphome/components/duty_time/* @dudanov
esphome/components/ee895/* @Stock-M esphome/components/ee895/* @Stock-M
esphome/components/ektf2232/* @jesserockz esphome/components/ektf2232/touchscreen/* @jesserockz
esphome/components/emc2101/* @ellull esphome/components/emc2101/* @ellull
esphome/components/ens160/* @vincentscode
esphome/components/ens210/* @itn3rd77 esphome/components/ens210/* @itn3rd77
esphome/components/esp32/* @esphome/core esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @Rapsssito @jesserockz esphome/components/esp32_ble/* @Rapsssito @jesserockz
@ -109,19 +111,24 @@ esphome/components/fastled_base/* @OttoWinter
esphome/components/feedback/* @ianchi esphome/components/feedback/* @ianchi
esphome/components/fingerprint_grow/* @OnFreund @loongyh esphome/components/fingerprint_grow/* @OnFreund @loongyh
esphome/components/fs3000/* @kahrendt esphome/components/fs3000/* @kahrendt
esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio
esphome/components/gcja5/* @gcormier esphome/components/gcja5/* @gcormier
esphome/components/globals/* @esphome/core esphome/components/globals/* @esphome/core
esphome/components/gp8403/* @jesserockz esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core esphome/components/gpio/* @esphome/core
esphome/components/gps/* @coogle esphome/components/gps/* @coogle
esphome/components/graph/* @synco esphome/components/graph/* @synco
esphome/components/graphical_display_menu/* @MrMDavidson
esphome/components/gree/* @orestismers esphome/components/gree/* @orestismers
esphome/components/grove_tb6612fng/* @max246 esphome/components/grove_tb6612fng/* @max246
esphome/components/growatt_solar/* @leeuwte esphome/components/growatt_solar/* @leeuwte
esphome/components/gt911/* @clydebarrow @jesserockz
esphome/components/haier/* @paveldn esphome/components/haier/* @paveldn
esphome/components/havells_solar/* @sourabhjaiswal esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/light/* @DotNetDann
esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hm3301/* @freekode esphome/components/hm3301/* @freekode
@ -233,11 +240,17 @@ esphome/components/pmwcs3/* @SeByDocKy
esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532/* @OttoWinter @jesserockz
esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz
esphome/components/pn532_spi/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz
esphome/components/pn7150/* @jesserockz @kbx81
esphome/components/pn7150_i2c/* @jesserockz @kbx81
esphome/components/pn7160/* @jesserockz @kbx81
esphome/components/pn7160_i2c/* @jesserockz @kbx81
esphome/components/pn7160_spi/* @jesserockz @kbx81
esphome/components/power_supply/* @esphome/core esphome/components/power_supply/* @esphome/core
esphome/components/preferences/* @esphome/core esphome/components/preferences/* @esphome/core
esphome/components/psram/* @esphome/core esphome/components/psram/* @esphome/core
esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter
esphome/components/pvvx_mithermometer/* @pasiz esphome/components/pvvx_mithermometer/* @pasiz
esphome/components/pylontech/* @functionpointer
esphome/components/qmp6988/* @andrewpc esphome/components/qmp6988/* @andrewpc
esphome/components/qr_code/* @wjtje esphome/components/qr_code/* @wjtje
esphome/components/qwiic_pir/* @kahrendt esphome/components/qwiic_pir/* @kahrendt
@ -326,7 +339,7 @@ esphome/components/tmp1075/* @sybrenstuvel
esphome/components/tmp117/* @Azimath esphome/components/tmp117/* @Azimath
esphome/components/tof10120/* @wstrzalka esphome/components/tof10120/* @wstrzalka
esphome/components/toshiba/* @kbx81 esphome/components/toshiba/* @kbx81
esphome/components/touchscreen/* @jesserockz esphome/components/touchscreen/* @jesserockz @nielsnl68
esphome/components/tsl2591/* @wjcarpenter esphome/components/tsl2591/* @wjcarpenter
esphome/components/tt21100/* @kroimon esphome/components/tt21100/* @kroimon
esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/binary_sensor/* @jesserockz
@ -359,6 +372,6 @@ esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xiaomi_rtcgq02lm/* @jesserockz esphome/components/xiaomi_rtcgq02lm/* @jesserockz
esphome/components/xl9535/* @mreditor97 esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/* @nielsnl68 @numo68 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zio_ultrasonic/* @kahrendt esphome/components/zio_ultrasonic/* @kahrendt

View file

@ -10,5 +10,3 @@ Things to note when contributing:
for more information. for more information.
- Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files - Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files
which checks if your new feature compiles correctly. which checks if your new feature compiles correctly.
- Sometimes I will let pull requests linger because I'm not 100% sure about them. Please feel free to ping
me after some time.

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

@ -389,6 +389,7 @@ def command_config(args, config):
output = re.sub( output = re.sub(
r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output
) )
if not CORE.quiet:
safe_print(output) safe_print(output)
_LOGGER.info("Configuration is valid!") _LOGGER.info("Configuration is valid!")
return 0 return 0
@ -514,7 +515,7 @@ def command_clean(args, config):
def command_dashboard(args): def command_dashboard(args):
from esphome.dashboard import dashboard from esphome.dashboard import dashboard
return dashboard.start_web_server(args) return dashboard.start_dashboard(args)
def command_update_all(args): def command_update_all(args):

View file

@ -8,31 +8,23 @@ namespace esphome {
namespace a01nyub { namespace a01nyub {
static const char *const TAG = "a01nyub.sensor"; static const char *const TAG = "a01nyub.sensor";
static const uint8_t MAX_DATA_LENGTH_BYTES = 4;
void A01nyubComponent::loop() { void A01nyubComponent::loop() {
uint8_t data; uint8_t data;
while (this->available() > 0) { while (this->available() > 0) {
if (this->read_byte(&data)) { this->read_byte(&data);
if (this->buffer_.empty() && (data != 0xff))
continue;
buffer_.push_back(data); buffer_.push_back(data);
if (this->buffer_.size() == 4)
this->check_buffer_(); this->check_buffer_();
} }
} }
}
void A01nyubComponent::check_buffer_() { void A01nyubComponent::check_buffer_() {
if (this->buffer_.size() >= MAX_DATA_LENGTH_BYTES) { uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2];
size_t i; if (this->buffer_[3] == checksum) {
for (i = 0; i < this->buffer_.size(); i++) { float distance = (this->buffer_[1] << 8) + this->buffer_[2];
// Look for the first packet
if (this->buffer_[i] == 0xFF) {
if (i + 1 + 3 < this->buffer_.size()) { // Packet is not complete
return; // Wait for completion
}
uint8_t checksum = (this->buffer_[i] + this->buffer_[i + 1] + this->buffer_[i + 2]) & 0xFF;
if (this->buffer_[i + 3] == checksum) {
float distance = (this->buffer_[i + 1] << 8) + this->buffer_[i + 2];
if (distance > 280) { if (distance > 280) {
float meters = distance / 1000.0; float meters = distance / 1000.0;
ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters); ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters);
@ -40,18 +32,13 @@ void A01nyubComponent::check_buffer_() {
} else { } else {
ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
} }
} } else {
break; ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]);
}
} }
this->buffer_.clear(); this->buffer_.clear();
} }
}
void A01nyubComponent::dump_config() { void A01nyubComponent::dump_config() { LOG_SENSOR("", "A01nyub Sensor", this); }
ESP_LOGCONFIG(TAG, "A01nyub Sensor:");
LOG_SENSOR(" ", "Distance", this);
}
} // namespace a01nyub } // namespace a01nyub
} // namespace esphome } // namespace esphome

View file

@ -0,0 +1 @@
CODEOWNERS = ["@TH-Braemer"]

View file

@ -0,0 +1,43 @@
// Datasheet https://wiki.dfrobot.com/_A02YYUW_Waterproof_Ultrasonic_Sensor_SKU_SEN0311
#include "a02yyuw.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace a02yyuw {
static const char *const TAG = "a02yyuw.sensor";
void A02yyuwComponent::loop() {
uint8_t data;
while (this->available() > 0) {
this->read_byte(&data);
if (this->buffer_.empty() && (data != 0xff))
continue;
buffer_.push_back(data);
if (this->buffer_.size() == 4)
this->check_buffer_();
}
}
void A02yyuwComponent::check_buffer_() {
uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2];
if (this->buffer_[3] == checksum) {
float distance = (this->buffer_[1] << 8) + this->buffer_[2];
if (distance > 30) {
ESP_LOGV(TAG, "Distance from sensor: %f mm", distance);
this->publish_state(distance);
} else {
ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
}
} else {
ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]);
}
this->buffer_.clear();
}
void A02yyuwComponent::dump_config() { LOG_SENSOR("", "A02yyuw Sensor", this); }
} // namespace a02yyuw
} // namespace esphome

View file

@ -0,0 +1,27 @@
#pragma once
#include <vector>
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace a02yyuw {
class A02yyuwComponent : public sensor::Sensor, public Component, public uart::UARTDevice {
public:
// Nothing really public.
// ========== INTERNAL METHODS ==========
void loop() override;
void dump_config() override;
protected:
void check_buffer_();
std::vector<uint8_t> buffer_;
};
} // namespace a02yyuw
} // namespace esphome

View file

@ -0,0 +1,41 @@
import esphome.codegen as cg
from esphome.components import sensor, uart
from esphome.const import (
STATE_CLASS_MEASUREMENT,
ICON_ARROW_EXPAND_VERTICAL,
DEVICE_CLASS_DISTANCE,
)
CODEOWNERS = ["@TH-Braemer"]
DEPENDENCIES = ["uart"]
UNIT_MILLIMETERS = "mm"
a02yyuw_ns = cg.esphome_ns.namespace("a02yyuw")
A02yyuwComponent = a02yyuw_ns.class_(
"A02yyuwComponent", sensor.Sensor, cg.Component, uart.UARTDevice
)
CONFIG_SCHEMA = sensor.sensor_schema(
A02yyuwComponent,
unit_of_measurement=UNIT_MILLIMETERS,
icon=ICON_ARROW_EXPAND_VERTICAL,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
device_class=DEVICE_CLASS_DISTANCE,
).extend(uart.UART_DEVICE_SCHEMA)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"a02yyuw",
baud_rate=9600,
require_tx=False,
require_rx=True,
data_bits=8,
parity=None,
stop_bits=1,
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)

View file

@ -1,7 +1,7 @@
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 import pins
from esphome.const import CONF_ANALOG, CONF_INPUT from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER
from esphome.core import CORE from esphome.core import CORE
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import get_esp32_variant
@ -152,7 +152,8 @@ def validate_adc_pin(value):
return cv.only_on_rp2040("TEMPERATURE") return cv.only_on_rp2040("TEMPERATURE")
if CORE.is_esp32: if CORE.is_esp32:
value = pins.internal_gpio_input_pin_number(value) conf = pins.internal_gpio_input_pin_schema(value)
value = conf[CONF_NUMBER]
variant = get_esp32_variant() variant = get_esp32_variant()
if ( if (
variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL
@ -166,24 +167,23 @@ def validate_adc_pin(value):
): ):
raise cv.Invalid(f"{variant} doesn't support ADC on this pin") raise cv.Invalid(f"{variant} doesn't support ADC on this pin")
return pins.internal_gpio_input_pin_schema(value) return conf
if CORE.is_esp8266: if CORE.is_esp8266:
value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( conf = pins.gpio_pin_schema(
value
)
if value != 17: # A0
raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC")
return pins.gpio_pin_schema(
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True {CONF_ANALOG: True, CONF_INPUT: True}, internal=True
)(value) )(value)
if conf[CONF_NUMBER] != 17: # A0
raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC")
return conf
if CORE.is_rp2040: if CORE.is_rp2040:
value = pins.internal_gpio_input_pin_number(value) conf = pins.internal_gpio_input_pin_schema(value)
if value not in (26, 27, 28, 29): number = conf[CONF_NUMBER]
if number not in (26, 27, 28, 29):
raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC") raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC")
return pins.internal_gpio_input_pin_schema(value) return conf
if CORE.is_libretiny: if CORE.is_libretiny:
return pins.gpio_pin_schema( return pins.gpio_pin_schema(

View file

@ -10,7 +10,7 @@
namespace esphome { namespace esphome {
namespace addressable_light { namespace addressable_light {
class AddressableLightDisplay : public display::DisplayBuffer, public PollingComponent { class AddressableLightDisplay : public display::DisplayBuffer {
public: public:
light::AddressableLight *get_light() const { return this->light_; } light::AddressableLight *get_light() const { return this->light_; }

View file

@ -45,7 +45,6 @@ async def to_code(config):
cg.add(var.set_height(config[CONF_HEIGHT])) cg.add(var.set_height(config[CONF_HEIGHT]))
cg.add(var.set_light(wrapped_light)) cg.add(var.set_light(wrapped_light))
await cg.register_component(var, config)
await display.register_display(var, config) await display.register_display(var, config)
if pixel_mapper := config.get(CONF_PIXEL_MAPPER): if pixel_mapper := config.get(CONF_PIXEL_MAPPER):

View file

@ -21,37 +21,50 @@ namespace esphome {
namespace aht10 { namespace aht10 {
static const char *const TAG = "aht10"; static const char *const TAG = "aht10";
static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1}; static const size_t SIZE_CALIBRATE_CMD = 3;
static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1, 0x08, 0x00};
static const uint8_t AHT20_CALIBRATE_CMD[] = {0xBE, 0x08, 0x00};
static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00};
static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement
static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms
static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms
static const uint8_t AHT10_CAL_ATTEMPTS = 10;
static const uint8_t AHT10_STATUS_BUSY = 0x80;
void AHT10Component::setup() { void AHT10Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up AHT10..."); const uint8_t *calibrate_cmd;
switch (this->variant_) {
case AHT10Variant::AHT20:
calibrate_cmd = AHT20_CALIBRATE_CMD;
ESP_LOGCONFIG(TAG, "Setting up AHT20");
break;
case AHT10Variant::AHT10:
default:
calibrate_cmd = AHT10_CALIBRATE_CMD;
ESP_LOGCONFIG(TAG, "Setting up AHT10");
}
if (!this->write_bytes(0, AHT10_CALIBRATE_CMD, sizeof(AHT10_CALIBRATE_CMD))) { if (this->write(calibrate_cmd, SIZE_CALIBRATE_CMD) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!"); ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->mark_failed(); this->mark_failed();
return; return;
} }
uint8_t data = 0; uint8_t data = AHT10_STATUS_BUSY;
if (this->write(&data, 1) != i2c::ERROR_OK) { int cal_attempts = 0;
ESP_LOGD(TAG, "Communication with AHT10 failed!"); while (data & AHT10_STATUS_BUSY) {
this->mark_failed();
return;
}
delay(AHT10_DEFAULT_DELAY); delay(AHT10_DEFAULT_DELAY);
if (this->read(&data, 1) != i2c::ERROR_OK) { if (this->read(&data, 1) != i2c::ERROR_OK) {
ESP_LOGD(TAG, "Communication with AHT10 failed!"); ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->mark_failed(); this->mark_failed();
return; return;
} }
if (this->read(&data, 1) != i2c::ERROR_OK) { ++cal_attempts;
ESP_LOGD(TAG, "Communication with AHT10 failed!"); if (cal_attempts > AHT10_CAL_ATTEMPTS) {
ESP_LOGE(TAG, "AHT10 calibration timed out!");
this->mark_failed(); this->mark_failed();
return; return;
} }
}
if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED
ESP_LOGE(TAG, "AHT10 calibration failed!"); ESP_LOGE(TAG, "AHT10 calibration failed!");
this->mark_failed(); this->mark_failed();
@ -62,7 +75,7 @@ void AHT10Component::setup() {
} }
void AHT10Component::update() { void AHT10Component::update() {
if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!"); ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->status_set_warning(); this->status_set_warning();
return; return;
@ -89,7 +102,7 @@ void AHT10Component::update() {
break; break;
} else { } else {
ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying..."); ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying...");
if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!"); ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->status_set_warning(); this->status_set_warning();
return; return;

View file

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <utility>
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/i2c/i2c.h"
@ -7,12 +9,15 @@
namespace esphome { namespace esphome {
namespace aht10 { namespace aht10 {
enum AHT10Variant { AHT10, AHT20 };
class AHT10Component : public PollingComponent, public i2c::I2CDevice { class AHT10Component : public PollingComponent, public i2c::I2CDevice {
public: public:
void setup() override; void setup() override;
void update() override; void update() override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override; float get_setup_priority() const override;
void set_variant(AHT10Variant variant) { this->variant_ = variant; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
@ -20,6 +25,7 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice {
protected: protected:
sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr};
AHT10Variant variant_{};
}; };
} // namespace aht10 } // namespace aht10

View file

@ -10,6 +10,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS, UNIT_CELSIUS,
UNIT_PERCENT, UNIT_PERCENT,
CONF_VARIANT,
) )
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@ -17,6 +18,12 @@ DEPENDENCIES = ["i2c"]
aht10_ns = cg.esphome_ns.namespace("aht10") aht10_ns = cg.esphome_ns.namespace("aht10")
AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice) AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice)
AHT10Variant = aht10_ns.enum("AHT10Variant")
AHT10_VARIANTS = {
"AHT10": AHT10Variant.AHT10,
"AHT20": AHT10Variant.AHT20,
}
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.Schema( cv.Schema(
{ {
@ -33,6 +40,9 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_HUMIDITY, device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional(CONF_VARIANT, default="AHT10"): cv.enum(
AHT10_VARIANTS, upper=True
),
} }
) )
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
@ -44,6 +54,7 @@ async def to_code(config):
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)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
cg.add(var.set_variant(config[CONF_VARIANT]))
if temperature := config.get(CONF_TEMPERATURE): if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature) sens = await sensor.new_sensor(temperature)

View file

@ -365,6 +365,7 @@ message ListEntitiesFanResponse {
bool disabled_by_default = 9; bool disabled_by_default = 9;
string icon = 10; string icon = 10;
EntityCategory entity_category = 11; EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12;
} }
enum FanSpeed { enum FanSpeed {
FAN_SPEED_LOW = 0; FAN_SPEED_LOW = 0;
@ -387,6 +388,7 @@ message FanStateResponse {
FanSpeed speed = 4 [deprecated = true]; FanSpeed speed = 4 [deprecated = true];
FanDirection direction = 5; FanDirection direction = 5;
int32 speed_level = 6; int32 speed_level = 6;
string preset_mode = 7;
} }
message FanCommandRequest { message FanCommandRequest {
option (id) = 31; option (id) = 31;
@ -405,6 +407,8 @@ message FanCommandRequest {
FanDirection direction = 9; FanDirection direction = 9;
bool has_speed_level = 10; bool has_speed_level = 10;
int32 speed_level = 11; int32 speed_level = 11;
bool has_preset_mode = 12;
string preset_mode = 13;
} }
// ==================== LIGHT ==================== // ==================== LIGHT ====================
@ -855,6 +859,10 @@ message ListEntitiesClimateResponse {
string icon = 19; string icon = 19;
EntityCategory entity_category = 20; EntityCategory entity_category = 20;
float visual_current_temperature_step = 21; float visual_current_temperature_step = 21;
bool supports_current_humidity = 22;
bool supports_target_humidity = 23;
float visual_min_humidity = 24;
float visual_max_humidity = 25;
} }
message ClimateStateResponse { message ClimateStateResponse {
option (id) = 47; option (id) = 47;
@ -875,6 +883,8 @@ message ClimateStateResponse {
string custom_fan_mode = 11; string custom_fan_mode = 11;
ClimatePreset preset = 12; ClimatePreset preset = 12;
string custom_preset = 13; string custom_preset = 13;
float current_humidity = 14;
float target_humidity = 15;
} }
message ClimateCommandRequest { message ClimateCommandRequest {
option (id) = 48; option (id) = 48;
@ -903,6 +913,8 @@ message ClimateCommandRequest {
ClimatePreset preset = 19; ClimatePreset preset = 19;
bool has_custom_preset = 20; bool has_custom_preset = 20;
string custom_preset = 21; string custom_preset = 21;
bool has_target_humidity = 22;
float target_humidity = 23;
} }
// ==================== NUMBER ==================== // ==================== NUMBER ====================

View file

@ -293,6 +293,8 @@ bool APIConnection::send_fan_state(fan::Fan *fan) {
} }
if (traits.supports_direction()) if (traits.supports_direction())
resp.direction = static_cast<enums::FanDirection>(fan->direction); resp.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes())
resp.preset_mode = fan->preset_mode;
return this->send_fan_state_response(resp); return this->send_fan_state_response(resp);
} }
bool APIConnection::send_fan_info(fan::Fan *fan) { bool APIConnection::send_fan_info(fan::Fan *fan) {
@ -307,6 +309,8 @@ bool APIConnection::send_fan_info(fan::Fan *fan) {
msg.supports_speed = traits.supports_speed(); msg.supports_speed = traits.supports_speed();
msg.supports_direction = traits.supports_direction(); msg.supports_direction = traits.supports_direction();
msg.supported_speed_count = traits.supported_speed_count(); msg.supported_speed_count = traits.supported_speed_count();
for (auto const &preset : traits.supported_preset_modes())
msg.supported_preset_modes.push_back(preset);
msg.disabled_by_default = fan->is_disabled_by_default(); msg.disabled_by_default = fan->is_disabled_by_default();
msg.icon = fan->get_icon(); msg.icon = fan->get_icon();
msg.entity_category = static_cast<enums::EntityCategory>(fan->get_entity_category()); msg.entity_category = static_cast<enums::EntityCategory>(fan->get_entity_category());
@ -328,6 +332,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
} }
if (msg.has_direction) if (msg.has_direction)
call.set_direction(static_cast<fan::FanDirection>(msg.direction)); call.set_direction(static_cast<fan::FanDirection>(msg.direction));
if (msg.has_preset_mode)
call.set_preset_mode(msg.preset_mode);
call.perform(); call.perform();
} }
#endif #endif
@ -554,6 +560,10 @@ bool APIConnection::send_climate_state(climate::Climate *climate) {
resp.custom_preset = climate->custom_preset.value(); resp.custom_preset = climate->custom_preset.value();
if (traits.get_supports_swing_modes()) if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode); resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
if (traits.get_supports_current_humidity())
resp.current_humidity = climate->current_humidity;
if (traits.get_supports_target_humidity())
resp.target_humidity = climate->target_humidity;
return this->send_climate_state_response(resp); return this->send_climate_state_response(resp);
} }
bool APIConnection::send_climate_info(climate::Climate *climate) { bool APIConnection::send_climate_info(climate::Climate *climate) {
@ -570,7 +580,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
msg.entity_category = static_cast<enums::EntityCategory>(climate->get_entity_category()); msg.entity_category = static_cast<enums::EntityCategory>(climate->get_entity_category());
msg.supports_current_temperature = traits.get_supports_current_temperature(); msg.supports_current_temperature = traits.get_supports_current_temperature();
msg.supports_current_humidity = traits.get_supports_current_humidity();
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
msg.supports_target_humidity = traits.get_supports_target_humidity();
for (auto mode : traits.get_supported_modes()) for (auto mode : traits.get_supported_modes())
msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode)); msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode));
@ -579,6 +591,8 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature();
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY);
msg.supports_action = traits.get_supports_action(); msg.supports_action = traits.get_supports_action();
@ -609,6 +623,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
call.set_target_temperature_low(msg.target_temperature_low); call.set_target_temperature_low(msg.target_temperature_low);
if (msg.has_target_temperature_high) if (msg.has_target_temperature_high)
call.set_target_temperature_high(msg.target_temperature_high); call.set_target_temperature_high(msg.target_temperature_high);
if (msg.has_target_humidity)
call.set_target_humidity(msg.target_humidity);
if (msg.has_fan_mode) if (msg.has_fan_mode)
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode)); call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
if (msg.has_custom_fan_mode) if (msg.has_custom_fan_mode)

View file

@ -1375,6 +1375,10 @@ bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimi
this->icon = value.as_string(); this->icon = value.as_string();
return true; return true;
} }
case 12: {
this->supported_preset_modes.push_back(value.as_string());
return true;
}
default: default:
return false; return false;
} }
@ -1401,6 +1405,9 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(9, this->disabled_by_default); buffer.encode_bool(9, this->disabled_by_default);
buffer.encode_string(10, this->icon); buffer.encode_string(10, this->icon);
buffer.encode_enum<enums::EntityCategory>(11, this->entity_category); buffer.encode_enum<enums::EntityCategory>(11, this->entity_category);
for (auto &it : this->supported_preset_modes) {
buffer.encode_string(12, it, true);
}
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesFanResponse::dump_to(std::string &out) const { void ListEntitiesFanResponse::dump_to(std::string &out) const {
@ -1451,6 +1458,12 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const {
out.append(" entity_category: "); out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n"); out.append("\n");
for (const auto &it : this->supported_preset_modes) {
out.append(" supported_preset_modes: ");
out.append("'").append(it).append("'");
out.append("\n");
}
out.append("}"); out.append("}");
} }
#endif #endif
@ -1480,6 +1493,16 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
return false; return false;
} }
} }
bool FanStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 7: {
this->preset_mode = value.as_string();
return true;
}
default:
return false;
}
}
bool FanStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { bool FanStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) { switch (field_id) {
case 1: { case 1: {
@ -1497,6 +1520,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_enum<enums::FanSpeed>(4, this->speed); buffer.encode_enum<enums::FanSpeed>(4, this->speed);
buffer.encode_enum<enums::FanDirection>(5, this->direction); buffer.encode_enum<enums::FanDirection>(5, this->direction);
buffer.encode_int32(6, this->speed_level); buffer.encode_int32(6, this->speed_level);
buffer.encode_string(7, this->preset_mode);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void FanStateResponse::dump_to(std::string &out) const { void FanStateResponse::dump_to(std::string &out) const {
@ -1527,6 +1551,10 @@ void FanStateResponse::dump_to(std::string &out) const {
sprintf(buffer, "%" PRId32, this->speed_level); sprintf(buffer, "%" PRId32, this->speed_level);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append(" preset_mode: ");
out.append("'").append(this->preset_mode).append("'");
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -1572,6 +1600,20 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
this->speed_level = value.as_int32(); this->speed_level = value.as_int32();
return true; return true;
} }
case 12: {
this->has_preset_mode = value.as_bool();
return true;
}
default:
return false;
}
}
bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 13: {
this->preset_mode = value.as_string();
return true;
}
default: default:
return false; return false;
} }
@ -1598,6 +1640,8 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_enum<enums::FanDirection>(9, this->direction); buffer.encode_enum<enums::FanDirection>(9, this->direction);
buffer.encode_bool(10, this->has_speed_level); buffer.encode_bool(10, this->has_speed_level);
buffer.encode_int32(11, this->speed_level); buffer.encode_int32(11, this->speed_level);
buffer.encode_bool(12, this->has_preset_mode);
buffer.encode_string(13, this->preset_mode);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void FanCommandRequest::dump_to(std::string &out) const { void FanCommandRequest::dump_to(std::string &out) const {
@ -1648,6 +1692,14 @@ void FanCommandRequest::dump_to(std::string &out) const {
sprintf(buffer, "%" PRId32, this->speed_level); sprintf(buffer, "%" PRId32, this->speed_level);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append(" has_preset_mode: ");
out.append(YESNO(this->has_preset_mode));
out.append("\n");
out.append(" preset_mode: ");
out.append("'").append(this->preset_mode).append("'");
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -3559,6 +3611,14 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v
this->entity_category = value.as_enum<enums::EntityCategory>(); this->entity_category = value.as_enum<enums::EntityCategory>();
return true; return true;
} }
case 22: {
this->supports_current_humidity = value.as_bool();
return true;
}
case 23: {
this->supports_target_humidity = value.as_bool();
return true;
}
default: default:
return false; return false;
} }
@ -3615,6 +3675,14 @@ bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit val
this->visual_current_temperature_step = value.as_float(); this->visual_current_temperature_step = value.as_float();
return true; return true;
} }
case 24: {
this->visual_min_humidity = value.as_float();
return true;
}
case 25: {
this->visual_max_humidity = value.as_float();
return true;
}
default: default:
return false; return false;
} }
@ -3653,6 +3721,10 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(19, this->icon); buffer.encode_string(19, this->icon);
buffer.encode_enum<enums::EntityCategory>(20, this->entity_category); buffer.encode_enum<enums::EntityCategory>(20, this->entity_category);
buffer.encode_float(21, this->visual_current_temperature_step); buffer.encode_float(21, this->visual_current_temperature_step);
buffer.encode_bool(22, this->supports_current_humidity);
buffer.encode_bool(23, this->supports_target_humidity);
buffer.encode_float(24, this->visual_min_humidity);
buffer.encode_float(25, this->visual_max_humidity);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesClimateResponse::dump_to(std::string &out) const { void ListEntitiesClimateResponse::dump_to(std::string &out) const {
@ -3758,7 +3830,24 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
sprintf(buffer, "%g", this->visual_current_temperature_step); sprintf(buffer, "%g", this->visual_current_temperature_step);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append("}");
out.append(" supports_current_humidity: ");
out.append(YESNO(this->supports_current_humidity));
out.append("\n");
out.append(" supports_target_humidity: ");
out.append(YESNO(this->supports_target_humidity));
out.append("\n");
out.append(" visual_min_humidity: ");
sprintf(buffer, "%g", this->visual_min_humidity);
out.append(buffer);
out.append("\n");
out.append(" visual_max_humidity: ");
sprintf(buffer, "%g", this->visual_max_humidity);
out.append(buffer);
out.append("\n");
} }
#endif #endif
bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
@ -3827,6 +3916,14 @@ bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
this->target_temperature_high = value.as_float(); this->target_temperature_high = value.as_float();
return true; return true;
} }
case 14: {
this->current_humidity = value.as_float();
return true;
}
case 15: {
this->target_humidity = value.as_float();
return true;
}
default: default:
return false; return false;
} }
@ -3845,6 +3942,8 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(11, this->custom_fan_mode); buffer.encode_string(11, this->custom_fan_mode);
buffer.encode_enum<enums::ClimatePreset>(12, this->preset); buffer.encode_enum<enums::ClimatePreset>(12, this->preset);
buffer.encode_string(13, this->custom_preset); buffer.encode_string(13, this->custom_preset);
buffer.encode_float(14, this->current_humidity);
buffer.encode_float(15, this->target_humidity);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void ClimateStateResponse::dump_to(std::string &out) const { void ClimateStateResponse::dump_to(std::string &out) const {
@ -3906,7 +4005,16 @@ void ClimateStateResponse::dump_to(std::string &out) const {
out.append(" custom_preset: "); out.append(" custom_preset: ");
out.append("'").append(this->custom_preset).append("'"); out.append("'").append(this->custom_preset).append("'");
out.append("\n"); out.append("\n");
out.append("}");
out.append(" current_humidity: ");
sprintf(buffer, "%g", this->current_humidity);
out.append(buffer);
out.append("\n");
out.append(" target_humidity: ");
sprintf(buffer, "%g", this->target_humidity);
out.append(buffer);
out.append("\n");
} }
#endif #endif
bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
@ -3971,6 +4079,10 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
this->has_custom_preset = value.as_bool(); this->has_custom_preset = value.as_bool();
return true; return true;
} }
case 22: {
this->has_target_humidity = value.as_bool();
return true;
}
default: default:
return false; return false;
} }
@ -4007,6 +4119,10 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
this->target_temperature_high = value.as_float(); this->target_temperature_high = value.as_float();
return true; return true;
} }
case 23: {
this->target_humidity = value.as_float();
return true;
}
default: default:
return false; return false;
} }
@ -4033,6 +4149,8 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_enum<enums::ClimatePreset>(19, this->preset); buffer.encode_enum<enums::ClimatePreset>(19, this->preset);
buffer.encode_bool(20, this->has_custom_preset); buffer.encode_bool(20, this->has_custom_preset);
buffer.encode_string(21, this->custom_preset); buffer.encode_string(21, this->custom_preset);
buffer.encode_bool(22, this->has_target_humidity);
buffer.encode_float(23, this->target_humidity);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void ClimateCommandRequest::dump_to(std::string &out) const { void ClimateCommandRequest::dump_to(std::string &out) const {
@ -4125,6 +4243,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
out.append(" custom_preset: "); out.append(" custom_preset: ");
out.append("'").append(this->custom_preset).append("'"); out.append("'").append(this->custom_preset).append("'");
out.append("\n"); out.append("\n");
out.append(" has_target_humidity: ");
out.append(YESNO(this->has_target_humidity));
out.append("\n");
out.append(" target_humidity: ");
sprintf(buffer, "%g", this->target_humidity);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif

View file

@ -472,6 +472,7 @@ class ListEntitiesFanResponse : public ProtoMessage {
bool disabled_by_default{false}; bool disabled_by_default{false};
std::string icon{}; std::string icon{};
enums::EntityCategory entity_category{}; enums::EntityCategory entity_category{};
std::vector<std::string> supported_preset_modes{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
@ -490,6 +491,7 @@ class FanStateResponse : public ProtoMessage {
enums::FanSpeed speed{}; enums::FanSpeed speed{};
enums::FanDirection direction{}; enums::FanDirection direction{};
int32_t speed_level{0}; int32_t speed_level{0};
std::string preset_mode{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
@ -497,6 +499,7 @@ class FanStateResponse : public ProtoMessage {
protected: protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class FanCommandRequest : public ProtoMessage { class FanCommandRequest : public ProtoMessage {
@ -512,6 +515,8 @@ class FanCommandRequest : public ProtoMessage {
enums::FanDirection direction{}; enums::FanDirection direction{};
bool has_speed_level{false}; bool has_speed_level{false};
int32_t speed_level{0}; int32_t speed_level{0};
bool has_preset_mode{false};
std::string preset_mode{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
@ -519,6 +524,7 @@ class FanCommandRequest : public ProtoMessage {
protected: protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesLightResponse : public ProtoMessage { class ListEntitiesLightResponse : public ProtoMessage {
@ -979,6 +985,10 @@ class ListEntitiesClimateResponse : public ProtoMessage {
std::string icon{}; std::string icon{};
enums::EntityCategory entity_category{}; enums::EntityCategory entity_category{};
float visual_current_temperature_step{0.0f}; float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false};
bool supports_target_humidity{false};
float visual_min_humidity{0.0f};
float visual_max_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
@ -1004,6 +1014,8 @@ class ClimateStateResponse : public ProtoMessage {
std::string custom_fan_mode{}; std::string custom_fan_mode{};
enums::ClimatePreset preset{}; enums::ClimatePreset preset{};
std::string custom_preset{}; std::string custom_preset{};
float current_humidity{0.0f};
float target_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
@ -1037,6 +1049,8 @@ class ClimateCommandRequest : public ProtoMessage {
enums::ClimatePreset preset{}; enums::ClimatePreset preset{};
bool has_custom_preset{false}; bool has_custom_preset{false};
std::string custom_preset{}; std::string custom_preset{};
bool has_target_humidity{false};
float target_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;

View file

@ -8,7 +8,6 @@ from typing import Any
from aioesphomeapi import APIClient from aioesphomeapi import APIClient
from aioesphomeapi.api_pb2 import SubscribeLogsResponse from aioesphomeapi.api_pb2 import SubscribeLogsResponse
from aioesphomeapi.log_runner import async_run from aioesphomeapi.log_runner import async_run
from zeroconf.asyncio import AsyncZeroconf
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
from esphome.core import CORE from esphome.core import CORE
@ -18,24 +17,22 @@ 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
if CONF_ENCRYPTION in conf: if CONF_ENCRYPTION in conf:
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()
cli = APIClient( cli = APIClient(
address, address,
port, port,
password, password,
client_info=f"ESPHome Logs {__version__}", client_info=f"ESPHome Logs {__version__}",
noise_psk=noise_psk, noise_psk=noise_psk,
zeroconf_instance=aiozc.zeroconf,
) )
dashboard = CORE.dashboard dashboard = CORE.dashboard
@ -48,12 +45,10 @@ 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, name=name)
try: try:
while True: await asyncio.Event().wait()
await asyncio.sleep(60)
finally: finally:
await aiozc.async_close()
await stop() await stop()

View file

@ -15,6 +15,16 @@ void BangBangClimate::setup() {
this->publish_state(); this->publish_state();
}); });
this->current_temperature = this->sensor_->state; this->current_temperature = this->sensor_->state;
// register for humidity values and get initial state
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->add_on_state_callback([this](float state) {
this->current_humidity = state;
this->publish_state();
});
this->current_humidity = this->humidity_sensor_->state;
}
// restore set points // restore set points
auto restore = this->restore_state_(); auto restore = this->restore_state_();
if (restore.has_value()) { if (restore.has_value()) {
@ -47,6 +57,8 @@ void BangBangClimate::control(const climate::ClimateCall &call) {
climate::ClimateTraits BangBangClimate::traits() { climate::ClimateTraits BangBangClimate::traits() {
auto traits = climate::ClimateTraits(); auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true); traits.set_supports_current_temperature(true);
if (this->humidity_sensor_ != nullptr)
traits.set_supports_current_humidity(true);
traits.set_supported_modes({ traits.set_supported_modes({
climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_OFF,
}); });
@ -171,6 +183,7 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa
BangBangClimate::BangBangClimate() BangBangClimate::BangBangClimate()
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }

View file

@ -24,6 +24,7 @@ class BangBangClimate : public climate::Climate, public Component {
void dump_config() override; void dump_config() override;
void set_sensor(sensor::Sensor *sensor); void set_sensor(sensor::Sensor *sensor);
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
Trigger<> *get_idle_trigger() const; Trigger<> *get_idle_trigger() const;
Trigger<> *get_cool_trigger() const; Trigger<> *get_cool_trigger() const;
void set_supports_cool(bool supports_cool); void set_supports_cool(bool supports_cool);
@ -48,6 +49,9 @@ class BangBangClimate : public climate::Climate, public Component {
/// The sensor used for getting the current temperature /// The sensor used for getting the current temperature
sensor::Sensor *sensor_{nullptr}; sensor::Sensor *sensor_{nullptr};
/// The sensor used for getting the current humidity
sensor::Sensor *humidity_sensor_{nullptr};
/** The trigger to call when the controller should switch to idle mode. /** The trigger to call when the controller should switch to idle mode.
* *
* In idle mode, the controller is assumed to have both heating and cooling disabled. * In idle mode, the controller is assumed to have both heating and cooling disabled.

View file

@ -8,6 +8,7 @@ from esphome.const import (
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_HIGH,
CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
CONF_HEAT_ACTION, CONF_HEAT_ACTION,
CONF_HUMIDITY_SENSOR,
CONF_ID, CONF_ID,
CONF_IDLE_ACTION, CONF_IDLE_ACTION,
CONF_SENSOR, CONF_SENSOR,
@ -22,6 +23,7 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(BangBangClimate), cv.GenerateID(): cv.declare_id(BangBangClimate),
cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True),
@ -47,6 +49,10 @@ async def to_code(config):
sens = await cg.get_variable(config[CONF_SENSOR]) sens = await cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_sensor(sens)) cg.add(var.set_sensor(sens))
if CONF_HUMIDITY_SENSOR in config:
sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR])
cg.add(var.set_humidity_sensor(sens))
normal_config = BangBangClimateTargetTempConfig( normal_config = BangBangClimateTargetTempConfig(
config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH],

View file

@ -90,40 +90,41 @@ void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) {
void BP1658CJ::write_bit_(bool value) { void BP1658CJ::write_bit_(bool value) {
this->data_pin_->digital_write(value); this->data_pin_->digital_write(value);
this->clock_pin_->digital_write(true);
delayMicroseconds(BP1658CJ_DELAY); delayMicroseconds(BP1658CJ_DELAY);
this->clock_pin_->digital_write(true);
delayMicroseconds(BP1658CJ_DELAY);
this->clock_pin_->digital_write(false); this->clock_pin_->digital_write(false);
delayMicroseconds(BP1658CJ_DELAY);
} }
void BP1658CJ::write_byte_(uint8_t data) { void BP1658CJ::write_byte_(uint8_t data) {
for (uint8_t mask = 0x80; mask; mask >>= 1) { for (uint8_t mask = 0x80; mask; mask >>= 1) {
this->write_bit_(data & mask); this->write_bit_(data & mask);
delayMicroseconds(BP1658CJ_DELAY);
} }
// ack bit // ack bit
this->data_pin_->pin_mode(gpio::FLAG_INPUT); this->data_pin_->pin_mode(gpio::FLAG_INPUT);
this->clock_pin_->digital_write(true); this->clock_pin_->digital_write(true);
delayMicroseconds(BP1658CJ_DELAY); delayMicroseconds(BP1658CJ_DELAY);
this->clock_pin_->digital_write(false); this->clock_pin_->digital_write(false);
delayMicroseconds(BP1658CJ_DELAY);
this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); this->data_pin_->pin_mode(gpio::FLAG_OUTPUT);
} }
void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) { void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) {
this->data_pin_->digital_write(false); this->data_pin_->digital_write(false);
delayMicroseconds(BP1658CJ_DELAY);
this->clock_pin_->digital_write(false); this->clock_pin_->digital_write(false);
delayMicroseconds(BP1658CJ_DELAY);
for (uint32_t i = 0; i < size; i++) { for (uint32_t i = 0; i < size; i++) {
this->write_byte_(buffer[i]); this->write_byte_(buffer[i]);
delayMicroseconds(BP1658CJ_DELAY);
} }
this->clock_pin_->digital_write(true); this->clock_pin_->digital_write(true);
delayMicroseconds(BP1658CJ_DELAY);
this->data_pin_->digital_write(true); this->data_pin_->digital_write(true);
delayMicroseconds(BP1658CJ_DELAY);
} }
} // namespace bp1658cj } // namespace bp1658cj

View file

@ -8,6 +8,7 @@ from esphome.const import (
CONF_AWAY, CONF_AWAY,
CONF_AWAY_COMMAND_TOPIC, CONF_AWAY_COMMAND_TOPIC,
CONF_AWAY_STATE_TOPIC, CONF_AWAY_STATE_TOPIC,
CONF_CURRENT_HUMIDITY_STATE_TOPIC,
CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CURRENT_TEMPERATURE_STATE_TOPIC,
CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_FAN_MODE,
CONF_CUSTOM_PRESET, CONF_CUSTOM_PRESET,
@ -28,6 +29,8 @@ from esphome.const import (
CONF_SWING_MODE, CONF_SWING_MODE,
CONF_SWING_MODE_COMMAND_TOPIC, CONF_SWING_MODE_COMMAND_TOPIC,
CONF_SWING_MODE_STATE_TOPIC, CONF_SWING_MODE_STATE_TOPIC,
CONF_TARGET_HUMIDITY_COMMAND_TOPIC,
CONF_TARGET_HUMIDITY_STATE_TOPIC,
CONF_TARGET_TEMPERATURE, CONF_TARGET_TEMPERATURE,
CONF_TARGET_TEMPERATURE_COMMAND_TOPIC, CONF_TARGET_TEMPERATURE_COMMAND_TOPIC,
CONF_TARGET_TEMPERATURE_STATE_TOPIC, CONF_TARGET_TEMPERATURE_STATE_TOPIC,
@ -106,6 +109,9 @@ CLIMATE_SWING_MODES = {
validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True)
CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_CURRENT_TEMPERATURE = "current_temperature"
CONF_MIN_HUMIDITY = "min_humidity"
CONF_MAX_HUMIDITY = "max_humidity"
CONF_TARGET_HUMIDITY = "target_humidity"
visual_temperature = cv.float_with_unit( visual_temperature = cv.float_with_unit(
"visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?"
@ -153,6 +159,8 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA, cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA,
cv.Optional(CONF_MIN_HUMIDITY): cv.percentage_int,
cv.Optional(CONF_MAX_HUMIDITY): cv.percentage_int,
} }
), ),
cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All( cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All(
@ -167,6 +175,9 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
cv.Optional(CONF_CURRENT_TEMPERATURE_STATE_TOPIC): cv.All( cv.Optional(CONF_CURRENT_TEMPERATURE_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic cv.requires_component("mqtt"), cv.publish_topic
), ),
cv.Optional(CONF_CURRENT_HUMIDITY_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
cv.Optional(CONF_FAN_MODE_COMMAND_TOPIC): cv.All( cv.Optional(CONF_FAN_MODE_COMMAND_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic cv.requires_component("mqtt"), cv.publish_topic
), ),
@ -209,6 +220,12 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All( cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic cv.requires_component("mqtt"), cv.publish_topic
), ),
cv.Optional(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
cv.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
cv.Optional(CONF_ON_CONTROL): automation.validate_automation( cv.Optional(CONF_ON_CONTROL): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger),
@ -238,6 +255,10 @@ async def setup_climate_core_(var, config):
visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE], visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE],
) )
) )
if CONF_MIN_HUMIDITY in visual:
cg.add(var.set_visual_min_humidity_override(visual[CONF_MIN_HUMIDITY]))
if CONF_MAX_HUMIDITY in visual:
cg.add(var.set_visual_max_humidity_override(visual[CONF_MAX_HUMIDITY]))
if CONF_MQTT_ID in config: if CONF_MQTT_ID in config:
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
@ -255,6 +276,12 @@ async def setup_climate_core_(var, config):
config[CONF_CURRENT_TEMPERATURE_STATE_TOPIC] config[CONF_CURRENT_TEMPERATURE_STATE_TOPIC]
) )
) )
if CONF_CURRENT_HUMIDITY_STATE_TOPIC in config:
cg.add(
mqtt_.set_custom_current_humidity_state_topic(
config[CONF_CURRENT_HUMIDITY_STATE_TOPIC]
)
)
if CONF_FAN_MODE_COMMAND_TOPIC in config: if CONF_FAN_MODE_COMMAND_TOPIC in config:
cg.add( cg.add(
mqtt_.set_custom_fan_mode_command_topic( mqtt_.set_custom_fan_mode_command_topic(
@ -323,6 +350,18 @@ async def setup_climate_core_(var, config):
config[CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC] config[CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC]
) )
) )
if CONF_TARGET_HUMIDITY_COMMAND_TOPIC in config:
cg.add(
mqtt_.set_custom_target_humidity_command_topic(
config[CONF_TARGET_HUMIDITY_COMMAND_TOPIC]
)
)
if CONF_TARGET_HUMIDITY_STATE_TOPIC in config:
cg.add(
mqtt_.set_custom_target_humidity_state_topic(
config[CONF_TARGET_HUMIDITY_STATE_TOPIC]
)
)
for conf in config.get(CONF_ON_STATE, []): for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
@ -351,6 +390,7 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int),
cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"), cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"),
cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
validate_climate_fan_mode validate_climate_fan_mode
@ -387,6 +427,9 @@ async def climate_control_to_code(config, action_id, template_arg, args):
config[CONF_TARGET_TEMPERATURE_HIGH], args, float config[CONF_TARGET_TEMPERATURE_HIGH], args, float
) )
cg.add(var.set_target_temperature_high(template_)) cg.add(var.set_target_temperature_high(template_))
if CONF_TARGET_HUMIDITY in config:
template_ = await cg.templatable(config[CONF_TARGET_HUMIDITY], args, float)
cg.add(var.set_target_humidity(template_))
if CONF_FAN_MODE in config: if CONF_FAN_MODE in config:
template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
cg.add(var.set_fan_mode(template_)) cg.add(var.set_fan_mode(template_))

View file

@ -14,6 +14,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, target_temperature) TEMPLATABLE_VALUE(float, target_temperature)
TEMPLATABLE_VALUE(float, target_temperature_low) TEMPLATABLE_VALUE(float, target_temperature_low)
TEMPLATABLE_VALUE(float, target_temperature_high) TEMPLATABLE_VALUE(float, target_temperature_high)
TEMPLATABLE_VALUE(float, target_humidity)
TEMPLATABLE_VALUE(bool, away) TEMPLATABLE_VALUE(bool, away)
TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) TEMPLATABLE_VALUE(ClimateFanMode, fan_mode)
TEMPLATABLE_VALUE(std::string, custom_fan_mode) TEMPLATABLE_VALUE(std::string, custom_fan_mode)
@ -27,6 +28,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
call.set_target_temperature(this->target_temperature_.optional_value(x...)); call.set_target_temperature(this->target_temperature_.optional_value(x...));
call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...));
call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...)); call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...));
call.set_target_humidity(this->target_humidity_.optional_value(x...));
if (away_.has_value()) { if (away_.has_value()) {
call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME); call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME);
} }

View file

@ -45,6 +45,9 @@ void ClimateCall::perform() {
if (this->target_temperature_high_.has_value()) { if (this->target_temperature_high_.has_value()) {
ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_);
} }
if (this->target_humidity_.has_value()) {
ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_);
}
this->parent_->control(*this); this->parent_->control(*this);
} }
void ClimateCall::validate_() { void ClimateCall::validate_() {
@ -262,10 +265,16 @@ ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_h
this->target_temperature_high_ = target_temperature_high; this->target_temperature_high_ = target_temperature_high;
return *this; return *this;
} }
ClimateCall &ClimateCall::set_target_humidity(float target_humidity) {
this->target_humidity_ = target_humidity;
return *this;
}
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; } const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; }
const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; } const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; }
const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; }
const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; }
const optional<float> &ClimateCall::get_target_humidity() const { return this->target_humidity_; }
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; } const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
@ -283,6 +292,10 @@ ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperat
this->target_temperature_ = target_temperature; this->target_temperature_ = target_temperature;
return *this; return *this;
} }
ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) {
this->target_humidity_ = target_humidity;
return *this;
}
ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) { ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
this->mode_ = mode; this->mode_ = mode;
return *this; return *this;
@ -343,6 +356,9 @@ void Climate::save_state_() {
} else { } else {
state.target_temperature = this->target_temperature; state.target_temperature = this->target_temperature;
} }
if (traits.get_supports_target_humidity()) {
state.target_humidity = this->target_humidity;
}
if (traits.get_supports_fan_modes() && fan_mode.has_value()) { if (traits.get_supports_fan_modes() && fan_mode.has_value()) {
state.uses_custom_fan_mode = false; state.uses_custom_fan_mode = false;
state.fan_mode = this->fan_mode.value(); state.fan_mode = this->fan_mode.value();
@ -408,6 +424,12 @@ void Climate::publish_state() {
} else { } else {
ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature); ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature);
} }
if (traits.get_supports_current_humidity()) {
ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity);
}
if (traits.get_supports_target_humidity()) {
ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity);
}
// Send state to frontend // Send state to frontend
this->state_callback_.call(*this); this->state_callback_.call(*this);
@ -427,6 +449,12 @@ ClimateTraits Climate::get_traits() {
traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_); traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_);
traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_); traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_);
} }
if (this->visual_min_humidity_override_.has_value()) {
traits.set_visual_min_humidity(*this->visual_min_humidity_override_);
}
if (this->visual_max_humidity_override_.has_value()) {
traits.set_visual_max_humidity(*this->visual_max_humidity_override_);
}
return traits; return traits;
} }
@ -441,6 +469,12 @@ void Climate::set_visual_temperature_step_override(float target, float current)
this->visual_target_temperature_step_override_ = target; this->visual_target_temperature_step_override_ = target;
this->visual_current_temperature_step_override_ = current; this->visual_current_temperature_step_override_ = current;
} }
void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) {
this->visual_min_humidity_override_ = visual_min_humidity_override;
}
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
this->visual_max_humidity_override_ = visual_max_humidity_override;
}
ClimateCall Climate::make_call() { return ClimateCall(this); } ClimateCall Climate::make_call() { return ClimateCall(this); }
@ -454,6 +488,9 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
} else { } else {
call.set_target_temperature(this->target_temperature); call.set_target_temperature(this->target_temperature);
} }
if (traits.get_supports_target_humidity()) {
call.set_target_humidity(this->target_humidity);
}
if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
call.set_fan_mode(this->fan_mode); call.set_fan_mode(this->fan_mode);
} }
@ -474,6 +511,9 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
} else { } else {
climate->target_temperature = this->target_temperature; climate->target_temperature = this->target_temperature;
} }
if (traits.get_supports_target_humidity()) {
climate->target_humidity = this->target_humidity;
}
if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
climate->fan_mode = this->fan_mode; climate->fan_mode = this->fan_mode;
} }
@ -530,17 +570,25 @@ void Climate::dump_traits_(const char *tag) {
auto traits = this->get_traits(); auto traits = this->get_traits();
ESP_LOGCONFIG(tag, "ClimateTraits:"); ESP_LOGCONFIG(tag, "ClimateTraits:");
ESP_LOGCONFIG(tag, " [x] Visual settings:"); ESP_LOGCONFIG(tag, " [x] Visual settings:");
ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature()); ESP_LOGCONFIG(tag, " - Min temperature: %.1f", traits.get_visual_min_temperature());
ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature()); ESP_LOGCONFIG(tag, " - Max temperature: %.1f", traits.get_visual_max_temperature());
ESP_LOGCONFIG(tag, " - Step:"); ESP_LOGCONFIG(tag, " - Temperature step:");
ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step()); ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step());
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step()); ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
ESP_LOGCONFIG(tag, " - Min humidity: %.0f", traits.get_visual_min_humidity());
ESP_LOGCONFIG(tag, " - Max humidity: %.0f", traits.get_visual_max_humidity());
if (traits.get_supports_current_temperature()) { if (traits.get_supports_current_temperature()) {
ESP_LOGCONFIG(tag, " [x] Supports current temperature"); ESP_LOGCONFIG(tag, " [x] Supports current temperature");
} }
if (traits.get_supports_current_humidity()) {
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
}
if (traits.get_supports_two_point_target_temperature()) { if (traits.get_supports_two_point_target_temperature()) {
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature"); ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
} }
if (traits.get_supports_target_humidity()) {
ESP_LOGCONFIG(tag, " [x] Supports target humidity");
}
if (traits.get_supports_action()) { if (traits.get_supports_action()) {
ESP_LOGCONFIG(tag, " [x] Supports action"); ESP_LOGCONFIG(tag, " [x] Supports action");
} }

View file

@ -64,6 +64,10 @@ class ClimateCall {
* For climate devices with two point target temperature control * For climate devices with two point target temperature control
*/ */
ClimateCall &set_target_temperature_high(optional<float> target_temperature_high); ClimateCall &set_target_temperature_high(optional<float> target_temperature_high);
/// Set the target humidity of the climate device.
ClimateCall &set_target_humidity(float target_humidity);
/// Set the target humidity of the climate device.
ClimateCall &set_target_humidity(optional<float> target_humidity);
/// Set the fan mode of the climate device. /// Set the fan mode of the climate device.
ClimateCall &set_fan_mode(ClimateFanMode fan_mode); ClimateCall &set_fan_mode(ClimateFanMode fan_mode);
/// Set the fan mode of the climate device. /// Set the fan mode of the climate device.
@ -93,6 +97,7 @@ class ClimateCall {
const optional<float> &get_target_temperature() const; const optional<float> &get_target_temperature() const;
const optional<float> &get_target_temperature_low() const; const optional<float> &get_target_temperature_low() const;
const optional<float> &get_target_temperature_high() const; const optional<float> &get_target_temperature_high() const;
const optional<float> &get_target_humidity() const;
const optional<ClimateFanMode> &get_fan_mode() const; const optional<ClimateFanMode> &get_fan_mode() const;
const optional<ClimateSwingMode> &get_swing_mode() const; const optional<ClimateSwingMode> &get_swing_mode() const;
const optional<std::string> &get_custom_fan_mode() const; const optional<std::string> &get_custom_fan_mode() const;
@ -107,6 +112,7 @@ class ClimateCall {
optional<float> target_temperature_; optional<float> target_temperature_;
optional<float> target_temperature_low_; optional<float> target_temperature_low_;
optional<float> target_temperature_high_; optional<float> target_temperature_high_;
optional<float> target_humidity_;
optional<ClimateFanMode> fan_mode_; optional<ClimateFanMode> fan_mode_;
optional<ClimateSwingMode> swing_mode_; optional<ClimateSwingMode> swing_mode_;
optional<std::string> custom_fan_mode_; optional<std::string> custom_fan_mode_;
@ -136,6 +142,7 @@ struct ClimateDeviceRestoreState {
float target_temperature_high; float target_temperature_high;
}; };
}; };
float target_humidity;
/// Convert this struct to a climate call that can be performed. /// Convert this struct to a climate call that can be performed.
ClimateCall to_call(Climate *climate); ClimateCall to_call(Climate *climate);
@ -160,24 +167,34 @@ struct ClimateDeviceRestoreState {
*/ */
class Climate : public EntityBase { class Climate : public EntityBase {
public: public:
Climate() {}
/// The active mode of the climate device. /// The active mode of the climate device.
ClimateMode mode{CLIMATE_MODE_OFF}; ClimateMode mode{CLIMATE_MODE_OFF};
/// The active state of the climate device. /// The active state of the climate device.
ClimateAction action{CLIMATE_ACTION_OFF}; ClimateAction action{CLIMATE_ACTION_OFF};
/// The current temperature of the climate device, as reported from the integration. /// The current temperature of the climate device, as reported from the integration.
float current_temperature{NAN}; float current_temperature{NAN};
/// The current humidity of the climate device, as reported from the integration.
float current_humidity{NAN};
union { union {
/// The target temperature of the climate device. /// The target temperature of the climate device.
float target_temperature; float target_temperature;
struct { struct {
/// The minimum target temperature of the climate device, for climate devices with split target temperature. /// The minimum target temperature of the climate device, for climate devices with split target temperature.
float target_temperature_low; float target_temperature_low{NAN};
/// The maximum target temperature of the climate device, for climate devices with split target temperature. /// The maximum target temperature of the climate device, for climate devices with split target temperature.
float target_temperature_high; float target_temperature_high{NAN};
}; };
}; };
/// The target humidity of the climate device.
float target_humidity;
/// The active fan mode of the climate device. /// The active fan mode of the climate device.
optional<ClimateFanMode> fan_mode; optional<ClimateFanMode> fan_mode;
@ -231,6 +248,8 @@ class Climate : public EntityBase {
void set_visual_min_temperature_override(float visual_min_temperature_override); void set_visual_min_temperature_override(float visual_min_temperature_override);
void set_visual_max_temperature_override(float visual_max_temperature_override); void set_visual_max_temperature_override(float visual_max_temperature_override);
void set_visual_temperature_step_override(float target, float current); void set_visual_temperature_step_override(float target, float current);
void set_visual_min_humidity_override(float visual_min_humidity_override);
void set_visual_max_humidity_override(float visual_max_humidity_override);
protected: protected:
friend ClimateCall; friend ClimateCall;
@ -280,6 +299,8 @@ class Climate : public EntityBase {
optional<float> visual_max_temperature_override_{}; optional<float> visual_max_temperature_override_{};
optional<float> visual_target_temperature_step_override_{}; optional<float> visual_target_temperature_step_override_{};
optional<float> visual_current_temperature_step_override_{}; optional<float> visual_current_temperature_step_override_{};
optional<float> visual_min_humidity_override_{};
optional<float> visual_max_humidity_override_{};
}; };
} // namespace climate } // namespace climate

View file

@ -44,10 +44,18 @@ class ClimateTraits {
void set_supports_current_temperature(bool supports_current_temperature) { void set_supports_current_temperature(bool supports_current_temperature) {
supports_current_temperature_ = supports_current_temperature; supports_current_temperature_ = supports_current_temperature;
} }
bool get_supports_current_humidity() const { return supports_current_humidity_; }
void set_supports_current_humidity(bool supports_current_humidity) {
supports_current_humidity_ = supports_current_humidity;
}
bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; } bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; }
void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) {
supports_two_point_target_temperature_ = supports_two_point_target_temperature; supports_two_point_target_temperature_ = supports_two_point_target_temperature;
} }
bool get_supports_target_humidity() const { return supports_target_humidity_; }
void set_supports_target_humidity(bool supports_target_humidity) {
supports_target_humidity_ = supports_target_humidity;
}
void set_supported_modes(std::set<ClimateMode> modes) { supported_modes_ = std::move(modes); } void set_supported_modes(std::set<ClimateMode> modes) { supported_modes_ = std::move(modes); }
void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); } void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
@ -153,6 +161,11 @@ class ClimateTraits {
int8_t get_target_temperature_accuracy_decimals() const; int8_t get_target_temperature_accuracy_decimals() const;
int8_t get_current_temperature_accuracy_decimals() const; int8_t get_current_temperature_accuracy_decimals() const;
float get_visual_min_humidity() const { return visual_min_humidity_; }
void set_visual_min_humidity(float visual_min_humidity) { visual_min_humidity_ = visual_min_humidity; }
float get_visual_max_humidity() const { return visual_max_humidity_; }
void set_visual_max_humidity(float visual_max_humidity) { visual_max_humidity_ = visual_max_humidity; }
protected: protected:
void set_mode_support_(climate::ClimateMode mode, bool supported) { void set_mode_support_(climate::ClimateMode mode, bool supported) {
if (supported) { if (supported) {
@ -177,7 +190,9 @@ class ClimateTraits {
} }
bool supports_current_temperature_{false}; bool supports_current_temperature_{false};
bool supports_current_humidity_{false};
bool supports_two_point_target_temperature_{false}; bool supports_two_point_target_temperature_{false};
bool supports_target_humidity_{false};
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF}; std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
bool supports_action_{false}; bool supports_action_{false};
std::set<climate::ClimateFanMode> supported_fan_modes_; std::set<climate::ClimateFanMode> supported_fan_modes_;
@ -190,6 +205,8 @@ class ClimateTraits {
float visual_max_temperature_{30}; float visual_max_temperature_{30};
float visual_target_temperature_step_{0.1}; float visual_target_temperature_step_{0.1};
float visual_current_temperature_step_{0.1}; float visual_current_temperature_step_{0.1};
float visual_min_humidity_{30};
float visual_max_humidity_{99};
}; };
} // namespace climate } // namespace climate

View file

@ -1,38 +1,37 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import ( from esphome.components import climate, sensor, remote_base
climate,
remote_transmitter,
remote_receiver,
sensor,
remote_base,
)
from esphome.components.remote_base import CONF_RECEIVER_ID, CONF_TRANSMITTER_ID
from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR
DEPENDENCIES = ["remote_transmitter"]
AUTO_LOAD = ["sensor", "remote_base"] AUTO_LOAD = ["sensor", "remote_base"]
CODEOWNERS = ["@glmnet"] CODEOWNERS = ["@glmnet"]
climate_ir_ns = cg.esphome_ns.namespace("climate_ir") climate_ir_ns = cg.esphome_ns.namespace("climate_ir")
ClimateIR = climate_ir_ns.class_( ClimateIR = climate_ir_ns.class_(
"ClimateIR", climate.Climate, cg.Component, remote_base.RemoteReceiverListener "ClimateIR",
climate.Climate,
cg.Component,
remote_base.RemoteReceiverListener,
remote_base.RemoteTransmittable,
) )
CLIMATE_IR_SCHEMA = climate.CLIMATE_SCHEMA.extend( CLIMATE_IR_SCHEMA = (
climate.CLIMATE_SCHEMA.extend(
{ {
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
),
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
} }
).extend(cv.COMPONENT_SCHEMA) )
.extend(cv.COMPONENT_SCHEMA)
.extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA)
)
CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend( CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend(
{ {
cv.Optional(CONF_RECEIVER_ID): cv.use_id( cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id(
remote_receiver.RemoteReceiverComponent remote_base.RemoteReceiverBase
), ),
} }
) )
@ -41,15 +40,11 @@ CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend(
async def register_climate_ir(var, config): async def register_climate_ir(var, config):
await cg.register_component(var, config) await cg.register_component(var, config)
await climate.register_climate(var, config) await climate.register_climate(var, config)
await remote_base.register_transmittable(var, config)
cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
if remote_base.CONF_RECEIVER_ID in config:
await remote_base.register_listener(var, config)
if sensor_id := config.get(CONF_SENSOR): if sensor_id := config.get(CONF_SENSOR):
sens = await cg.get_variable(sensor_id) sens = await cg.get_variable(sensor_id)
cg.add(var.set_sensor(sens)) cg.add(var.set_sensor(sens))
if receiver_id := config.get(CONF_RECEIVER_ID):
receiver = await cg.get_variable(receiver_id)
cg.add(receiver.register_listener(var))
transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter))

View file

@ -18,7 +18,10 @@ namespace climate_ir {
Likewise to decode a IR into the AC state, implement Likewise to decode a IR into the AC state, implement
bool RemoteReceiverListener::on_receive(remote_base::RemoteReceiveData data) and return true bool RemoteReceiverListener::on_receive(remote_base::RemoteReceiveData data) and return true
*/ */
class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener { class ClimateIR : public Component,
public climate::Climate,
public remote_base::RemoteReceiverListener,
public remote_base::RemoteTransmittable {
public: public:
ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f,
bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {}, bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {},
@ -35,9 +38,6 @@ class ClimateIR : public climate::Climate, public Component, public remote_base:
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
this->transmitter_ = transmitter;
}
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
@ -64,7 +64,6 @@ class ClimateIR : public climate::Climate, public Component, public remote_base:
std::set<climate::ClimateSwingMode> swing_modes_ = {}; std::set<climate::ClimateSwingMode> swing_modes_ = {};
std::set<climate::ClimatePreset> presets_ = {}; std::set<climate::ClimatePreset> presets_ = {};
remote_transmitter::RemoteTransmitterComponent *transmitter_;
sensor::Sensor *sensor_{nullptr}; sensor::Sensor *sensor_{nullptr};
}; };

View file

@ -102,11 +102,7 @@ void CoolixClimate::transmit_state() {
} }
} }
ESP_LOGV(TAG, "Sending coolix code: 0x%06" PRIX32, remote_state); ESP_LOGV(TAG, "Sending coolix code: 0x%06" PRIX32, remote_state);
this->transmit_<remote_base::CoolixProtocol>(remote_state);
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
remote_base::CoolixProtocol().encode(data, remote_state);
transmit.perform();
} }
bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data) { bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data) {

View file

@ -12,6 +12,7 @@ void CopyFan::setup() {
this->oscillating = source_->oscillating; this->oscillating = source_->oscillating;
this->speed = source_->speed; this->speed = source_->speed;
this->direction = source_->direction; this->direction = source_->direction;
this->preset_mode = source_->preset_mode;
this->publish_state(); this->publish_state();
}); });
@ -19,6 +20,7 @@ void CopyFan::setup() {
this->oscillating = source_->oscillating; this->oscillating = source_->oscillating;
this->speed = source_->speed; this->speed = source_->speed;
this->direction = source_->direction; this->direction = source_->direction;
this->preset_mode = source_->preset_mode;
this->publish_state(); this->publish_state();
} }
@ -33,6 +35,7 @@ fan::FanTraits CopyFan::get_traits() {
traits.set_speed(base.supports_speed()); traits.set_speed(base.supports_speed());
traits.set_supported_speed_count(base.supported_speed_count()); traits.set_supported_speed_count(base.supported_speed_count());
traits.set_direction(base.supports_direction()); traits.set_direction(base.supports_direction());
traits.set_supported_preset_modes(base.supported_preset_modes());
return traits; return traits;
} }
@ -46,6 +49,8 @@ void CopyFan::control(const fan::FanCall &call) {
call2.set_speed(*call.get_speed()); call2.set_speed(*call.get_speed());
if (call.get_direction().has_value()) if (call.get_direction().has_value())
call2.set_direction(*call.get_direction()); call2.set_direction(*call.get_direction());
if (!call.get_preset_mode().empty())
call2.set_preset_mode(call.get_preset_mode());
call2.perform(); call2.perform();
} }

View file

@ -58,7 +58,7 @@ BASIC_DISPLAY_SCHEMA = cv.Schema(
{ {
cv.Optional(CONF_LAMBDA): cv.lambda_, cv.Optional(CONF_LAMBDA): cv.lambda_,
} }
) ).extend(cv.polling_component_schema("1s"))
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
{ {
@ -116,6 +116,7 @@ async def setup_display_core_(var, config):
async def register_display(var, config): async def register_display(var, config):
await cg.register_component(var, config)
await setup_display_core_(var, config) await setup_display_core_(var, config)

View file

@ -166,6 +166,13 @@ void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, in
} }
#endif // USE_QR_CODE #endif // USE_QR_CODE
#ifdef USE_GRAPHICAL_DISPLAY_MENU
void Display::menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height) {
Rect rect(x, y, width, height);
menu->draw(this, &rect);
}
#endif // USE_GRAPHICAL_DISPLAY_MENU
void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1,
int *width, int *height) { int *width, int *height) {
int x_offset, baseline; int x_offset, baseline;

View file

@ -17,6 +17,10 @@
#include "esphome/components/qr_code/qr_code.h" #include "esphome/components/qr_code/qr_code.h"
#endif #endif
#ifdef USE_GRAPHICAL_DISPLAY_MENU
#include "esphome/components/graphical_display_menu/graphical_display_menu.h"
#endif
namespace esphome { namespace esphome {
namespace display { namespace display {
@ -163,7 +167,7 @@ class BaseFont {
virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0; virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0;
}; };
class Display { class Display : public PollingComponent {
public: public:
/// Fill the entire screen with the given color. /// Fill the entire screen with the given color.
virtual void fill(Color color); virtual void fill(Color color);
@ -392,6 +396,17 @@ class Display {
void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1); void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1);
#endif #endif
#ifdef USE_GRAPHICAL_DISPLAY_MENU
/**
* @param x The x coordinate of the upper left corner
* @param y The y coordinate of the upper left corner
* @param menu The GraphicalDisplayMenu to draw
* @param width Width of the menu
* @param height Height of the menu
*/
void menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height);
#endif // USE_GRAPHICAL_DISPLAY_MENU
/** Get the text bounds of the given string. /** Get the text bounds of the given string.
* *
* @param x The x coordinate to place the string at, can be 0 if only interested in dimensions. * @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.

View file

@ -172,6 +172,8 @@ void DisplayMenuComponent::show_main() {
this->process_initial_(); this->process_initial_();
this->on_before_show();
if (this->active_ && this->editing_) if (this->active_ && this->editing_)
this->finish_editing_(); this->finish_editing_();
@ -188,6 +190,8 @@ void DisplayMenuComponent::show_main() {
} }
this->draw_and_update(); this->draw_and_update();
this->on_after_show();
} }
void DisplayMenuComponent::show() { void DisplayMenuComponent::show() {
@ -196,18 +200,26 @@ void DisplayMenuComponent::show() {
this->process_initial_(); this->process_initial_();
this->on_before_show();
if (!this->active_) { if (!this->active_) {
this->active_ = true; this->active_ = true;
this->draw_and_update(); this->draw_and_update();
} }
this->on_after_show();
} }
void DisplayMenuComponent::hide() { void DisplayMenuComponent::hide() {
if (this->check_healthy_and_active_()) { if (this->check_healthy_and_active_()) {
this->on_before_hide();
if (this->editing_) if (this->editing_)
this->finish_editing_(); this->finish_editing_();
this->active_ = false; this->active_ = false;
this->update(); this->update();
this->on_after_hide();
} }
} }

View file

@ -60,6 +60,11 @@ class DisplayMenuComponent : public Component {
update(); update();
} }
virtual void on_before_show(){};
virtual void on_after_show(){};
virtual void on_before_hide(){};
virtual void on_after_hide(){};
uint8_t rows_; uint8_t rows_;
bool active_; bool active_;
MenuMode mode_; MenuMode mode_;

View file

@ -5,6 +5,29 @@
namespace esphome { namespace esphome {
namespace display_menu_base { namespace display_menu_base {
const LogString *menu_item_type_to_string(MenuItemType type) {
switch (type) {
case MenuItemType::MENU_ITEM_LABEL:
return LOG_STR("MENU_ITEM_LABEL");
case MenuItemType::MENU_ITEM_MENU:
return LOG_STR("MENU_ITEM_MENU");
case MenuItemType::MENU_ITEM_BACK:
return LOG_STR("MENU_ITEM_BACK");
case MenuItemType::MENU_ITEM_SELECT:
return LOG_STR("MENU_ITEM_SELECT");
case MenuItemType::MENU_ITEM_NUMBER:
return LOG_STR("MENU_ITEM_NUMBER");
case MenuItemType::MENU_ITEM_SWITCH:
return LOG_STR("MENU_ITEM_SWITCH");
case MenuItemType::MENU_ITEM_COMMAND:
return LOG_STR("MENU_ITEM_COMMAND");
case MenuItemType::MENU_ITEM_CUSTOM:
return LOG_STR("MENU_ITEM_CUSTOM");
default:
return LOG_STR("UNKNOWN");
}
}
void MenuItem::on_enter() { this->on_enter_callbacks_.call(); } void MenuItem::on_enter() { this->on_enter_callbacks_.call(); }
void MenuItem::on_leave() { this->on_leave_callbacks_.call(); } void MenuItem::on_leave() { this->on_leave_callbacks_.call(); }

View file

@ -14,6 +14,7 @@
#endif #endif
#include <vector> #include <vector>
#include "esphome/core/log.h"
namespace esphome { namespace esphome {
namespace display_menu_base { namespace display_menu_base {
@ -29,6 +30,9 @@ enum MenuItemType {
MENU_ITEM_CUSTOM, MENU_ITEM_CUSTOM,
}; };
/// @brief Returns a string representation of a menu item type suitable for logging
const LogString *menu_item_type_to_string(MenuItemType type);
class MenuItem; class MenuItem;
class MenuItemMenu; class MenuItemMenu;
using value_getter_t = std::function<std::string(const MenuItem *)>; using value_getter_t = std::function<std::string(const MenuItem *)>;

View file

@ -12,7 +12,6 @@ ektf2232_ns = cg.esphome_ns.namespace("ektf2232")
EKTF2232Touchscreen = ektf2232_ns.class_( EKTF2232Touchscreen = ektf2232_ns.class_(
"EKTF2232Touchscreen", "EKTF2232Touchscreen",
touchscreen.Touchscreen, touchscreen.Touchscreen,
cg.Component,
i2c.I2CDevice, i2c.I2CDevice,
) )
@ -28,17 +27,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
), ),
cv.Required(CONF_RTS_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_RTS_PIN): pins.gpio_output_pin_schema,
} }
) ).extend(i2c.i2c_device_schema(0x15))
.extend(i2c.i2c_device_schema(0x15))
.extend(cv.COMPONENT_SCHEMA)
) )
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
await touchscreen.register_touchscreen(var, config) await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN])
cg.add(var.set_interrupt_pin(interrupt_pin)) cg.add(var.set_interrupt_pin(interrupt_pin))

View file

@ -15,16 +15,12 @@ static const uint8_t GET_X_RES[4] = {0x53, 0x60, 0x00, 0x00};
static const uint8_t GET_Y_RES[4] = {0x53, 0x63, 0x00, 0x00}; static const uint8_t GET_Y_RES[4] = {0x53, 0x63, 0x00, 0x00};
static const uint8_t GET_POWER_STATE_CMD[4] = {0x53, 0x50, 0x00, 0x01}; static const uint8_t GET_POWER_STATE_CMD[4] = {0x53, 0x50, 0x00, 0x01};
void EKTF2232TouchscreenStore::gpio_intr(EKTF2232TouchscreenStore *store) { store->touch = true; }
void EKTF2232Touchscreen::setup() { void EKTF2232Touchscreen::setup() {
ESP_LOGCONFIG(TAG, "Setting up EKT2232 Touchscreen..."); ESP_LOGCONFIG(TAG, "Setting up EKT2232 Touchscreen...");
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
this->interrupt_pin_->setup(); this->interrupt_pin_->setup();
this->store_.pin = this->interrupt_pin_->to_isr(); this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
this->interrupt_pin_->attach_interrupt(EKTF2232TouchscreenStore::gpio_intr, &this->store_,
gpio::INTERRUPT_FALLING_EDGE);
this->rts_pin_->setup(); this->rts_pin_->setup();
@ -45,7 +41,7 @@ void EKTF2232Touchscreen::setup() {
this->mark_failed(); this->mark_failed();
return; return;
} }
this->x_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
this->write(GET_Y_RES, 4); this->write(GET_Y_RES, 4);
if (this->read(received, 4)) { if (this->read(received, 4)) {
@ -54,19 +50,14 @@ void EKTF2232Touchscreen::setup() {
this->mark_failed(); this->mark_failed();
return; return;
} }
this->y_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
this->store_.touch = false;
this->set_power_state(true); this->set_power_state(true);
} }
void EKTF2232Touchscreen::loop() { void EKTF2232Touchscreen::update_touches() {
if (!this->store_.touch)
return;
this->store_.touch = false;
uint8_t touch_count = 0; uint8_t touch_count = 0;
std::vector<TouchPoint> touches; int16_t x_raw, y_raw;
uint8_t raw[8]; uint8_t raw[8];
this->read(raw, 8); this->read(raw, 8);
@ -75,45 +66,15 @@ void EKTF2232Touchscreen::loop() {
touch_count++; touch_count++;
} }
if (touch_count == 0) {
for (auto *listener : this->touch_listeners_)
listener->release();
return;
}
touch_count = std::min<uint8_t>(touch_count, 2); touch_count = std::min<uint8_t>(touch_count, 2);
ESP_LOGV(TAG, "Touch count: %d", touch_count); ESP_LOGV(TAG, "Touch count: %d", touch_count);
for (int i = 0; i < touch_count; i++) { for (int i = 0; i < touch_count; i++) {
uint8_t *d = raw + 1 + (i * 3); uint8_t *d = raw + 1 + (i * 3);
uint32_t raw_x = (d[0] & 0xF0) << 4 | d[1]; x_raw = (d[0] & 0xF0) << 4 | d[1];
uint32_t raw_y = (d[0] & 0x0F) << 8 | d[2]; y_raw = (d[0] & 0x0F) << 8 | d[2];
this->set_raw_touch_position_(i, x_raw, y_raw);
raw_x = raw_x * this->display_height_ - 1;
raw_y = raw_y * this->display_width_ - 1;
TouchPoint tp;
switch (this->rotation_) {
case ROTATE_0_DEGREES:
tp.y = raw_x / this->x_resolution_;
tp.x = this->display_width_ - 1 - (raw_y / this->y_resolution_);
break;
case ROTATE_90_DEGREES:
tp.x = raw_x / this->x_resolution_;
tp.y = raw_y / this->y_resolution_;
break;
case ROTATE_180_DEGREES:
tp.y = this->display_height_ - 1 - (raw_x / this->x_resolution_);
tp.x = raw_y / this->y_resolution_;
break;
case ROTATE_270_DEGREES:
tp.x = this->display_height_ - 1 - (raw_x / this->x_resolution_);
tp.y = this->display_width_ - 1 - (raw_y / this->y_resolution_);
break;
}
this->defer([this, tp]() { this->send_touch_(tp); });
} }
} }
@ -126,7 +87,7 @@ void EKTF2232Touchscreen::set_power_state(bool enable) {
bool EKTF2232Touchscreen::get_power_state() { bool EKTF2232Touchscreen::get_power_state() {
uint8_t received[4]; uint8_t received[4];
this->write(GET_POWER_STATE_CMD, 4); this->write(GET_POWER_STATE_CMD, 4);
this->store_.touch = false; this->store_.touched = false;
this->read(received, 4); this->read(received, 4);
return (received[1] >> 3) & 1; return (received[1] >> 3) & 1;
} }
@ -145,14 +106,14 @@ bool EKTF2232Touchscreen::soft_reset_() {
uint8_t received[4]; uint8_t received[4];
uint16_t timeout = 1000; uint16_t timeout = 1000;
while (!this->store_.touch && timeout > 0) { while (!this->store_.touched && timeout > 0) {
delay(1); delay(1);
timeout--; timeout--;
} }
if (timeout > 0) if (timeout > 0)
this->store_.touch = true; this->store_.touched = true;
this->read(received, 4); this->read(received, 4);
this->store_.touch = false; this->store_.touched = false;
return !memcmp(received, HELLO, 4); return !memcmp(received, HELLO, 4);
} }

View file

@ -9,19 +9,11 @@
namespace esphome { namespace esphome {
namespace ektf2232 { namespace ektf2232 {
struct EKTF2232TouchscreenStore {
volatile bool touch;
ISRInternalGPIOPin pin;
static void gpio_intr(EKTF2232TouchscreenStore *store);
};
using namespace touchscreen; using namespace touchscreen;
class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice {
public: public:
void setup() override; void setup() override;
void loop() override;
void dump_config() override; void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
@ -33,12 +25,10 @@ class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2
protected: protected:
void hard_reset_(); void hard_reset_();
bool soft_reset_(); bool soft_reset_();
void update_touches() override;
InternalGPIOPin *interrupt_pin_; InternalGPIOPin *interrupt_pin_;
GPIOPin *rts_pin_; GPIOPin *rts_pin_;
EKTF2232TouchscreenStore store_;
uint16_t x_resolution_;
uint16_t y_resolution_;
}; };
} // namespace ektf2232 } // namespace ektf2232

View file

@ -0,0 +1 @@
CODEOWNERS = ["@vincentscode"]

View file

@ -0,0 +1,321 @@
// ENS160 sensor with I2C interface from ScioSense
//
// Datasheet: https://www.sciosense.com/wp-content/uploads/documents/SC-001224-DS-7-ENS160-Datasheet.pdf
//
// Implementation based on:
// https://github.com/sciosense/ENS160_driver
#include "ens160.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace ens160 {
static const char *const TAG = "ens160";
static const uint8_t ENS160_BOOTING = 10;
static const uint16_t ENS160_PART_ID = 0x0160;
static const uint8_t ENS160_REG_PART_ID = 0x00;
static const uint8_t ENS160_REG_OPMODE = 0x10;
static const uint8_t ENS160_REG_CONFIG = 0x11;
static const uint8_t ENS160_REG_COMMAND = 0x12;
static const uint8_t ENS160_REG_TEMP_IN = 0x13;
static const uint8_t ENS160_REG_DATA_STATUS = 0x20;
static const uint8_t ENS160_REG_DATA_AQI = 0x21;
static const uint8_t ENS160_REG_DATA_TVOC = 0x22;
static const uint8_t ENS160_REG_DATA_ECO2 = 0x24;
static const uint8_t ENS160_REG_GPR_READ_0 = 0x48;
static const uint8_t ENS160_REG_GPR_READ_4 = ENS160_REG_GPR_READ_0 + 4;
static const uint8_t ENS160_COMMAND_NOP = 0x00;
static const uint8_t ENS160_COMMAND_CLRGPR = 0xCC;
static const uint8_t ENS160_COMMAND_GET_APPVER = 0x0E;
static const uint8_t ENS160_OPMODE_RESET = 0xF0;
static const uint8_t ENS160_OPMODE_IDLE = 0x01;
static const uint8_t ENS160_OPMODE_STD = 0x02;
static const uint8_t ENS160_DATA_STATUS_STATAS = 0x80;
static const uint8_t ENS160_DATA_STATUS_STATER = 0x40;
static const uint8_t ENS160_DATA_STATUS_VALIDITY = 0x0C;
static const uint8_t ENS160_DATA_STATUS_NEWDAT = 0x02;
static const uint8_t ENS160_DATA_STATUS_NEWGPR = 0x01;
// helps remove reserved bits in aqi data register
static const uint8_t ENS160_DATA_AQI = 0x07;
void ENS160Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up ENS160...");
// check part_id
uint16_t part_id;
if (!this->read_bytes(ENS160_REG_PART_ID, reinterpret_cast<uint8_t *>(&part_id), 2)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
if (part_id != ENS160_PART_ID) {
this->error_code_ = INVALID_ID;
this->mark_failed();
return;
}
// set mode to reset
if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_RESET)) {
this->error_code_ = WRITE_FAILED;
this->mark_failed();
return;
}
delay(ENS160_BOOTING);
// check status
uint8_t status_value;
if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) {
this->error_code_ = READ_FAILED;
this->mark_failed();
return;
}
this->validity_flag_ = static_cast<ValidityFlag>((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2);
if (this->validity_flag_ == INVALID_OUTPUT) {
this->error_code_ = VALIDITY_INVALID;
this->mark_failed();
return;
}
// set mode to idle
if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_IDLE)) {
this->error_code_ = WRITE_FAILED;
this->mark_failed();
return;
}
// clear command
if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_NOP)) {
this->error_code_ = WRITE_FAILED;
this->mark_failed();
return;
}
if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_CLRGPR)) {
this->error_code_ = WRITE_FAILED;
this->mark_failed();
return;
}
// read firmware version
if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) {
this->error_code_ = WRITE_FAILED;
this->mark_failed();
return;
}
uint8_t version_data[3];
if (!this->read_bytes(ENS160_REG_GPR_READ_4, version_data, 3)) {
this->error_code_ = READ_FAILED;
this->mark_failed();
return;
}
this->firmware_ver_major_ = version_data[0];
this->firmware_ver_minor_ = version_data[1];
this->firmware_ver_build_ = version_data[2];
// set mode to standard
if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_STD)) {
this->error_code_ = WRITE_FAILED;
this->mark_failed();
return;
}
// read opmode and check standard mode is achieved before finishing Setup
uint8_t op_mode;
if (!this->read_byte(ENS160_REG_OPMODE, &op_mode)) {
this->error_code_ = READ_FAILED;
this->mark_failed();
return;
}
if (op_mode != ENS160_OPMODE_STD) {
this->error_code_ = STD_OPMODE_FAILED;
this->mark_failed();
return;
}
}
void ENS160Component::update() {
uint8_t status_value, data_ready;
if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) {
ESP_LOGW(TAG, "Error reading status register");
this->status_set_warning();
return;
}
// verbose status logging
ESP_LOGV(TAG, "Status: ENS160 STATAS bit 0x%x",
(ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS);
ESP_LOGV(TAG, "Status: ENS160 STATER bit 0x%x",
(ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER);
ESP_LOGV(TAG, "Status: ENS160 VALIDITY FLAG 0x%02x", (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2);
ESP_LOGV(TAG, "Status: ENS160 NEWDAT bit 0x%x",
(ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT);
ESP_LOGV(TAG, "Status: ENS160 NEWGPR bit 0x%x",
(ENS160_DATA_STATUS_NEWGPR & (status_value)) == ENS160_DATA_STATUS_NEWGPR);
data_ready = ENS160_DATA_STATUS_NEWDAT & status_value;
this->validity_flag_ = static_cast<ValidityFlag>((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2);
switch (validity_flag_) {
case NORMAL_OPERATION:
if (data_ready != ENS160_DATA_STATUS_NEWDAT) {
ESP_LOGD(TAG, "ENS160 readings unavailable - Normal Operation but readings not ready");
return;
}
break;
case INITIAL_STARTUP:
if (!this->initial_startup_) {
this->initial_startup_ = true;
ESP_LOGI(TAG, "ENS160 readings unavailable - 1 hour startup required after first power on");
}
return;
case WARMING_UP:
if (!this->warming_up_) {
this->warming_up_ = true;
ESP_LOGI(TAG, "ENS160 readings not available yet - Warming up requires 3 minutes");
this->send_env_data_();
}
return;
case INVALID_OUTPUT:
ESP_LOGE(TAG, "ENS160 Invalid Status - No Invalid Output");
this->status_set_warning();
return;
}
// read new data
uint16_t data_eco2;
if (!this->read_bytes(ENS160_REG_DATA_ECO2, reinterpret_cast<uint8_t *>(&data_eco2), 2)) {
ESP_LOGW(TAG, "Error reading eCO2 data register");
this->status_set_warning();
return;
}
if (this->co2_ != nullptr) {
this->co2_->publish_state(data_eco2);
}
uint16_t data_tvoc;
if (!this->read_bytes(ENS160_REG_DATA_TVOC, reinterpret_cast<uint8_t *>(&data_tvoc), 2)) {
ESP_LOGW(TAG, "Error reading TVOC data register");
this->status_set_warning();
return;
}
if (this->tvoc_ != nullptr) {
this->tvoc_->publish_state(data_tvoc);
}
uint8_t data_aqi;
if (!this->read_byte(ENS160_REG_DATA_AQI, &data_aqi)) {
ESP_LOGW(TAG, "Error reading AQI data register");
this->status_set_warning();
return;
}
if (this->aqi_ != nullptr) {
// remove reserved bits, just in case they are used in future
data_aqi = ENS160_DATA_AQI & data_aqi;
this->aqi_->publish_state(data_aqi);
}
this->status_clear_warning();
// set temperature and humidity compensation data
this->send_env_data_();
}
void ENS160Component::send_env_data_() {
if (this->temperature_ == nullptr && this->humidity_ == nullptr)
return;
float temperature = NAN;
if (this->temperature_ != nullptr)
temperature = this->temperature_->state;
if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
ESP_LOGW(TAG, "Invalid external temperature - compensation values not updated");
return;
} else {
ESP_LOGV(TAG, "External temperature compensation: %.1f°C", temperature);
}
float humidity = NAN;
if (this->humidity_ != nullptr)
humidity = this->humidity_->state;
if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
ESP_LOGW(TAG, "Invalid external humidity - compensation values not updated");
return;
} else {
ESP_LOGV(TAG, "External humidity compensation: %.1f%%", humidity);
}
uint16_t t = (uint16_t) ((temperature + 273.15f) * 64.0f);
uint16_t h = (uint16_t) (humidity * 512.0f);
uint8_t data[4];
data[0] = t & 0xff;
data[1] = (t >> 8) & 0xff;
data[2] = h & 0xff;
data[3] = (h >> 8) & 0xff;
if (!this->write_bytes(ENS160_REG_TEMP_IN, data, 4)) {
ESP_LOGE(TAG, "Error writing compensation values");
this->status_set_warning();
return;
}
}
void ENS160Component::dump_config() {
ESP_LOGCONFIG(TAG, "ENS160:");
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGE(TAG, "Communication failed! Is the sensor connected?");
break;
case READ_FAILED:
ESP_LOGE(TAG, "Error reading from register");
break;
case WRITE_FAILED:
ESP_LOGE(TAG, "Error writing to register");
break;
case INVALID_ID:
ESP_LOGE(TAG, "Sensor reported an invalid ID. Is this a ENS160?");
break;
case VALIDITY_INVALID:
ESP_LOGE(TAG, "Invalid Device Status - No valid output");
break;
case STD_OPMODE_FAILED:
ESP_LOGE(TAG, "Device failed to achieve Standard Operating Mode");
break;
case NONE:
ESP_LOGD(TAG, "Setup successful");
break;
}
ESP_LOGI(TAG, "Firmware Version: %d.%d.%d", this->firmware_ver_major_, this->firmware_ver_minor_,
this->firmware_ver_build_);
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "CO2 Sensor:", this->co2_);
LOG_SENSOR(" ", "TVOC Sensor:", this->tvoc_);
LOG_SENSOR(" ", "AQI Sensor:", this->aqi_);
if (this->temperature_ != nullptr && this->humidity_ != nullptr) {
LOG_SENSOR(" ", " Temperature Compensation:", this->temperature_);
LOG_SENSOR(" ", " Humidity Compensation:", this->humidity_);
} else {
ESP_LOGCONFIG(TAG, " Compensation: Not configured");
}
}
} // namespace ens160
} // namespace esphome

View file

@ -0,0 +1,60 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace ens160 {
class ENS160Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor {
public:
void set_co2(sensor::Sensor *co2) { co2_ = co2; }
void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; }
void set_aqi(sensor::Sensor *aqi) { aqi_ = aqi; }
void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
void send_env_data_();
enum ErrorCode {
NONE = 0,
COMMUNICATION_FAILED,
INVALID_ID,
VALIDITY_INVALID,
READ_FAILED,
WRITE_FAILED,
STD_OPMODE_FAILED,
} error_code_{NONE};
enum ValidityFlag {
NORMAL_OPERATION = 0,
WARMING_UP,
INITIAL_STARTUP,
INVALID_OUTPUT,
} validity_flag_;
bool warming_up_{false};
bool initial_startup_{false};
uint8_t firmware_ver_major_{0};
uint8_t firmware_ver_minor_{0};
uint8_t firmware_ver_build_{0};
sensor::Sensor *co2_{nullptr};
sensor::Sensor *tvoc_{nullptr};
sensor::Sensor *aqi_{nullptr};
sensor::Sensor *humidity_{nullptr};
sensor::Sensor *temperature_{nullptr};
};
} // namespace ens160
} // namespace esphome

View file

@ -0,0 +1,88 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_ECO2,
CONF_HUMIDITY,
CONF_ID,
CONF_TEMPERATURE,
CONF_TVOC,
DEVICE_CLASS_AQI,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
ICON_CHEMICAL_WEAPON,
ICON_MOLECULE_CO2,
ICON_RADIATOR,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_BILLION,
UNIT_PARTS_PER_MILLION,
)
CODEOWNERS = ["@vincentscode"]
DEPENDENCIES = ["i2c"]
ens160_ns = cg.esphome_ns.namespace("ens160")
ENS160Component = ens160_ns.class_(
"ENS160Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor
)
CONF_AQI = "aqi"
CONF_COMPENSATION = "compensation"
UNIT_INDEX = "index"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ENS160Component),
cv.Required(CONF_ECO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_AQI): sensor.sensor_schema(
unit_of_measurement=UNIT_INDEX,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_COMPENSATION): cv.Schema(
{
cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor),
cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor),
}
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x53))
)
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)
sens = await sensor.new_sensor(config[CONF_ECO2])
cg.add(var.set_co2(sens))
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))
sens = await sensor.new_sensor(config[CONF_AQI])
cg.add(var.set_aqi(sens))
if CONF_COMPENSATION in config:
compensation_config = config[CONF_COMPENSATION]
sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE])
cg.add(var.set_temperature(sens))
sens = await cg.get_variable(compensation_config[CONF_HUMIDITY])
cg.add(var.set_humidity(sens))

View file

@ -462,7 +462,7 @@ async def to_code(config):
add_extra_script( add_extra_script(
"post", "post",
"post_build2.py", "post_build.py",
os.path.join(os.path.dirname(__file__), "post_build.py.script"), os.path.join(os.path.dirname(__file__), "post_build.py.script"),
) )
@ -497,10 +497,11 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False)
if CONF_PARTITIONS in config:
cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS])
else:
cg.add_platformio_option("board_build.partitions", "partitions.csv") cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
)
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))
@ -639,6 +640,7 @@ def _write_sdkconfig():
# Called by writer.py # Called by writer.py
def copy_files(): def copy_files():
if CORE.using_arduino: if CORE.using_arduino:
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
write_file_if_changed( write_file_if_changed(
CORE.relative_build_path("partitions.csv"), CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv( get_arduino_partition_csv(
@ -647,6 +649,7 @@ def copy_files():
) )
if CORE.using_esp_idf: if CORE.using_esp_idf:
_write_sdkconfig() _write_sdkconfig()
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
write_file_if_changed( write_file_if_changed(
CORE.relative_build_path("partitions.csv"), CORE.relative_build_path("partitions.csv"),
get_idf_partition_csv( get_idf_partition_csv(

View file

@ -3,15 +3,13 @@ from typing import Any
from esphome.const import ( from esphome.const import (
CONF_ID, CONF_ID,
CONF_INPUT,
CONF_INVERTED, CONF_INVERTED,
CONF_MODE, CONF_MODE,
CONF_NUMBER, CONF_NUMBER,
CONF_OPEN_DRAIN, CONF_OPEN_DRAIN,
CONF_OUTPUT, CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
CONF_IGNORE_STRAPPING_WARNING, CONF_IGNORE_STRAPPING_WARNING,
PLATFORM_ESP32,
) )
from esphome import pins from esphome import pins
from esphome.core import CORE from esphome.core import CORE
@ -33,7 +31,6 @@ from .const import (
esp32_ns, esp32_ns,
) )
from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports
@ -42,7 +39,6 @@ from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_support
from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin) ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin)
@ -161,33 +157,22 @@ DRIVE_STRENGTHS = {
} }
gpio_num_t = cg.global_ns.enum("gpio_num_t") gpio_num_t = cg.global_ns.enum("gpio_num_t")
CONF_DRIVE_STRENGTH = "drive_strength" CONF_DRIVE_STRENGTH = "drive_strength"
ESP32_PIN_SCHEMA = cv.All( ESP32_PIN_SCHEMA = cv.All(
pins.gpio_base_schema(ESP32InternalGPIOPin, validate_gpio_pin).extend(
{ {
cv.GenerateID(): cv.declare_id(ESP32InternalGPIOPin),
cv.Required(CONF_NUMBER): validate_gpio_pin,
cv.Optional(CONF_MODE, default={}): cv.Schema(
{
cv.Optional(CONF_INPUT, default=False): cv.boolean,
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean,
cv.Optional(CONF_PULLUP, default=False): cv.boolean,
cv.Optional(CONF_PULLDOWN, default=False): cv.boolean,
}
),
cv.Optional(CONF_INVERTED, default=False): cv.boolean,
cv.Optional(CONF_IGNORE_STRAPPING_WARNING, default=False): cv.boolean, cv.Optional(CONF_IGNORE_STRAPPING_WARNING, default=False): cv.boolean,
cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All( cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All(
cv.float_with_unit("current", "mA", optional_unit=True), cv.float_with_unit("current", "mA", optional_unit=True),
cv.enum(DRIVE_STRENGTHS), cv.enum(DRIVE_STRENGTHS),
), ),
}, }
),
validate_supports, validate_supports,
) )
@pins.PIN_SCHEMA_REGISTRY.register("esp32", ESP32_PIN_SCHEMA) @pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA)
async def esp32_pin_to_code(config): async def esp32_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER] num = config[CONF_NUMBER]

View file

@ -25,6 +25,11 @@ AUTO_LOAD = ["psram"]
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData")
# Triggers
ESP32CameraImageTrigger = esp32_camera_ns.class_(
"ESP32CameraImageTrigger", automation.Trigger.template()
)
ESP32CameraStreamStartTrigger = esp32_camera_ns.class_( ESP32CameraStreamStartTrigger = esp32_camera_ns.class_(
"ESP32CameraStreamStartTrigger", "ESP32CameraStreamStartTrigger",
automation.Trigger.template(), automation.Trigger.template(),
@ -139,6 +144,7 @@ CONF_IDLE_FRAMERATE = "idle_framerate"
# stream trigger # stream trigger
CONF_ON_STREAM_START = "on_stream_start" CONF_ON_STREAM_START = "on_stream_start"
CONF_ON_STREAM_STOP = "on_stream_stop" CONF_ON_STREAM_STOP = "on_stream_stop"
CONF_ON_IMAGE = "on_image"
camera_range_param = cv.int_range(min=-2, max=2) camera_range_param = cv.int_range(min=-2, max=2)
@ -221,6 +227,11 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
), ),
} }
), ),
cv.Optional(CONF_ON_IMAGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger),
}
),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
@ -289,3 +300,9 @@ async def to_code(config):
for conf in config.get(CONF_ON_STREAM_STOP, []): for conf in config.get(CONF_ON_STREAM_STOP, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_IMAGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(ESP32CameraImageData, "image")], conf
)

View file

@ -335,8 +335,8 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) {
} }
/* ---------------- public API (specific) ---------------- */ /* ---------------- public API (specific) ---------------- */
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f) { void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&callback) {
this->new_image_callback_.add(std::move(f)); this->new_image_callback_.add(std::move(callback));
} }
void ESP32Camera::add_stream_start_callback(std::function<void()> &&callback) { void ESP32Camera::add_stream_start_callback(std::function<void()> &&callback) {
this->stream_start_callback_.add(std::move(callback)); this->stream_start_callback_.add(std::move(callback));

View file

@ -86,6 +86,11 @@ class CameraImage {
uint8_t requesters_; uint8_t requesters_;
}; };
struct CameraImageData {
uint8_t *data;
size_t length;
};
/* ---------------- CameraImageReader class ---------------- */ /* ---------------- CameraImageReader class ---------------- */
class CameraImageReader { class CameraImageReader {
public: public:
@ -147,12 +152,12 @@ class ESP32Camera : public Component, public EntityBase {
void dump_config() override; void dump_config() override;
float get_setup_priority() const override; float get_setup_priority() const override;
/* public API (specific) */ /* public API (specific) */
void add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f);
void start_stream(CameraRequester requester); void start_stream(CameraRequester requester);
void stop_stream(CameraRequester requester); void stop_stream(CameraRequester requester);
void request_image(CameraRequester requester); void request_image(CameraRequester requester);
void update_camera_parameters(); void update_camera_parameters();
void add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&callback);
void add_stream_start_callback(std::function<void()> &&callback); void add_stream_start_callback(std::function<void()> &&callback);
void add_stream_stop_callback(std::function<void()> &&callback); void add_stream_stop_callback(std::function<void()> &&callback);
@ -196,7 +201,7 @@ class ESP32Camera : public Component, public EntityBase {
uint8_t stream_requesters_{0}; uint8_t stream_requesters_{0};
QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_get_queue_;
QueueHandle_t framebuffer_return_queue_; QueueHandle_t framebuffer_return_queue_;
CallbackManager<void(std::shared_ptr<CameraImage>)> new_image_callback_; CallbackManager<void(std::shared_ptr<CameraImage>)> new_image_callback_{};
CallbackManager<void()> stream_start_callback_{}; CallbackManager<void()> stream_start_callback_{};
CallbackManager<void()> stream_stop_callback_{}; CallbackManager<void()> stream_stop_callback_{};
@ -207,6 +212,18 @@ class ESP32Camera : public Component, public EntityBase {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern ESP32Camera *global_esp32_camera; extern ESP32Camera *global_esp32_camera;
class ESP32CameraImageTrigger : public Trigger<CameraImageData> {
public:
explicit ESP32CameraImageTrigger(ESP32Camera *parent) {
parent->add_image_callback([this](const std::shared_ptr<esp32_camera::CameraImage> &image) {
CameraImageData camera_image_data{};
camera_image_data.length = image->get_data_length();
camera_image_data.data = image->get_data_buffer();
this->trigger(camera_image_data);
});
}
};
class ESP32CameraStreamStartTrigger : public Trigger<> { class ESP32CameraStreamStartTrigger : public Trigger<> {
public: public:
explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) {

View file

@ -12,6 +12,7 @@ from esphome.const import (
CONF_OUTPUT, CONF_OUTPUT,
CONF_PULLDOWN, CONF_PULLDOWN,
CONF_PULLUP, CONF_PULLUP,
PLATFORM_ESP8266,
) )
from esphome import pins from esphome import pins
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
@ -21,10 +22,8 @@ import esphome.codegen as cg
from . import boards from . import boards
from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin) ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin)
@ -124,6 +123,8 @@ def validate_supports(value):
(True, False, False, False, False), (True, False, False, False, False),
# OUTPUT # OUTPUT
(False, True, False, False, False), (False, True, False, False, False),
# INPUT and OUTPUT, e.g. for i2c
(True, True, False, False, False),
# INPUT_PULLUP # INPUT_PULLUP
(True, False, False, True, False), (True, False, False, True, False),
# INPUT_PULLDOWN_16 # INPUT_PULLDOWN_16
@ -142,21 +143,11 @@ def validate_supports(value):
ESP8266_PIN_SCHEMA = cv.All( ESP8266_PIN_SCHEMA = cv.All(
{ pins.gpio_base_schema(
cv.GenerateID(): cv.declare_id(ESP8266GPIOPin), ESP8266GPIOPin,
cv.Required(CONF_NUMBER): validate_gpio_pin, validate_gpio_pin,
cv.Optional(CONF_MODE, default={}): cv.Schema( modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,),
{
cv.Optional(CONF_ANALOG, default=False): cv.boolean,
cv.Optional(CONF_INPUT, default=False): cv.boolean,
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean,
cv.Optional(CONF_PULLUP, default=False): cv.boolean,
cv.Optional(CONF_PULLDOWN, default=False): cv.boolean,
}
), ),
cv.Optional(CONF_INVERTED, default=False): cv.boolean,
},
validate_supports, validate_supports,
) )
@ -167,7 +158,7 @@ class PinInitialState:
level: int = 255 level: int = 255
@pins.PIN_SCHEMA_REGISTRY.register("esp8266", ESP8266_PIN_SCHEMA) @pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP8266, ESP8266_PIN_SCHEMA)
async def esp8266_pin_to_code(config): async def esp8266_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER] num = config[CONF_NUMBER]

View file

@ -18,6 +18,7 @@ from esphome.const import (
CONF_ON_SPEED_SET, CONF_ON_SPEED_SET,
CONF_ON_TURN_OFF, CONF_ON_TURN_OFF,
CONF_ON_TURN_ON, CONF_ON_TURN_ON,
CONF_ON_PRESET_SET,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_DIRECTION, CONF_DIRECTION,
CONF_RESTORE_MODE, CONF_RESTORE_MODE,
@ -57,6 +58,9 @@ CycleSpeedAction = fan_ns.class_("CycleSpeedAction", automation.Action)
FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template())
FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template())
FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template()) FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template())
FanPresetSetTrigger = fan_ns.class_(
"FanPresetSetTrigger", automation.Trigger.template()
)
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template()) FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template())
@ -101,9 +105,46 @@ FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).exte
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger),
} }
), ),
cv.Optional(CONF_ON_PRESET_SET): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanPresetSetTrigger),
}
),
} }
) )
_PRESET_MODES_SCHEMA = cv.All(
cv.ensure_list(cv.string_strict),
cv.Length(min=1),
)
def validate_preset_modes(value):
# Check against defined schema
value = _PRESET_MODES_SCHEMA(value)
# Ensure preset names are unique
errors = []
presets = set()
for i, preset in enumerate(value):
# If name does not exist yet add it
if preset not in presets:
presets.add(preset)
continue
# Otherwise it's an error
errors.append(
cv.Invalid(
f"Found duplicate preset name '{preset}'. Presets must have unique names.",
[i],
)
)
if errors:
raise cv.MultipleInvalid(errors)
return value
async def setup_fan_core_(var, config): async def setup_fan_core_(var, config):
await setup_entity(var, config) await setup_entity(var, config)
@ -154,6 +195,9 @@ async def setup_fan_core_(var, config):
for conf in config.get(CONF_ON_SPEED_SET, []): for conf in config.get(CONF_ON_SPEED_SET, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_PRESET_SET, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
async def register_fan(var, config): async def register_fan(var, config):

View file

@ -165,5 +165,23 @@ class FanSpeedSetTrigger : public Trigger<> {
int last_speed_; int last_speed_;
}; };
class FanPresetSetTrigger : public Trigger<> {
public:
FanPresetSetTrigger(Fan *state) {
state->add_on_state_callback([this, state]() {
auto preset_mode = state->preset_mode;
auto should_trigger = preset_mode != this->last_preset_mode_;
this->last_preset_mode_ = preset_mode;
if (should_trigger) {
this->trigger();
}
});
this->last_preset_mode_ = state->preset_mode;
}
protected:
std::string last_preset_mode_;
};
} // namespace fan } // namespace fan
} // namespace esphome } // namespace esphome

View file

@ -32,9 +32,12 @@ void FanCall::perform() {
if (this->direction_.has_value()) { if (this->direction_.has_value()) {
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_)));
} }
if (!this->preset_mode_.empty()) {
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_.c_str());
}
this->parent_.control(*this); this->parent_.control(*this);
} }
void FanCall::validate_() { void FanCall::validate_() {
auto traits = this->parent_.get_traits(); auto traits = this->parent_.get_traits();
@ -62,6 +65,15 @@ void FanCall::validate_() {
ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str()); ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str());
this->direction_.reset(); this->direction_.reset();
} }
if (!this->preset_mode_.empty()) {
const auto &preset_modes = traits.supported_preset_modes();
if (preset_modes.find(this->preset_mode_) == preset_modes.end()) {
ESP_LOGW(TAG, "'%s' - This fan does not support preset mode '%s'!", this->parent_.get_name().c_str(),
this->preset_mode_.c_str());
this->preset_mode_.clear();
}
}
} }
FanCall FanRestoreState::to_call(Fan &fan) { FanCall FanRestoreState::to_call(Fan &fan) {
@ -70,6 +82,14 @@ FanCall FanRestoreState::to_call(Fan &fan) {
call.set_oscillating(this->oscillating); call.set_oscillating(this->oscillating);
call.set_speed(this->speed); call.set_speed(this->speed);
call.set_direction(this->direction); call.set_direction(this->direction);
if (fan.get_traits().supports_preset_modes()) {
// Use stored preset index to get preset name
const auto &preset_modes = fan.get_traits().supported_preset_modes();
if (this->preset_mode < preset_modes.size()) {
call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode));
}
}
return call; return call;
} }
void FanRestoreState::apply(Fan &fan) { void FanRestoreState::apply(Fan &fan) {
@ -77,6 +97,14 @@ void FanRestoreState::apply(Fan &fan) {
fan.oscillating = this->oscillating; fan.oscillating = this->oscillating;
fan.speed = this->speed; fan.speed = this->speed;
fan.direction = this->direction; fan.direction = this->direction;
if (fan.get_traits().supports_preset_modes()) {
// Use stored preset index to get preset name
const auto &preset_modes = fan.get_traits().supported_preset_modes();
if (this->preset_mode < preset_modes.size()) {
fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode);
}
}
fan.publish_state(); fan.publish_state();
} }
@ -100,7 +128,9 @@ void Fan::publish_state() {
if (traits.supports_direction()) { if (traits.supports_direction()) {
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction)));
} }
if (traits.supports_preset_modes() && !this->preset_mode.empty()) {
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode.c_str());
}
this->state_callback_.call(); this->state_callback_.call();
this->save_state_(); this->save_state_();
} }
@ -143,20 +173,36 @@ void Fan::save_state_() {
state.oscillating = this->oscillating; state.oscillating = this->oscillating;
state.speed = this->speed; state.speed = this->speed;
state.direction = this->direction; state.direction = this->direction;
if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) {
const auto &preset_modes = this->get_traits().supported_preset_modes();
// Store index of current preset mode
auto preset_iterator = preset_modes.find(this->preset_mode);
if (preset_iterator != preset_modes.end())
state.preset_mode = std::distance(preset_modes.begin(), preset_iterator);
}
this->rtc_.save(&state); this->rtc_.save(&state);
} }
void Fan::dump_traits_(const char *tag, const char *prefix) { void Fan::dump_traits_(const char *tag, const char *prefix) {
if (this->get_traits().supports_speed()) { auto traits = this->get_traits();
if (traits.supports_speed()) {
ESP_LOGCONFIG(tag, "%s Speed: YES", prefix); ESP_LOGCONFIG(tag, "%s Speed: YES", prefix);
ESP_LOGCONFIG(tag, "%s Speed count: %d", prefix, this->get_traits().supported_speed_count()); ESP_LOGCONFIG(tag, "%s Speed count: %d", prefix, traits.supported_speed_count());
} }
if (this->get_traits().supports_oscillation()) { if (traits.supports_oscillation()) {
ESP_LOGCONFIG(tag, "%s Oscillation: YES", prefix); ESP_LOGCONFIG(tag, "%s Oscillation: YES", prefix);
} }
if (this->get_traits().supports_direction()) { if (traits.supports_direction()) {
ESP_LOGCONFIG(tag, "%s Direction: YES", prefix); ESP_LOGCONFIG(tag, "%s Direction: YES", prefix);
} }
if (traits.supports_preset_modes()) {
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
for (const std::string &s : traits.supported_preset_modes())
ESP_LOGCONFIG(tag, "%s - %s", prefix, s.c_str());
}
} }
} // namespace fan } // namespace fan

View file

@ -72,6 +72,11 @@ class FanCall {
return *this; return *this;
} }
optional<FanDirection> get_direction() const { return this->direction_; } optional<FanDirection> get_direction() const { return this->direction_; }
FanCall &set_preset_mode(const std::string &preset_mode) {
this->preset_mode_ = preset_mode;
return *this;
}
std::string get_preset_mode() const { return this->preset_mode_; }
void perform(); void perform();
@ -83,6 +88,7 @@ class FanCall {
optional<bool> oscillating_; optional<bool> oscillating_;
optional<int> speed_; optional<int> speed_;
optional<FanDirection> direction_{}; optional<FanDirection> direction_{};
std::string preset_mode_{};
}; };
struct FanRestoreState { struct FanRestoreState {
@ -90,6 +96,7 @@ struct FanRestoreState {
int speed; int speed;
bool oscillating; bool oscillating;
FanDirection direction; FanDirection direction;
uint8_t preset_mode;
/// Convert this struct to a fan call that can be performed. /// Convert this struct to a fan call that can be performed.
FanCall to_call(Fan &fan); FanCall to_call(Fan &fan);
@ -107,6 +114,8 @@ class Fan : public EntityBase {
int speed{0}; int speed{0};
/// The current direction of the fan /// The current direction of the fan
FanDirection direction{FanDirection::FORWARD}; FanDirection direction{FanDirection::FORWARD};
// The current preset mode of the fan
std::string preset_mode{};
FanCall turn_on(); FanCall turn_on();
FanCall turn_off(); FanCall turn_off();

View file

@ -1,3 +1,6 @@
#include <set>
#include <utility>
#pragma once #pragma once
namespace esphome { namespace esphome {
@ -25,12 +28,19 @@ class FanTraits {
bool supports_direction() const { return this->direction_; } bool supports_direction() const { return this->direction_; }
/// Set whether this fan supports changing direction /// Set whether this fan supports changing direction
void set_direction(bool direction) { this->direction_ = direction; } void set_direction(bool direction) { this->direction_ = direction; }
/// Return the preset modes supported by the fan.
std::set<std::string> supported_preset_modes() const { return this->preset_modes_; }
/// Set the preset modes supported by the fan.
void set_supported_preset_modes(const std::set<std::string> &preset_modes) { this->preset_modes_ = preset_modes; }
/// Return if preset modes are supported
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
protected: protected:
bool oscillation_{false}; bool oscillation_{false};
bool speed_{false}; bool speed_{false};
bool direction_{false}; bool direction_{false};
int speed_count_{}; int speed_count_{};
std::set<std::string> preset_modes_{};
}; };
} // namespace fan } // namespace fan

View file

@ -67,13 +67,13 @@ def validate_pillow_installed(value):
except ImportError as err: except ImportError as err:
raise cv.Invalid( raise cv.Invalid(
"Please install the pillow python package to use this feature. " "Please install the pillow python package to use this feature. "
'(pip install "pillow==10.0.1")' '(pip install "pillow==10.1.0")'
) from err ) from err
if version.parse(PIL.__version__) != version.parse("10.0.1"): if version.parse(PIL.__version__) != version.parse("10.1.0"):
raise cv.Invalid( raise cv.Invalid(
"Please update your pillow installation to 10.0.1. " "Please update your pillow installation to 10.1.0. "
'(pip install "pillow==10.0.1")' '(pip install "pillow==10.1.0")'
) )
return value return value

View file

@ -0,0 +1,6 @@
import esphome.codegen as cg
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["i2c"]
ft5x06_ns = cg.esphome_ns.namespace("ft5x06")

View file

@ -0,0 +1,26 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, touchscreen
from esphome.const import CONF_ID
from .. import ft5x06_ns
FT5x06ButtonListener = ft5x06_ns.class_("FT5x06ButtonListener")
FT5x06Touchscreen = ft5x06_ns.class_(
"FT5x06Touchscreen",
touchscreen.Touchscreen,
cg.Component,
i2c.I2CDevice,
)
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(FT5x06Touchscreen),
}
).extend(i2c.i2c_device_schema(0x48))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await i2c.register_i2c_device(var, config)
await touchscreen.register_touchscreen(var, config)

View file

@ -0,0 +1,124 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ft5x06 {
static const char *const TAG = "ft5x06.touchscreen";
enum VendorId {
FT5X06_ID_UNKNOWN = 0,
FT5X06_ID_1 = 0x51,
FT5X06_ID_2 = 0x11,
FT5X06_ID_3 = 0xCD,
};
enum FTCmd : uint8_t {
FT5X06_MODE_REG = 0x00,
FT5X06_ORIGIN_REG = 0x08,
FT5X06_RESOLUTION_REG = 0x0C,
FT5X06_VENDOR_ID_REG = 0xA8,
FT5X06_TD_STATUS = 0x02,
FT5X06_TOUCH_DATA = 0x03,
FT5X06_I_MODE = 0xA4,
FT5X06_TOUCH_MAX = 0x4C,
};
enum FTMode : uint8_t {
FT5X06_OP_MODE = 0,
FT5X06_SYSINFO_MODE = 0x10,
FT5X06_TEST_MODE = 0x40,
};
static const size_t MAX_TOUCHES = 5; // max number of possible touches reported
class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override {
esph_log_config(TAG, "Setting up FT5x06 Touchscreen...");
// wait 200ms after reset.
this->set_timeout(200, [this] { this->continue_setup_(); });
}
void continue_setup_(void) {
uint8_t data[4];
if (!this->set_mode_(FT5X06_OP_MODE))
return;
if (!this->err_check_(this->read_register(FT5X06_VENDOR_ID_REG, data, 1), "Read Vendor ID"))
return;
switch (data[0]) {
case FT5X06_ID_1:
case FT5X06_ID_2:
case FT5X06_ID_3:
this->vendor_id_ = (VendorId) data[0];
esph_log_d(TAG, "Read vendor ID 0x%X", data[0]);
break;
default:
esph_log_e(TAG, "Unknown vendor ID 0x%X", data[0]);
this->mark_failed();
return;
}
// reading the chip registers to get max x/y does not seem to work.
this->x_raw_max_ = this->display_->get_width();
this->y_raw_max_ = this->display_->get_height();
esph_log_config(TAG, "FT5x06 Touchscreen setup complete");
}
void update_touches() override {
uint8_t touch_cnt;
uint8_t data[MAX_TOUCHES][6];
if (!this->read_byte(FT5X06_TD_STATUS, &touch_cnt) || touch_cnt > MAX_TOUCHES) {
esph_log_w(TAG, "Failed to read status");
return;
}
if (touch_cnt == 0)
return;
if (!this->read_bytes(FT5X06_TOUCH_DATA, (uint8_t *) data, touch_cnt * 6)) {
esph_log_w(TAG, "Failed to read touch data");
return;
}
for (uint8_t i = 0; i != touch_cnt; i++) {
uint8_t status = data[i][0] >> 6;
uint8_t id = data[i][2] >> 3;
uint16_t x = encode_uint16(data[i][0] & 0x0F, data[i][1]);
uint16_t y = encode_uint16(data[i][2] & 0xF, data[i][3]);
esph_log_d(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y);
if (status == 0 || status == 2) {
this->set_raw_touch_position_(id, x, y);
}
}
}
void dump_config() override {
esph_log_config(TAG, "FT5x06 Touchscreen:");
esph_log_config(TAG, " Address: 0x%02X", this->address_);
esph_log_config(TAG, " Vendor ID: 0x%X", (int) this->vendor_id_);
}
protected:
bool err_check_(i2c::ErrorCode err, const char *msg) {
if (err != i2c::ERROR_OK) {
this->mark_failed();
esph_log_e(TAG, "%s failed - err 0x%X", msg, err);
return false;
}
return true;
}
bool set_mode_(FTMode mode) {
return this->err_check_(this->write_register(FT5X06_MODE_REG, (uint8_t *) &mode, 1), "Set mode");
}
VendorId vendor_id_{FT5X06_ID_UNKNOWN};
};
} // namespace ft5x06
} // namespace esphome

View file

@ -0,0 +1 @@
CODEOWNERS = ["@gpambrozio"]

View file

@ -0,0 +1,99 @@
/**************************************************************************/
/*!
Author: Gustavo Ambrozio
Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U)
*/
/**************************************************************************/
#include "ft63x6.h"
#include "esphome/core/log.h"
// Registers
// Reference: https://focuslcds.com/content/FT6236.pdf
namespace esphome {
namespace ft63x6 {
static const uint8_t FT63X6_ADDR_TOUCH_COUNT = 0x02;
static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05;
static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03;
static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05;
static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B;
static const uint8_t FT63X6_ADDR_TOUCH2_X = 0x09;
static const uint8_t FT63X6_ADDR_TOUCH2_Y = 0x0B;
static const char *const TAG = "FT63X6Touchscreen";
void FT63X6Touchscreen::setup() {
ESP_LOGCONFIG(TAG, "Setting up FT63X6Touchscreen Touchscreen...");
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
}
this->hard_reset_();
// Get touch resolution
this->x_raw_max_ = 320;
this->y_raw_max_ = 480;
}
void FT63X6Touchscreen::update_touches() {
int touch_count = this->read_touch_count_();
if (touch_count == 0) {
return;
}
uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID); // id1 = 0 or 1
int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X);
int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y);
this->set_raw_touch_position_(touch_id, x, y);
if (touch_count >= 2) {
touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID); // id2 = 0 or 1(~id1 & 0x01)
x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X);
y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y);
this->set_raw_touch_position_(touch_id, x, y);
}
}
void FT63X6Touchscreen::hard_reset_() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false);
delay(10);
this->reset_pin_->digital_write(true);
}
}
void FT63X6Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG, "FT63X6 Touchscreen:");
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
}
uint8_t FT63X6Touchscreen::read_touch_count_() { return this->read_byte_(FT63X6_ADDR_TOUCH_COUNT); }
// Touch functions
uint16_t FT63X6Touchscreen::read_touch_coordinate_(uint8_t coordinate) {
uint8_t read_buf[2];
read_buf[0] = this->read_byte_(coordinate);
read_buf[1] = this->read_byte_(coordinate + 1);
return ((read_buf[0] & 0x0f) << 8) | read_buf[1];
}
uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t id_address) { return this->read_byte_(id_address) >> 4; }
uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) {
uint8_t byte = 0;
this->read_byte(addr, &byte);
return byte;
}
} // namespace ft63x6
} // namespace esphome

View file

@ -0,0 +1,41 @@
/**************************************************************************/
/*!
Author: Gustavo Ambrozio
Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U)
*/
/**************************************************************************/
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
namespace esphome {
namespace ft63x6 {
using namespace touchscreen;
class FT63X6Touchscreen : public Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
protected:
void hard_reset_();
uint8_t read_byte_(uint8_t addr);
void update_touches() override;
InternalGPIOPin *interrupt_pin_{nullptr};
GPIOPin *reset_pin_{nullptr};
uint8_t read_touch_count_();
uint16_t read_touch_coordinate_(uint8_t coordinate);
uint8_t read_touch_id_(uint8_t id_address);
};
} // namespace ft63x6
} // namespace esphome

View file

@ -0,0 +1,44 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import i2c, touchscreen
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
CODEOWNERS = ["@gpambrozio"]
DEPENDENCIES = ["i2c"]
ft6336u_ns = cg.esphome_ns.namespace("ft63x6")
FT63X6Touchscreen = ft6336u_ns.class_(
"FT63X6Touchscreen",
touchscreen.Touchscreen,
i2c.I2CDevice,
)
CONF_FT63X6_ID = "ft63x6_id"
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(FT63X6Touchscreen),
cv.Optional(CONF_INTERRUPT_PIN): cv.All(
pins.internal_gpio_input_pin_schema
),
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
).extend(i2c.i2c_device_schema(0x38))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin_config := config.get(CONF_INTERRUPT_PIN):
interrupt_pin = await cg.gpio_pin_expression(interrupt_pin_config)
cg.add(var.set_interrupt_pin(interrupt_pin))
if reset_pin_config := config.get(CONF_RESET_PIN):
reset_pin = await cg.gpio_pin_expression(reset_pin_config)
cg.add(var.set_reset_pin(reset_pin))

View file

@ -0,0 +1,96 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import display, font, color
from esphome.const import CONF_ID, CONF_TRIGGER_ID
from esphome import automation, core
from esphome.components.display_menu_base import (
DISPLAY_MENU_BASE_SCHEMA,
DisplayMenuComponent,
display_menu_to_code,
)
CONF_DISPLAY = "display"
CONF_FONT = "font"
CONF_MENU_ITEM_VALUE = "menu_item_value"
CONF_FOREGROUND_COLOR = "foreground_color"
CONF_BACKGROUND_COLOR = "background_color"
CONF_ON_REDRAW = "on_redraw"
graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu")
GraphicalDisplayMenu = graphical_display_menu_ns.class_(
"GraphicalDisplayMenu", DisplayMenuComponent
)
GraphicalDisplayMenuConstPtr = GraphicalDisplayMenu.operator("ptr").operator("const")
MenuItemValueArguments = graphical_display_menu_ns.struct("MenuItemValueArguments")
MenuItemValueArgumentsConstPtr = MenuItemValueArguments.operator("ptr").operator(
"const"
)
GraphicalDisplayMenuOnRedrawTrigger = graphical_display_menu_ns.class_(
"GraphicalDisplayMenuOnRedrawTrigger", automation.Trigger
)
CODEOWNERS = ["@MrMDavidson"]
AUTO_LOAD = ["display_menu_base"]
CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(GraphicalDisplayMenu),
cv.Optional(CONF_DISPLAY): cv.use_id(display.DisplayBuffer),
cv.Required(CONF_FONT): cv.use_id(font.Font),
cv.Optional(CONF_MENU_ITEM_VALUE): cv.templatable(cv.string),
cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct),
cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct),
cv.Optional(CONF_ON_REDRAW): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
GraphicalDisplayMenuOnRedrawTrigger
)
}
),
}
)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if display_config := config.get(CONF_DISPLAY):
drawing_display = await cg.get_variable(display_config)
cg.add(var.set_display(drawing_display))
menu_font = await cg.get_variable(config[CONF_FONT])
cg.add(var.set_font(menu_font))
if (menu_item_value_config := config.get(CONF_MENU_ITEM_VALUE, None)) is not None:
if isinstance(menu_item_value_config, core.Lambda):
template_ = await cg.templatable(
menu_item_value_config,
[(MenuItemValueArgumentsConstPtr, "it")],
cg.std_string,
)
cg.add(var.set_menu_item_value(template_))
else:
cg.add(var.set_menu_item_value(menu_item_value_config))
if foreground_color_config := config.get(CONF_FOREGROUND_COLOR):
foreground_color = await cg.get_variable(foreground_color_config)
cg.add(var.set_foreground_color(foreground_color))
if background_color_config := config.get(CONF_BACKGROUND_COLOR):
background_color = await cg.get_variable(background_color_config)
cg.add(var.set_background_color(background_color))
for conf in config.get(CONF_ON_REDRAW, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(GraphicalDisplayMenuConstPtr, "it")], conf
)
await display_menu_to_code(var, config)
cg.add_define("USE_GRAPHICAL_DISPLAY_MENU")

View file

@ -0,0 +1,243 @@
#include "graphical_display_menu.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cstdlib>
#include "esphome/components/display/display.h"
namespace esphome {
namespace graphical_display_menu {
static const char *const TAG = "graphical_display_menu";
void GraphicalDisplayMenu::setup() {
if (this->display_ != nullptr) {
display::display_writer_t writer = [this](display::Display &it) { this->draw_menu(); };
this->display_page_ = make_unique<display::DisplayPage>(writer);
}
if (!this->menu_item_value_.has_value()) {
this->menu_item_value_ = [](const MenuItemValueArguments *it) {
std::string label = " ";
if (it->is_item_selected && it->is_menu_editing) {
label.append(">");
label.append(it->item->get_value_text());
label.append("<");
} else {
label.append("(");
label.append(it->item->get_value_text());
label.append(")");
}
return label;
};
}
display_menu_base::DisplayMenuComponent::setup();
}
void GraphicalDisplayMenu::dump_config() {
ESP_LOGCONFIG(TAG, "Graphical Display Menu");
ESP_LOGCONFIG(TAG, "Has Display: %s", YESNO(this->display_ != nullptr));
ESP_LOGCONFIG(TAG, "Popup Mode: %s", YESNO(this->display_ != nullptr));
ESP_LOGCONFIG(TAG, "Advanced Drawing Mode: %s", YESNO(this->display_ == nullptr));
ESP_LOGCONFIG(TAG, "Has Font: %s", YESNO(this->font_ != nullptr));
ESP_LOGCONFIG(TAG, "Mode: %s", this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick");
ESP_LOGCONFIG(TAG, "Active: %s", YESNO(this->active_));
ESP_LOGCONFIG(TAG, "Menu items:");
for (size_t i = 0; i < this->displayed_item_->items_size(); i++) {
auto *item = this->displayed_item_->get_item(i);
ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(),
LOG_STR_ARG(display_menu_base::menu_item_type_to_string(item->get_type())),
YESNO(item->get_immediate_edit()));
}
}
void GraphicalDisplayMenu::set_display(display::Display *display) { this->display_ = display; }
void GraphicalDisplayMenu::set_font(display::BaseFont *font) { this->font_ = font; }
void GraphicalDisplayMenu::set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; }
void GraphicalDisplayMenu::set_background_color(Color background_color) { this->background_color_ = background_color; }
void GraphicalDisplayMenu::on_before_show() {
if (this->display_ != nullptr) {
this->previous_display_page_ = this->display_->get_active_page();
this->display_->show_page(this->display_page_.get());
this->display_->clear();
} else {
this->update();
}
}
void GraphicalDisplayMenu::on_before_hide() {
if (this->previous_display_page_ != nullptr) {
this->display_->show_page((display::DisplayPage *) this->previous_display_page_);
this->display_->clear();
this->update();
this->previous_display_page_ = nullptr;
} else {
this->update();
}
}
void GraphicalDisplayMenu::draw_and_update() {
this->update();
// If we're in advanced drawing mode we won't have a display and will instead require the update callback to do
// our drawing
if (this->display_ != nullptr) {
draw_menu();
}
}
void GraphicalDisplayMenu::draw_menu() {
if (this->display_ == nullptr) {
ESP_LOGE(TAG, "draw_menu() called without a display_. This is only available when using the menu in pop up mode");
return;
}
display::Rect bounds(0, 0, this->display_->get_width(), this->display_->get_height());
this->draw_menu_internal_(this->display_, &bounds);
}
void GraphicalDisplayMenu::draw(display::Display *display, const display::Rect *bounds) {
this->draw_menu_internal_(display, bounds);
}
void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const display::Rect *bounds) {
int total_height = 0;
int y_padding = 2;
bool scroll_menu_items = false;
std::vector<display::Rect> menu_dimensions;
int number_items_fit_to_screen = 0;
const int max_item_index = this->displayed_item_->items_size() - 1;
for (size_t i = 0; i <= max_item_index; i++) {
const auto *item = this->displayed_item_->get_item(i);
const bool selected = i == this->cursor_index_;
const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected);
menu_dimensions.push_back(item_dimensions);
total_height += item_dimensions.h + (i == 0 ? 0 : y_padding);
if (total_height <= bounds->h) {
number_items_fit_to_screen++;
} else {
// Scroll the display if the selected item or the item immediately after it overflows
if ((selected) || (i == this->cursor_index_ + 1)) {
scroll_menu_items = true;
}
}
}
// Determine what items to draw
int first_item_index = 0;
int last_item_index = max_item_index;
if (number_items_fit_to_screen <= 1) {
// If only one item can fit to the bounds draw the current cursor item
last_item_index = std::min(last_item_index, this->cursor_index_ + 1);
first_item_index = this->cursor_index_;
} else {
if (scroll_menu_items) {
// Attempt to draw the item after the current item (+1 for equality check in the draw loop)
last_item_index = std::min(last_item_index, this->cursor_index_ + 1);
// Go back through the measurements to determine how many prior items we can fit
int height_left_to_use = bounds->h;
for (int i = last_item_index; i >= 0; i--) {
const display::Rect item_dimensions = menu_dimensions[i];
height_left_to_use -= (item_dimensions.h + y_padding);
if (height_left_to_use <= 0) {
// Ran out of space - this is our first item to draw
first_item_index = i;
break;
}
}
const int items_to_draw = last_item_index - first_item_index;
// Dont't draw last item partially if it is the selected item
if ((this->cursor_index_ == last_item_index) && (number_items_fit_to_screen <= items_to_draw) &&
(first_item_index < max_item_index)) {
first_item_index++;
}
}
}
// Render the items into the view port
display->start_clipping(*bounds);
int y_offset = bounds->y;
for (size_t i = first_item_index; i <= last_item_index; i++) {
const auto *item = this->displayed_item_->get_item(i);
const bool selected = i == this->cursor_index_;
display::Rect dimensions = menu_dimensions[i];
dimensions.y = y_offset;
dimensions.x = bounds->x;
this->draw_item(display, item, &dimensions, selected);
y_offset = dimensions.y + dimensions.h + y_padding;
}
display->end_clipping();
}
display::Rect GraphicalDisplayMenu::measure_item(display::Display *display, const display_menu_base::MenuItem *item,
const display::Rect *bounds, const bool selected) {
display::Rect dimensions(0, 0, 0, 0);
if (selected) {
// TODO: Support selection glyph
dimensions.w += 0;
dimensions.h += 0;
}
std::string label = item->get_text();
if (item->has_value()) {
// Append to label
MenuItemValueArguments args(item, selected, this->editing_);
label.append(this->menu_item_value_.value(&args));
}
int x1;
int y1;
int width;
int height;
display->get_text_bounds(0, 0, label.c_str(), this->font_, display::TextAlign::TOP_LEFT, &x1, &y1, &width, &height);
dimensions.w = std::min((int16_t) width, bounds->w);
dimensions.h = std::min((int16_t) height, bounds->h);
return dimensions;
}
inline void GraphicalDisplayMenu::draw_item(display::Display *display, const display_menu_base::MenuItem *item,
const display::Rect *bounds, const bool selected) {
const auto background_color = selected ? this->foreground_color_ : this->background_color_;
const auto foreground_color = selected ? this->background_color_ : this->foreground_color_;
// int background_width = std::max(bounds->width, available_width);
int background_width = bounds->w;
if (selected) {
display->filled_rectangle(bounds->x, bounds->y, background_width, bounds->h, background_color);
}
std::string label = item->get_text();
if (item->has_value()) {
MenuItemValueArguments args(item, selected, this->editing_);
label.append(this->menu_item_value_.value(&args));
}
display->print(bounds->x, bounds->y, this->font_, foreground_color, display::TextAlign::TOP_LEFT, label.c_str());
}
void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, const uint8_t row, const bool selected) {
ESP_LOGE(TAG, "draw_item(MenuItem *item, uint8_t row, bool selected) called. The graphical_display_menu specific "
"draw_item should be called.");
}
void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(); }
} // namespace graphical_display_menu
} // namespace esphome

View file

@ -0,0 +1,84 @@
#pragma once
#include "esphome/core/color.h"
#include "esphome/components/display_menu_base/display_menu_base.h"
#include "esphome/components/display_menu_base/menu_item.h"
#include "esphome/core/automation.h"
#include <cstdlib>
namespace esphome {
// forward declare from display namespace
namespace display {
class Display;
class DisplayPage;
class BaseFont;
class Rect;
} // namespace display
namespace graphical_display_menu {
const Color COLOR_ON(255, 255, 255, 255);
const Color COLOR_OFF(0, 0, 0, 0);
struct MenuItemValueArguments {
MenuItemValueArguments(const display_menu_base::MenuItem *item, bool is_item_selected, bool is_menu_editing) {
this->item = item;
this->is_item_selected = is_item_selected;
this->is_menu_editing = is_menu_editing;
}
const display_menu_base::MenuItem *item;
bool is_item_selected;
bool is_menu_editing;
};
class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent {
public:
void setup() override;
void dump_config() override;
void set_display(display::Display *display);
void set_font(display::BaseFont *font);
template<typename V> void set_menu_item_value(V menu_item_value) { this->menu_item_value_ = menu_item_value; }
void set_foreground_color(Color foreground_color);
void set_background_color(Color background_color);
void add_on_redraw_callback(std::function<void()> &&cb) { this->on_redraw_callbacks_.add(std::move(cb)); }
void draw(display::Display *display, const display::Rect *bounds);
protected:
void draw_and_update() override;
void draw_menu() override;
void draw_menu_internal_(display::Display *display, const display::Rect *bounds);
void draw_item(const display_menu_base::MenuItem *item, uint8_t row, bool selected) override;
virtual display::Rect measure_item(display::Display *display, const display_menu_base::MenuItem *item,
const display::Rect *bounds, bool selected);
virtual void draw_item(display::Display *display, const display_menu_base::MenuItem *item,
const display::Rect *bounds, bool selected);
void update() override;
void on_before_show() override;
void on_before_hide() override;
std::unique_ptr<display::DisplayPage> display_page_{nullptr};
const display::DisplayPage *previous_display_page_{nullptr};
display::Display *display_{nullptr};
display::BaseFont *font_{nullptr};
TemplatableValue<std::string, const MenuItemValueArguments *> menu_item_value_;
Color foreground_color_{COLOR_ON};
Color background_color_{COLOR_OFF};
CallbackManager<void()> on_redraw_callbacks_{};
};
class GraphicalDisplayMenuOnRedrawTrigger : public Trigger<const GraphicalDisplayMenu *> {
public:
explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) {
parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); });
}
};
} // namespace graphical_display_menu
} // namespace esphome

View file

@ -0,0 +1,6 @@
import esphome.codegen as cg
CODEOWNERS = ["@jesserockz", "@clydebarrow"]
DEPENDENCIES = ["i2c"]
gt911_ns = cg.esphome_ns.namespace("gt911")

View file

@ -0,0 +1,31 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import CONF_INDEX
from .. import gt911_ns
from ..touchscreen import GT911Touchscreen, GT911ButtonListener
CONF_GT911_ID = "gt911_id"
GT911Button = gt911_ns.class_(
"GT911Button",
binary_sensor.BinarySensor,
cg.Component,
GT911ButtonListener,
cg.Parented.template(GT911Touchscreen),
)
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(GT911Button).extend(
{
cv.GenerateID(CONF_GT911_ID): cv.use_id(GT911Touchscreen),
cv.Optional(CONF_INDEX, default=0): cv.int_range(min=0, max=3),
}
)
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_GT911_ID])
cg.add(var.set_index(config[CONF_INDEX]))

View file

@ -0,0 +1,27 @@
#include "gt911_button.h"
#include "esphome/core/log.h"
namespace esphome {
namespace gt911 {
static const char *const TAG = "GT911.binary_sensor";
void GT911Button::setup() {
this->parent_->register_button_listener(this);
this->publish_initial_state(false);
}
void GT911Button::dump_config() {
LOG_BINARY_SENSOR("", "GT911 Button", this);
ESP_LOGCONFIG(TAG, " Index: %u", this->index_);
}
void GT911Button::update_button(uint8_t index, bool state) {
if (index != this->index_)
return;
this->publish_state(state);
}
} // namespace gt911
} // namespace esphome

View file

@ -0,0 +1,28 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/gt911/touchscreen/gt911_touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace gt911 {
class GT911Button : public binary_sensor::BinarySensor,
public Component,
public GT911ButtonListener,
public Parented<GT911Touchscreen> {
public:
void setup() override;
void dump_config() override;
void set_index(uint8_t index) { this->index_ = index; }
void update_button(uint8_t index, bool state) override;
protected:
uint8_t index_;
};
} // namespace gt911
} // namespace esphome

View file

@ -0,0 +1,31 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import i2c, touchscreen
from esphome.const import CONF_INTERRUPT_PIN, CONF_ID
from .. import gt911_ns
GT911ButtonListener = gt911_ns.class_("GT911ButtonListener")
GT911Touchscreen = gt911_ns.class_(
"GT911Touchscreen",
touchscreen.Touchscreen,
i2c.I2CDevice,
)
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(GT911Touchscreen),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
).extend(i2c.i2c_device_schema(0x5D))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))

View file

@ -0,0 +1,111 @@
#include "gt911_touchscreen.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace gt911 {
static const char *const TAG = "gt911.touchscreen";
static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E};
static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00};
static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F};
static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D};
static const uint8_t GET_MAX_VALUES[2] = {0x80, 0x48};
static const size_t MAX_TOUCHES = 5; // max number of possible touches reported
#define ERROR_CHECK(err) \
if ((err) != i2c::ERROR_OK) { \
ESP_LOGE(TAG, "Failed to communicate!"); \
this->status_set_warning(); \
return; \
}
void GT911Touchscreen::setup() {
i2c::ErrorCode err;
ESP_LOGCONFIG(TAG, "Setting up GT911 Touchscreen...");
// check the configuration of the int line.
uint8_t data[4];
err = this->write(GET_SWITCHES, 2);
if (err == i2c::ERROR_OK) {
err = this->read(data, 1);
if (err == i2c::ERROR_OK) {
ESP_LOGD(TAG, "Read from switches: 0x%02X", data[0]);
if (this->interrupt_pin_ != nullptr) {
// datasheet says NOT to use pullup/down on the int line.
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_,
(data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
}
}
}
if (err == i2c::ERROR_OK) {
err = this->write(GET_MAX_VALUES, 2);
if (err == i2c::ERROR_OK) {
err = this->read(data, sizeof(data));
if (err == i2c::ERROR_OK) {
this->x_raw_max_ = encode_uint16(data[1], data[0]);
this->y_raw_max_ = encode_uint16(data[3], data[2]);
esph_log_d(TAG, "Read max_x/max_y %d/%d", this->x_raw_max_, this->y_raw_max_);
}
}
}
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to communicate!");
this->mark_failed();
return;
}
ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete");
}
void GT911Touchscreen::update_touches() {
i2c::ErrorCode err;
uint8_t touch_state = 0;
uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte
err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false);
ERROR_CHECK(err);
err = this->read(&touch_state, 1);
ERROR_CHECK(err);
this->write(CLEAR_TOUCH_STATE, sizeof(CLEAR_TOUCH_STATE));
uint8_t num_of_touches = touch_state & 0x07;
if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) {
this->skip_update_ = true; // skip send touch events, touchscreen is not ready yet.
return;
}
if (num_of_touches == 0)
return;
err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false);
ERROR_CHECK(err);
// num_of_touches is guaranteed to be 0..5. Also read the key data
err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1);
ERROR_CHECK(err);
for (uint8_t i = 0; i != num_of_touches; i++) {
uint16_t id = data[i][0];
uint16_t x = encode_uint16(data[i][2], data[i][1]);
uint16_t y = encode_uint16(data[i][4], data[i][3]);
this->set_raw_touch_position_(id, x, y);
}
auto keys = data[num_of_touches][0];
for (size_t i = 0; i != 4; i++) {
for (auto *listener : this->button_listeners_)
listener->update_button(i, (keys & (1 << i)) != 0);
}
}
void GT911Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG, "GT911 Touchscreen:");
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
}
} // namespace gt911
} // namespace esphome

View file

@ -0,0 +1,32 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace gt911 {
class GT911ButtonListener {
public:
virtual void update_button(uint8_t index, bool state) = 0;
};
class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); }
protected:
void update_touches() override;
InternalGPIOPin *interrupt_pin_{};
std::vector<GT911ButtonListener *> button_listeners_;
};
} // namespace gt911
} // namespace esphome

View file

@ -38,16 +38,20 @@ PROTOCOL_MIN_TEMPERATURE = 16.0
PROTOCOL_MAX_TEMPERATURE = 30.0 PROTOCOL_MAX_TEMPERATURE = 30.0
PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0 PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0
PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5 PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5
PROTOCOL_CONTROL_PACKET_SIZE = 10
CODEOWNERS = ["@paveldn"] CODEOWNERS = ["@paveldn"]
AUTO_LOAD = ["sensor"] AUTO_LOAD = ["sensor"]
DEPENDENCIES = ["climate", "uart"] DEPENDENCIES = ["climate", "uart"]
CONF_WIFI_SIGNAL = "wifi_signal" CONF_ALTERNATIVE_SWING_CONTROL = "alternative_swing_control"
CONF_ANSWER_TIMEOUT = "answer_timeout" CONF_ANSWER_TIMEOUT = "answer_timeout"
CONF_CONTROL_METHOD = "control_method"
CONF_CONTROL_PACKET_SIZE = "control_packet_size"
CONF_DISPLAY = "display" CONF_DISPLAY = "display"
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
CONF_VERTICAL_AIRFLOW = "vertical_airflow" CONF_VERTICAL_AIRFLOW = "vertical_airflow"
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" CONF_WIFI_SIGNAL = "wifi_signal"
PROTOCOL_HON = "HON" PROTOCOL_HON = "HON"
PROTOCOL_SMARTAIR2 = "SMARTAIR2" PROTOCOL_SMARTAIR2 = "SMARTAIR2"
@ -107,6 +111,13 @@ SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = {
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
} }
HonControlMethod = haier_ns.enum("HonControlMethod", True)
SUPPORTED_HON_CONTROL_METHODS = {
"MONITOR_ONLY": HonControlMethod.MONITOR_ONLY,
"SET_GROUP_PARAMETERS": HonControlMethod.SET_GROUP_PARAMETERS,
"SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER,
}
def validate_visual(config): def validate_visual(config):
if CONF_VISUAL in config: if CONF_VISUAL in config:
@ -184,6 +195,9 @@ CONFIG_SCHEMA = cv.All(
PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend(
{ {
cv.GenerateID(): cv.declare_id(Smartair2Climate), cv.GenerateID(): cv.declare_id(Smartair2Climate),
cv.Optional(
CONF_ALTERNATIVE_SWING_CONTROL, default=False
): cv.boolean,
cv.Optional( cv.Optional(
CONF_SUPPORTED_PRESETS, CONF_SUPPORTED_PRESETS,
default=list( default=list(
@ -197,7 +211,15 @@ CONFIG_SCHEMA = cv.All(
PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend(
{ {
cv.GenerateID(): cv.declare_id(HonClimate), cv.GenerateID(): cv.declare_id(HonClimate),
cv.Optional(
CONF_CONTROL_METHOD, default="SET_GROUP_PARAMETERS"
): cv.ensure_list(
cv.enum(SUPPORTED_HON_CONTROL_METHODS, upper=True)
),
cv.Optional(CONF_BEEPER, default=True): cv.boolean, cv.Optional(CONF_BEEPER, default=True): cv.boolean,
cv.Optional(
CONF_CONTROL_PACKET_SIZE, default=PROTOCOL_CONTROL_PACKET_SIZE
): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50),
cv.Optional( cv.Optional(
CONF_SUPPORTED_PRESETS, CONF_SUPPORTED_PRESETS,
default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()), default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()),
@ -408,6 +430,8 @@ async def to_code(config):
await climate.register_climate(var, config) await climate.register_climate(var, config)
cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL]))
if CONF_CONTROL_METHOD in config:
cg.add(var.set_control_method(config[CONF_CONTROL_METHOD]))
if CONF_BEEPER in config: if CONF_BEEPER in config:
cg.add(var.set_beeper_state(config[CONF_BEEPER])) cg.add(var.set_beeper_state(config[CONF_BEEPER]))
if CONF_DISPLAY in config: if CONF_DISPLAY in config:
@ -423,5 +447,15 @@ async def to_code(config):
cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS]))
if CONF_ANSWER_TIMEOUT in config: if CONF_ANSWER_TIMEOUT in config:
cg.add(var.set_answer_timeout(config[CONF_ANSWER_TIMEOUT])) cg.add(var.set_answer_timeout(config[CONF_ANSWER_TIMEOUT]))
if CONF_ALTERNATIVE_SWING_CONTROL in config:
cg.add(
var.set_alternative_swing_control(config[CONF_ALTERNATIVE_SWING_CONTROL])
)
if CONF_CONTROL_PACKET_SIZE in config:
cg.add(
var.set_extra_control_packet_bytes_size(
config[CONF_CONTROL_PACKET_SIZE] - PROTOCOL_CONTROL_PACKET_SIZE
)
)
# https://github.com/paveldn/HaierProtocol # https://github.com/paveldn/HaierProtocol
cg.add_library("pavlodn/HaierProtocol", "0.9.20") cg.add_library("pavlodn/HaierProtocol", "0.9.24")

View file

@ -19,56 +19,45 @@ constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000;
constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000; constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000;
constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000; constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000;
constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400; constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400;
constexpr size_t CONTROL_TIMEOUT_MS = 7000;
constexpr size_t NO_COMMAND = 0xFF; // Indicate that there is no command supplied
#if (HAIER_LOG_LEVEL > 4)
// To reduce size of binary this function only available when log level is Verbose
const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) { const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
static const char *phase_names[] = { static const char *phase_names[] = {
"SENDING_INIT_1", "SENDING_INIT_1",
"WAITING_INIT_1_ANSWER",
"SENDING_INIT_2", "SENDING_INIT_2",
"WAITING_INIT_2_ANSWER",
"SENDING_FIRST_STATUS_REQUEST", "SENDING_FIRST_STATUS_REQUEST",
"WAITING_FIRST_STATUS_ANSWER",
"SENDING_ALARM_STATUS_REQUEST", "SENDING_ALARM_STATUS_REQUEST",
"WAITING_ALARM_STATUS_ANSWER",
"IDLE", "IDLE",
"UNKNOWN",
"SENDING_STATUS_REQUEST", "SENDING_STATUS_REQUEST",
"WAITING_STATUS_ANSWER",
"SENDING_UPDATE_SIGNAL_REQUEST", "SENDING_UPDATE_SIGNAL_REQUEST",
"WAITING_UPDATE_SIGNAL_ANSWER",
"SENDING_SIGNAL_LEVEL", "SENDING_SIGNAL_LEVEL",
"WAITING_SIGNAL_LEVEL_ANSWER",
"SENDING_CONTROL", "SENDING_CONTROL",
"WAITING_CONTROL_ANSWER", "SENDING_ACTION_COMMAND",
"SENDING_POWER_ON_COMMAND",
"WAITING_POWER_ON_ANSWER",
"SENDING_POWER_OFF_COMMAND",
"WAITING_POWER_OFF_ANSWER",
"UNKNOWN" // Should be the last! "UNKNOWN" // Should be the last!
}; };
static_assert(
(sizeof(phase_names) / sizeof(char *)) == (((int) ProtocolPhases::NUM_PROTOCOL_PHASES) + 1),
"Wrong phase_names array size. Please, make sure that this array is aligned with the enum ProtocolPhases");
int phase_index = (int) phase; int phase_index = (int) phase;
if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0)) if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0))
phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES; phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES;
return phase_names[phase_index]; return phase_names[phase_index];
} }
#endif
bool check_timeout(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint,
size_t timeout) {
return std::chrono::duration_cast<std::chrono::milliseconds>(now - tpoint).count() > timeout;
}
HaierClimateBase::HaierClimateBase() HaierClimateBase::HaierClimateBase()
: haier_protocol_(*this), : haier_protocol_(*this),
protocol_phase_(ProtocolPhases::SENDING_INIT_1), protocol_phase_(ProtocolPhases::SENDING_INIT_1),
action_request_(ActionRequest::NO_ACTION),
display_status_(true), display_status_(true),
health_mode_(false), health_mode_(false),
force_send_control_(false), force_send_control_(false),
forced_publish_(false),
forced_request_status_(false), forced_request_status_(false),
first_control_attempt_(false),
reset_protocol_request_(false), reset_protocol_request_(false),
send_wifi_signal_(true) { send_wifi_signal_(true),
use_crc_(false) {
this->traits_ = climate::ClimateTraits(); this->traits_ = climate::ClimateTraits();
this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY,
@ -84,42 +73,43 @@ HaierClimateBase::~HaierClimateBase() {}
void HaierClimateBase::set_phase(ProtocolPhases phase) { void HaierClimateBase::set_phase(ProtocolPhases phase) {
if (this->protocol_phase_ != phase) { if (this->protocol_phase_ != phase) {
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase)); ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase));
#else
ESP_LOGV(TAG, "Phase transition: %d => %d", (int) this->protocol_phase_, (int) phase);
#endif
this->protocol_phase_ = phase; this->protocol_phase_ = phase;
} }
} }
bool HaierClimateBase::check_timeout_(std::chrono::steady_clock::time_point now, void HaierClimateBase::reset_phase_() {
std::chrono::steady_clock::time_point tpoint, size_t timeout) { this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
return std::chrono::duration_cast<std::chrono::milliseconds>(now - tpoint).count() > timeout; : ProtocolPhases::SENDING_INIT_1);
}
void HaierClimateBase::reset_to_idle_() {
this->force_send_control_ = false;
if (this->current_hvac_settings_.valid)
this->current_hvac_settings_.reset();
this->forced_request_status_ = true;
this->set_phase(ProtocolPhases::IDLE);
this->action_request_.reset();
} }
bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS); return check_timeout(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS);
} }
bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) { bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS); return check_timeout(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS);
}
bool HaierClimateBase::is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->control_request_timestamp_, CONTROL_TIMEOUT_MS);
} }
bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS); return check_timeout(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS);
} }
bool HaierClimateBase::is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now) { bool HaierClimateBase::is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL); return check_timeout(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL);
} }
#ifdef USE_WIFI #ifdef USE_WIFI
haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_(uint8_t message_type) { haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_() {
static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00}; static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00};
if (wifi::global_wifi_component->is_connected()) { if (wifi::global_wifi_component->is_connected()) {
wifi_status_data[1] = 0; wifi_status_data[1] = 0;
@ -131,7 +121,8 @@ haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_(uint8_t
wifi_status_data[1] = 1; wifi_status_data[1] = 1;
wifi_status_data[3] = 0; wifi_status_data[3] = 0;
} }
return haier_protocol::HaierMessage(message_type, wifi_status_data, sizeof(wifi_status_data)); return haier_protocol::HaierMessage(haier_protocol::FrameType::REPORT_NETWORK_STATUS, wifi_status_data,
sizeof(wifi_status_data));
} }
#endif #endif
@ -140,7 +131,7 @@ bool HaierClimateBase::get_display_state() const { return this->display_status_;
void HaierClimateBase::set_display_state(bool state) { void HaierClimateBase::set_display_state(bool state) {
if (this->display_status_ != state) { if (this->display_status_ != state) {
this->display_status_ = state; this->display_status_ = state;
this->set_force_send_control_(true); this->force_send_control_ = true;
} }
} }
@ -149,15 +140,24 @@ bool HaierClimateBase::get_health_mode() const { return this->health_mode_; }
void HaierClimateBase::set_health_mode(bool state) { void HaierClimateBase::set_health_mode(bool state) {
if (this->health_mode_ != state) { if (this->health_mode_ != state) {
this->health_mode_ = state; this->health_mode_ = state;
this->set_force_send_control_(true); this->force_send_control_ = true;
} }
} }
void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; } void HaierClimateBase::send_power_on_command() {
this->action_request_ =
PendingAction({ActionRequest::TURN_POWER_ON, esphome::optional<haier_protocol::HaierMessage>()});
}
void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; } void HaierClimateBase::send_power_off_command() {
this->action_request_ =
PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional<haier_protocol::HaierMessage>()});
}
void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; } void HaierClimateBase::toggle_power() {
this->action_request_ =
PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()});
}
void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) { void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
this->traits_.set_supported_swing_modes(modes); this->traits_.set_supported_swing_modes(modes);
@ -165,9 +165,7 @@ void HaierClimateBase::set_supported_swing_modes(const std::set<climate::Climate
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF);
} }
void HaierClimateBase::set_answer_timeout(uint32_t timeout) { void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); }
this->answer_timeout_ = std::chrono::milliseconds(timeout);
}
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) { void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) {
this->traits_.set_supported_modes(modes); this->traits_.set_supported_modes(modes);
@ -183,29 +181,42 @@ void HaierClimateBase::set_supported_presets(const std::set<climate::ClimatePres
void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; }
haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type, void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) {
uint8_t expected_request_message_type, this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message});
uint8_t answer_message_type, }
uint8_t expected_answer_message_type,
haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(
haier_protocol::FrameType request_message_type, haier_protocol::FrameType expected_request_message_type,
haier_protocol::FrameType answer_message_type, haier_protocol::FrameType expected_answer_message_type,
ProtocolPhases expected_phase) { ProtocolPhases expected_phase) {
haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK; haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK;
if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type)) if ((expected_request_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) &&
(request_message_type != expected_request_message_type))
result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type)) if ((expected_answer_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) &&
(answer_message_type != expected_answer_message_type))
result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_)) if (!this->haier_protocol_.is_waiting_for_answer() ||
((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_)))
result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE; result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
if (is_message_invalid(answer_message_type)) if (answer_message_type == haier_protocol::FrameType::INVALID)
result = haier_protocol::HandlerError::INVALID_ANSWER; result = haier_protocol::HandlerError::INVALID_ANSWER;
return result; return result;
} }
haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) { haier_protocol::HandlerError HaierClimateBase::report_network_status_answer_handler_(
#if (HAIER_LOG_LEVEL > 4) haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data,
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_)); size_t data_size) {
#else haier_protocol::HandlerError result =
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_); this->answer_preprocess_(request_type, haier_protocol::FrameType::REPORT_NETWORK_STATUS, message_type,
#endif haier_protocol::FrameType::CONFIRM, ProtocolPhases::SENDING_SIGNAL_LEVEL);
this->set_phase(ProtocolPhases::IDLE);
return result;
}
haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(haier_protocol::FrameType request_type) {
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) request_type,
phase_to_string_(this->protocol_phase_));
if (this->protocol_phase_ > ProtocolPhases::IDLE) { if (this->protocol_phase_ > ProtocolPhases::IDLE) {
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
} else { } else {
@ -219,79 +230,95 @@ void HaierClimateBase::setup() {
// Set timestamp here to give AC time to boot // Set timestamp here to give AC time to boot
this->last_request_timestamp_ = std::chrono::steady_clock::now(); this->last_request_timestamp_ = std::chrono::steady_clock::now();
this->set_phase(ProtocolPhases::SENDING_INIT_1); this->set_phase(ProtocolPhases::SENDING_INIT_1);
this->set_handlers();
this->haier_protocol_.set_default_timeout_handler( this->haier_protocol_.set_default_timeout_handler(
std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1));
this->set_handlers();
} }
void HaierClimateBase::dump_config() { void HaierClimateBase::dump_config() {
LOG_CLIMATE("", "Haier Climate", this); LOG_CLIMATE("", "Haier Climate", this);
ESP_LOGCONFIG(TAG, " Device communication status: %s", ESP_LOGCONFIG(TAG, " Device communication status: %s", this->valid_connection() ? "established" : "none");
(this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none");
} }
void HaierClimateBase::loop() { void HaierClimateBase::loop() {
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
if ((std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_valid_status_timestamp_).count() > if ((std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_valid_status_timestamp_).count() >
COMMUNICATION_TIMEOUT_MS) || COMMUNICATION_TIMEOUT_MS) ||
(this->reset_protocol_request_)) { (this->reset_protocol_request_ && (!this->haier_protocol_.is_waiting_for_answer()))) {
this->last_valid_status_timestamp_ = now;
if (this->protocol_phase_ >= ProtocolPhases::IDLE) { if (this->protocol_phase_ >= ProtocolPhases::IDLE) {
// No status too long, reseting protocol // No status too long, reseting protocol
// No need to reset protocol if we didn't pass initialization phase
if (this->reset_protocol_request_) { if (this->reset_protocol_request_) {
this->reset_protocol_request_ = false; this->reset_protocol_request_ = false;
ESP_LOGW(TAG, "Protocol reset requested"); ESP_LOGW(TAG, "Protocol reset requested");
} else { } else {
ESP_LOGW(TAG, "Communication timeout, reseting protocol"); ESP_LOGW(TAG, "Communication timeout, reseting protocol");
} }
this->last_valid_status_timestamp_ = now; this->process_protocol_reset();
this->set_force_send_control_(false);
if (this->hvac_settings_.valid)
this->hvac_settings_.reset();
this->set_phase(ProtocolPhases::SENDING_INIT_1);
return; return;
} else {
// No need to reset protocol if we didn't pass initialization phase
this->last_valid_status_timestamp_ = now;
} }
}; };
if ((this->protocol_phase_ == ProtocolPhases::IDLE) || if ((!this->haier_protocol_.is_waiting_for_answer()) &&
((this->protocol_phase_ == ProtocolPhases::IDLE) ||
(this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) || (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) ||
(this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) || (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) ||
(this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL)) { (this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL))) {
// If control message or action is pending we should send it ASAP unless we are in initialisation // If control message or action is pending we should send it ASAP unless we are in initialisation
// procedure or waiting for an answer // procedure or waiting for an answer
if (this->action_request_ != ActionRequest::NO_ACTION) { if (this->action_request_.has_value() && this->prepare_pending_action()) {
this->process_pending_action(); this->set_phase(ProtocolPhases::SENDING_ACTION_COMMAND);
} else if (this->hvac_settings_.valid || this->force_send_control_) { } else if (this->next_hvac_settings_.valid || this->force_send_control_) {
ESP_LOGV(TAG, "Control packet is pending..."); ESP_LOGV(TAG, "Control packet is pending...");
this->set_phase(ProtocolPhases::SENDING_CONTROL); this->set_phase(ProtocolPhases::SENDING_CONTROL);
if (this->next_hvac_settings_.valid) {
this->current_hvac_settings_ = this->next_hvac_settings_;
this->next_hvac_settings_.reset();
} else {
this->current_hvac_settings_.reset();
}
} }
} }
this->process_phase(now); this->process_phase(now);
this->haier_protocol_.loop(); this->haier_protocol_.loop();
} }
void HaierClimateBase::process_pending_action() { void HaierClimateBase::process_protocol_reset() {
ActionRequest request = this->action_request_; this->force_send_control_ = false;
if (this->action_request_ == ActionRequest::TOGGLE_POWER) { if (this->current_hvac_settings_.valid)
request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF; this->current_hvac_settings_.reset();
if (this->next_hvac_settings_.valid)
this->next_hvac_settings_.reset();
this->mode = CLIMATE_MODE_OFF;
this->current_temperature = NAN;
this->target_temperature = NAN;
this->fan_mode.reset();
this->preset.reset();
this->publish_state();
this->set_phase(ProtocolPhases::SENDING_INIT_1);
} }
switch (request) {
bool HaierClimateBase::prepare_pending_action() {
if (this->action_request_.has_value()) {
switch (this->action_request_.value().action) {
case ActionRequest::SEND_CUSTOM_COMMAND:
return true;
case ActionRequest::TURN_POWER_ON: case ActionRequest::TURN_POWER_ON:
this->set_phase(ProtocolPhases::SENDING_POWER_ON_COMMAND); this->action_request_.value().message = this->get_power_message(true);
break; return true;
case ActionRequest::TURN_POWER_OFF: case ActionRequest::TURN_POWER_OFF:
this->set_phase(ProtocolPhases::SENDING_POWER_OFF_COMMAND); this->action_request_.value().message = this->get_power_message(false);
break; return true;
case ActionRequest::TOGGLE_POWER: case ActionRequest::TOGGLE_POWER:
case ActionRequest::NO_ACTION: this->action_request_.value().message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF);
// shouldn't get here, do nothing return true;
break;
default: default:
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_); ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_.value().action);
break; this->action_request_.reset();
return false;
} }
this->action_request_ = ActionRequest::NO_ACTION; } else
return false;
} }
ClimateTraits HaierClimateBase::traits() { return traits_; } ClimateTraits HaierClimateBase::traits() { return traits_; }
@ -302,23 +329,22 @@ void HaierClimateBase::control(const ClimateCall &call) {
ESP_LOGW(TAG, "Can't send control packet, first poll answer not received"); ESP_LOGW(TAG, "Can't send control packet, first poll answer not received");
return; // cancel the control, we cant do it without a poll answer. return; // cancel the control, we cant do it without a poll answer.
} }
if (this->hvac_settings_.valid) { if (this->current_hvac_settings_.valid) {
ESP_LOGW(TAG, "Overriding old valid settings before they were applied!"); ESP_LOGW(TAG, "New settings come faster then processed!");
} }
{ {
if (call.get_mode().has_value()) if (call.get_mode().has_value())
this->hvac_settings_.mode = call.get_mode(); this->next_hvac_settings_.mode = call.get_mode();
if (call.get_fan_mode().has_value()) if (call.get_fan_mode().has_value())
this->hvac_settings_.fan_mode = call.get_fan_mode(); this->next_hvac_settings_.fan_mode = call.get_fan_mode();
if (call.get_swing_mode().has_value()) if (call.get_swing_mode().has_value())
this->hvac_settings_.swing_mode = call.get_swing_mode(); this->next_hvac_settings_.swing_mode = call.get_swing_mode();
if (call.get_target_temperature().has_value()) if (call.get_target_temperature().has_value())
this->hvac_settings_.target_temperature = call.get_target_temperature(); this->next_hvac_settings_.target_temperature = call.get_target_temperature();
if (call.get_preset().has_value()) if (call.get_preset().has_value())
this->hvac_settings_.preset = call.get_preset(); this->next_hvac_settings_.preset = call.get_preset();
this->hvac_settings_.valid = true; this->next_hvac_settings_.valid = true;
} }
this->first_control_attempt_ = true;
} }
void HaierClimateBase::HvacSettings::reset() { void HaierClimateBase::HvacSettings::reset() {
@ -330,19 +356,9 @@ void HaierClimateBase::HvacSettings::reset() {
this->preset.reset(); this->preset.reset();
} }
void HaierClimateBase::set_force_send_control_(bool status) { void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats,
this->force_send_control_ = status; std::chrono::milliseconds interval) {
if (status) { this->haier_protocol_.send_message(command, use_crc, num_repeats, interval);
this->first_control_attempt_ = true;
}
}
void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) {
if (this->answer_timeout_.has_value()) {
this->haier_protocol_.send_message(command, use_crc, this->answer_timeout_.value());
} else {
this->haier_protocol_.send_message(command, use_crc);
}
this->last_request_timestamp_ = std::chrono::steady_clock::now(); this->last_request_timestamp_ = std::chrono::steady_clock::now();
} }

View file

@ -11,7 +11,7 @@ namespace esphome {
namespace haier { namespace haier {
enum class ActionRequest : uint8_t { enum class ActionRequest : uint8_t {
NO_ACTION = 0, SEND_CUSTOM_COMMAND = 0,
TURN_POWER_ON = 1, TURN_POWER_ON = 1,
TURN_POWER_OFF = 2, TURN_POWER_OFF = 2,
TOGGLE_POWER = 3, TOGGLE_POWER = 3,
@ -33,7 +33,6 @@ class HaierClimateBase : public esphome::Component,
void control(const esphome::climate::ClimateCall &call) override; void control(const esphome::climate::ClimateCall &call) override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; } float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; }
void set_fahrenheit(bool fahrenheit);
void set_display_state(bool state); void set_display_state(bool state);
bool get_display_state() const; bool get_display_state() const;
void set_health_mode(bool state); void set_health_mode(bool state);
@ -45,6 +44,7 @@ class HaierClimateBase : public esphome::Component,
void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes); void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes);
void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes); void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes);
void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets); void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets);
bool valid_connection() { return this->protocol_phase_ >= ProtocolPhases::IDLE; };
size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
size_t read_array(uint8_t *data, size_t len) noexcept override { size_t read_array(uint8_t *data, size_t len) noexcept override {
return esphome::uart::UARTDevice::read_array(data, len) ? len : 0; return esphome::uart::UARTDevice::read_array(data, len) ? len : 0;
@ -55,63 +55,56 @@ class HaierClimateBase : public esphome::Component,
bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; }; bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; };
void set_answer_timeout(uint32_t timeout); void set_answer_timeout(uint32_t timeout);
void set_send_wifi(bool send_wifi); void set_send_wifi(bool send_wifi);
void send_custom_command(const haier_protocol::HaierMessage &message);
protected: protected:
enum class ProtocolPhases { enum class ProtocolPhases {
UNKNOWN = -1, UNKNOWN = -1,
// INITIALIZATION // INITIALIZATION
SENDING_INIT_1 = 0, SENDING_INIT_1 = 0,
WAITING_INIT_1_ANSWER = 1, SENDING_INIT_2,
SENDING_INIT_2 = 2, SENDING_FIRST_STATUS_REQUEST,
WAITING_INIT_2_ANSWER = 3, SENDING_ALARM_STATUS_REQUEST,
SENDING_FIRST_STATUS_REQUEST = 4,
WAITING_FIRST_STATUS_ANSWER = 5,
SENDING_ALARM_STATUS_REQUEST = 6,
WAITING_ALARM_STATUS_ANSWER = 7,
// FUNCTIONAL STATE // FUNCTIONAL STATE
IDLE = 8, IDLE,
SENDING_STATUS_REQUEST = 10, SENDING_STATUS_REQUEST,
WAITING_STATUS_ANSWER = 11, SENDING_UPDATE_SIGNAL_REQUEST,
SENDING_UPDATE_SIGNAL_REQUEST = 12, SENDING_SIGNAL_LEVEL,
WAITING_UPDATE_SIGNAL_ANSWER = 13, SENDING_CONTROL,
SENDING_SIGNAL_LEVEL = 14, SENDING_ACTION_COMMAND,
WAITING_SIGNAL_LEVEL_ANSWER = 15,
SENDING_CONTROL = 16,
WAITING_CONTROL_ANSWER = 17,
SENDING_POWER_ON_COMMAND = 18,
WAITING_POWER_ON_ANSWER = 19,
SENDING_POWER_OFF_COMMAND = 20,
WAITING_POWER_OFF_ANSWER = 21,
NUM_PROTOCOL_PHASES NUM_PROTOCOL_PHASES
}; };
#if (HAIER_LOG_LEVEL > 4)
const char *phase_to_string_(ProtocolPhases phase); const char *phase_to_string_(ProtocolPhases phase);
#endif
virtual void set_handlers() = 0; virtual void set_handlers() = 0;
virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; virtual void process_phase(std::chrono::steady_clock::time_point now) = 0;
virtual haier_protocol::HaierMessage get_control_message() = 0; virtual haier_protocol::HaierMessage get_control_message() = 0;
virtual bool is_message_invalid(uint8_t message_type) = 0; virtual haier_protocol::HaierMessage get_power_message(bool state) = 0;
virtual void process_pending_action(); virtual bool prepare_pending_action();
virtual void process_protocol_reset();
esphome::climate::ClimateTraits traits() override; esphome::climate::ClimateTraits traits() override;
// Answers handlers // Answer handlers
haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type, haier_protocol::HandlerError answer_preprocess_(haier_protocol::FrameType request_message_type,
uint8_t answer_message_type, uint8_t expected_answer_message_type, haier_protocol::FrameType expected_request_message_type,
haier_protocol::FrameType answer_message_type,
haier_protocol::FrameType expected_answer_message_type,
ProtocolPhases expected_phase); ProtocolPhases expected_phase);
haier_protocol::HandlerError report_network_status_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size);
// Timeout handler // Timeout handler
haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type); haier_protocol::HandlerError timeout_default_handler_(haier_protocol::FrameType request_type);
// Helper functions // Helper functions
void set_force_send_control_(bool status); void send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats = 0,
void send_message_(const haier_protocol::HaierMessage &command, bool use_crc); std::chrono::milliseconds interval = std::chrono::milliseconds::zero());
virtual void set_phase(ProtocolPhases phase); virtual void set_phase(ProtocolPhases phase);
bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint, void reset_phase_();
size_t timeout); void reset_to_idle_();
bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now);
bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now);
bool is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now);
bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now);
bool is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now);
#ifdef USE_WIFI #ifdef USE_WIFI
haier_protocol::HaierMessage get_wifi_signal_message_(uint8_t message_type); haier_protocol::HaierMessage get_wifi_signal_message_();
#endif #endif
struct HvacSettings { struct HvacSettings {
@ -122,28 +115,33 @@ class HaierClimateBase : public esphome::Component,
esphome::optional<esphome::climate::ClimatePreset> preset; esphome::optional<esphome::climate::ClimatePreset> preset;
bool valid; bool valid;
HvacSettings() : valid(false){}; HvacSettings() : valid(false){};
HvacSettings(const HvacSettings &) = default;
HvacSettings &operator=(const HvacSettings &) = default;
void reset(); void reset();
}; };
struct PendingAction {
ActionRequest action;
esphome::optional<haier_protocol::HaierMessage> message;
};
haier_protocol::ProtocolHandler haier_protocol_; haier_protocol::ProtocolHandler haier_protocol_;
ProtocolPhases protocol_phase_; ProtocolPhases protocol_phase_;
ActionRequest action_request_; esphome::optional<PendingAction> action_request_;
uint8_t fan_mode_speed_; uint8_t fan_mode_speed_;
uint8_t other_modes_fan_speed_; uint8_t other_modes_fan_speed_;
bool display_status_; bool display_status_;
bool health_mode_; bool health_mode_;
bool force_send_control_; bool force_send_control_;
bool forced_publish_;
bool forced_request_status_; bool forced_request_status_;
bool first_control_attempt_;
bool reset_protocol_request_; bool reset_protocol_request_;
bool send_wifi_signal_;
bool use_crc_;
esphome::climate::ClimateTraits traits_; esphome::climate::ClimateTraits traits_;
HvacSettings hvac_settings_; HvacSettings current_hvac_settings_;
HvacSettings next_hvac_settings_;
std::unique_ptr<uint8_t[]> last_status_message_;
std::chrono::steady_clock::time_point last_request_timestamp_; // For interval between messages std::chrono::steady_clock::time_point last_request_timestamp_; // For interval between messages
std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout
std::chrono::steady_clock::time_point last_status_request_; // To request AC status std::chrono::steady_clock::time_point last_status_request_; // To request AC status
std::chrono::steady_clock::time_point control_request_timestamp_; // To send control message
optional<std::chrono::milliseconds> answer_timeout_; // Message answer timeout
bool send_wifi_signal_;
std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level
}; };

View file

@ -14,6 +14,8 @@ namespace haier {
static const char *const TAG = "haier.climate"; static const char *const TAG = "haier.climate";
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64; constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5;
constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500);
hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) { hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) {
switch (direction) { switch (direction) {
@ -48,14 +50,11 @@ hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDir
} }
HonClimate::HonClimate() HonClimate::HonClimate()
: last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]), : cleaning_status_(CleaningState::NO_CLEANING),
cleaning_status_(CleaningState::NO_CLEANING),
got_valid_outdoor_temp_(false), got_valid_outdoor_temp_(false),
hvac_hardware_info_available_(false),
hvac_functions_{false, false, false, false, false},
use_crc_(hvac_functions_[2]),
active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
outdoor_sensor_(nullptr) { outdoor_sensor_(nullptr) {
last_status_message_ = std::unique_ptr<uint8_t[]>(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]);
this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID; this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID;
this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO; this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
} }
@ -72,14 +71,14 @@ AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this-
void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) { void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) {
this->vertical_direction_ = direction; this->vertical_direction_ = direction;
this->set_force_send_control_(true); this->force_send_control_ = true;
} }
AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; } AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; }
void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) { void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) {
this->horizontal_direction_ = direction; this->horizontal_direction_ = direction;
this->set_force_send_control_(true); this->force_send_control_ = true;
} }
std::string HonClimate::get_cleaning_status_text() const { std::string HonClimate::get_cleaning_status_text() const {
@ -98,35 +97,35 @@ CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_st
void HonClimate::start_self_cleaning() { void HonClimate::start_self_cleaning() {
if (this->cleaning_status_ == CleaningState::NO_CLEANING) { if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
ESP_LOGI(TAG, "Sending self cleaning start request"); ESP_LOGI(TAG, "Sending self cleaning start request");
this->action_request_ = ActionRequest::START_SELF_CLEAN; this->action_request_ =
this->set_force_send_control_(true); PendingAction({ActionRequest::START_SELF_CLEAN, esphome::optional<haier_protocol::HaierMessage>()});
} }
} }
void HonClimate::start_steri_cleaning() { void HonClimate::start_steri_cleaning() {
if (this->cleaning_status_ == CleaningState::NO_CLEANING) { if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
ESP_LOGI(TAG, "Sending steri cleaning start request"); ESP_LOGI(TAG, "Sending steri cleaning start request");
this->action_request_ = ActionRequest::START_STERI_CLEAN; this->action_request_ =
this->set_force_send_control_(true); PendingAction({ActionRequest::START_STERI_CLEAN, esphome::optional<haier_protocol::HaierMessage>()});
} }
} }
haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size) { const uint8_t *data, size_t data_size) {
// Should check this before preprocess // Should check this before preprocess
if (message_type == (uint8_t) hon_protocol::FrameType::INVALID) { if (message_type == haier_protocol::FrameType::INVALID) {
ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the smartAir2 " ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the smartAir2 "
"protocol instead of hOn"); "protocol instead of hOn");
this->set_phase(ProtocolPhases::SENDING_INIT_1); this->set_phase(ProtocolPhases::SENDING_INIT_1);
return haier_protocol::HandlerError::INVALID_ANSWER; return haier_protocol::HandlerError::INVALID_ANSWER;
} }
haier_protocol::HandlerError result = this->answer_preprocess_( haier_protocol::HandlerError result =
request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type, this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_VERSION, message_type,
(uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_INIT_1_ANSWER); haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::SENDING_INIT_1);
if (result == haier_protocol::HandlerError::HANDLER_OK) { if (result == haier_protocol::HandlerError::HANDLER_OK) {
if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) { if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) {
// Wrong structure // Wrong structure
this->set_phase(ProtocolPhases::SENDING_INIT_1);
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
} }
// All OK // All OK
@ -134,54 +133,57 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint
char tmp[9]; char tmp[9];
tmp[8] = 0; tmp[8] = 0;
strncpy(tmp, answr->protocol_version, 8); strncpy(tmp, answr->protocol_version, 8);
this->hvac_protocol_version_ = std::string(tmp); this->hvac_hardware_info_ = HardwareInfo();
this->hvac_hardware_info_.value().protocol_version_ = std::string(tmp);
strncpy(tmp, answr->software_version, 8); strncpy(tmp, answr->software_version, 8);
this->hvac_software_version_ = std::string(tmp); this->hvac_hardware_info_.value().software_version_ = std::string(tmp);
strncpy(tmp, answr->hardware_version, 8); strncpy(tmp, answr->hardware_version, 8);
this->hvac_hardware_version_ = std::string(tmp); this->hvac_hardware_info_.value().hardware_version_ = std::string(tmp);
strncpy(tmp, answr->device_name, 8); strncpy(tmp, answr->device_name, 8);
this->hvac_device_name_ = std::string(tmp); this->hvac_hardware_info_.value().device_name_ = std::string(tmp);
this->hvac_functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support this->hvac_hardware_info_.value().functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support
this->hvac_functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support this->hvac_hardware_info_.value().functions_[1] =
this->hvac_functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support (answr->functions[1] & 0x02) != 0; // controller-device mode support
this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support this->hvac_hardware_info_.value().functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support
this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support this->hvac_hardware_info_.value().functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support
this->hvac_hardware_info_available_ = true; this->hvac_hardware_info_.value().functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support
this->use_crc_ = this->hvac_hardware_info_.value().functions_[2];
this->set_phase(ProtocolPhases::SENDING_INIT_2); this->set_phase(ProtocolPhases::SENDING_INIT_2);
return result; return result;
} else { } else {
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE this->reset_phase_();
: ProtocolPhases::SENDING_INIT_1);
return result; return result;
} }
} }
haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size) { const uint8_t *data, size_t data_size) {
haier_protocol::HandlerError result = this->answer_preprocess_( haier_protocol::HandlerError result =
request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type, this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_ID, message_type,
(uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_INIT_2_ANSWER); haier_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::SENDING_INIT_2);
if (result == haier_protocol::HandlerError::HANDLER_OK) { if (result == haier_protocol::HandlerError::HANDLER_OK) {
this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
return result; return result;
} else { } else {
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE this->reset_phase_();
: ProtocolPhases::SENDING_INIT_1);
return result; return result;
} }
} }
haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameType request_type,
const uint8_t *data, size_t data_size) { haier_protocol::FrameType message_type, const uint8_t *data,
size_t data_size) {
haier_protocol::HandlerError result = haier_protocol::HandlerError result =
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type, this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type,
(uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
if (result == haier_protocol::HandlerError::HANDLER_OK) { if (result == haier_protocol::HandlerError::HANDLER_OK) {
result = this->process_status_message_(data, data_size); result = this->process_status_message_(data, data_size);
if (result != haier_protocol::HandlerError::HANDLER_OK) { if (result != haier_protocol::HandlerError::HANDLER_OK) {
ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE this->reset_phase_();
: ProtocolPhases::SENDING_INIT_1); this->action_request_.reset();
this->force_send_control_ = false;
} else { } else {
if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) { if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) {
memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl)); memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl));
@ -189,36 +191,48 @@ haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, u
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
sizeof(hon_protocol::HaierPacketControl)); sizeof(hon_protocol::HaierPacketControl));
} }
if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
ESP_LOGI(TAG, "First HVAC status received"); ESP_LOGI(TAG, "First HVAC status received");
this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
} else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) || break;
(this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) || case ProtocolPhases::SENDING_ACTION_COMMAND:
(this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) { // Do nothing, phase will be changed in process_phase
break;
case ProtocolPhases::SENDING_STATUS_REQUEST:
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { break;
case ProtocolPhases::SENDING_CONTROL:
if (!this->control_messages_queue_.empty())
this->control_messages_queue_.pop();
if (this->control_messages_queue_.empty()) {
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
this->set_force_send_control_(false); this->force_send_control_ = false;
if (this->hvac_settings_.valid) if (this->current_hvac_settings_.valid)
this->hvac_settings_.reset(); this->current_hvac_settings_.reset();
} else {
this->set_phase(ProtocolPhases::SENDING_CONTROL);
}
break;
default:
break;
} }
} }
return result; return result;
} else { } else {
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE this->action_request_.reset();
: ProtocolPhases::SENDING_INIT_1); this->force_send_control_ = false;
this->reset_phase_();
return result; return result;
} }
} }
haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type, haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(
uint8_t message_type, haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data,
const uint8_t *data,
size_t data_size) { size_t data_size) {
haier_protocol::HandlerError result = haier_protocol::HandlerError result = this->answer_preprocess_(
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION, request_type, haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, message_type,
message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
if (result == haier_protocol::HandlerError::HANDLER_OK) { if (result == haier_protocol::HandlerError::HANDLER_OK) {
this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL);
return result; return result;
@ -228,25 +242,16 @@ haier_protocol::HandlerError HonClimate::get_management_information_answer_handl
} }
} }
haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(uint8_t request_type, haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_protocol::FrameType request_type,
uint8_t message_type, haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size) { const uint8_t *data, size_t data_size) {
haier_protocol::HandlerError result = if (request_type == haier_protocol::FrameType::GET_ALARM_STATUS) {
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, if (message_type != haier_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) {
(uint8_t) hon_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
this->set_phase(ProtocolPhases::IDLE);
return result;
}
haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type,
const uint8_t *data, size_t data_size) {
if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) {
if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) {
// Unexpected answer to request // Unexpected answer to request
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
} }
if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) { if (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) {
// Don't expect this answer now // Don't expect this answer now
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
@ -263,27 +268,27 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_
void HonClimate::set_handlers() { void HonClimate::set_handlers() {
// Set handlers // Set handlers
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION), haier_protocol::FrameType::GET_DEVICE_VERSION,
std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4)); std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::GET_DEVICE_ID), haier_protocol::FrameType::GET_DEVICE_ID,
std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4)); std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::CONTROL), haier_protocol::FrameType::CONTROL,
std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3,
std::placeholders::_4)); std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION), haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION,
std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::GET_ALARM_STATUS), haier_protocol::FrameType::GET_ALARM_STATUS,
std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4)); std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::REPORT_NETWORK_STATUS), haier_protocol::FrameType::REPORT_NETWORK_STATUS,
std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4)); std::placeholders::_3, std::placeholders::_4));
} }
@ -291,14 +296,18 @@ void HonClimate::set_handlers() {
void HonClimate::dump_config() { void HonClimate::dump_config() {
HaierClimateBase::dump_config(); HaierClimateBase::dump_config();
ESP_LOGCONFIG(TAG, " Protocol version: hOn"); ESP_LOGCONFIG(TAG, " Protocol version: hOn");
if (this->hvac_hardware_info_available_) { ESP_LOGCONFIG(TAG, " Control method: %d", (uint8_t) this->control_method_);
ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_protocol_version_.c_str()); if (this->hvac_hardware_info_.has_value()) {
ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_software_version_.c_str()); ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_hardware_info_.value().protocol_version_.c_str());
ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_version_.c_str()); ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_hardware_info_.value().software_version_.c_str());
ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_device_name_.c_str()); ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_info_.value().hardware_version_.c_str());
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""), ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_hardware_info_.value().device_name_.c_str());
(this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""), ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s",
(this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : "")); (this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""),
(this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""),
(this->hvac_hardware_info_.value().functions_[2] ? " crc" : ""),
(this->hvac_hardware_info_.value().functions_[3] ? " multinode" : ""),
(this->hvac_hardware_info_.value().functions_[4] ? " role" : ""));
ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str()); ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
} }
} }
@ -307,7 +316,6 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
switch (this->protocol_phase_) { switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_INIT_1: case ProtocolPhases::SENDING_INIT_1:
if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) { if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) {
this->hvac_hardware_info_available_ = false;
// Indicate device capabilities: // Indicate device capabilities:
// bit 0 - if 1 module support interactive mode // bit 0 - if 1 module support interactive mode
// bit 1 - if 1 module support controller-device mode // bit 1 - if 1 module support controller-device mode
@ -316,108 +324,94 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
// bit 4..bit 15 - not used // bit 4..bit 15 - not used
uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; uint8_t module_capabilities[2] = {0b00000000, 0b00000111};
static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST(
(uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities));
this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_); this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_);
this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER);
} }
break; break;
case ProtocolPhases::SENDING_INIT_2: case ProtocolPhases::SENDING_INIT_2:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage DEVICEID_REQUEST((uint8_t) hon_protocol::FrameType::GET_DEVICE_ID); static const haier_protocol::HaierMessage DEVICEID_REQUEST(haier_protocol::FrameType::GET_DEVICE_ID);
this->send_message_(DEVICEID_REQUEST, this->use_crc_); this->send_message_(DEVICEID_REQUEST, this->use_crc_);
this->set_phase(ProtocolPhases::WAITING_INIT_2_ANSWER);
} }
break; break;
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
case ProtocolPhases::SENDING_STATUS_REQUEST: case ProtocolPhases::SENDING_STATUS_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage STATUS_REQUEST( static const haier_protocol::HaierMessage STATUS_REQUEST(
(uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA); haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA);
this->send_message_(STATUS_REQUEST, this->use_crc_); this->send_message_(STATUS_REQUEST, this->use_crc_);
this->last_status_request_ = now; this->last_status_request_ = now;
this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1));
} }
break; break;
#ifdef USE_WIFI #ifdef USE_WIFI
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST( static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST(
(uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION); haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION);
this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_); this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_);
this->last_signal_request_ = now; this->last_signal_request_ = now;
this->set_phase(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
} }
break; break;
case ProtocolPhases::SENDING_SIGNAL_LEVEL: case ProtocolPhases::SENDING_SIGNAL_LEVEL:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
this->send_message_(this->get_wifi_signal_message_((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS), this->send_message_(this->get_wifi_signal_message_(), this->use_crc_);
this->use_crc_);
this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
} }
break; break;
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
break;
#else #else
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
case ProtocolPhases::SENDING_SIGNAL_LEVEL: case ProtocolPhases::SENDING_SIGNAL_LEVEL:
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
break; break;
#endif #endif
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST( static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS);
(uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS);
this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_);
this->set_phase(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER);
} }
break; break;
case ProtocolPhases::SENDING_CONTROL: case ProtocolPhases::SENDING_CONTROL:
if (this->first_control_attempt_) { if (this->control_messages_queue_.empty()) {
this->control_request_timestamp_ = now; switch (this->control_method_) {
this->first_control_attempt_ = false; case HonControlMethod::SET_GROUP_PARAMETERS: {
haier_protocol::HaierMessage control_message = this->get_control_message();
this->control_messages_queue_.push(control_message);
} break;
case HonControlMethod::SET_SINGLE_PARAMETER:
this->fill_control_messages_queue_();
break;
case HonControlMethod::MONITOR_ONLY:
ESP_LOGI(TAG, "AC control is disabled, monitor only");
this->reset_to_idle_();
return;
default:
ESP_LOGW(TAG, "Unsupported control method for hOn protocol!");
this->reset_to_idle_();
return;
} }
if (this->is_control_message_timeout_exceeded_(now)) { }
ESP_LOGW(TAG, "Sending control packet timeout!"); if (this->control_messages_queue_.empty()) {
this->set_force_send_control_(false); ESP_LOGW(TAG, "Control message queue is empty!");
if (this->hvac_settings_.valid) this->reset_to_idle_();
this->hvac_settings_.reset();
this->forced_request_status_ = true;
this->forced_publish_ = true;
this->set_phase(ProtocolPhases::IDLE);
} else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { } else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
haier_protocol::HaierMessage control_message = get_control_message(); ESP_LOGI(TAG, "Sending control packet, queue size %d", this->control_messages_queue_.size());
this->send_message_(control_message, this->use_crc_); this->send_message_(this->control_messages_queue_.front(), this->use_crc_, CONTROL_MESSAGE_RETRIES,
ESP_LOGI(TAG, "Control packet sent"); CONTROL_MESSAGE_RETRIES_INTERVAL);
this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER);
} }
break; break;
case ProtocolPhases::SENDING_POWER_ON_COMMAND: case ProtocolPhases::SENDING_ACTION_COMMAND:
case ProtocolPhases::SENDING_POWER_OFF_COMMAND: if (this->action_request_.has_value()) {
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->action_request_.value().message.has_value()) {
uint8_t pwr_cmd_buf[2] = {0x00, 0x00}; this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND) this->action_request_.value().message.reset();
pwr_cmd_buf[1] = 0x01; } else {
haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL, // Message already sent, reseting request and return to idle
((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, this->action_request_.reset();
pwr_cmd_buf, sizeof(pwr_cmd_buf)); this->set_phase(ProtocolPhases::IDLE);
this->send_message_(power_cmd, this->use_crc_); }
this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND } else {
? ProtocolPhases::WAITING_POWER_ON_ANSWER ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!");
: ProtocolPhases::WAITING_POWER_OFF_ANSWER); this->set_phase(ProtocolPhases::IDLE);
} }
break;
case ProtocolPhases::WAITING_INIT_1_ANSWER:
case ProtocolPhases::WAITING_INIT_2_ANSWER:
case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER:
case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER:
case ProtocolPhases::WAITING_STATUS_ANSWER:
case ProtocolPhases::WAITING_CONTROL_ANSWER:
case ProtocolPhases::WAITING_POWER_ON_ANSWER:
case ProtocolPhases::WAITING_POWER_OFF_ANSWER:
break; break;
case ProtocolPhases::IDLE: { case ProtocolPhases::IDLE: {
if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
@ -433,26 +427,35 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
} break; } break;
default: default:
// Shouldn't get here // Shouldn't get here
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
#else
ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_);
#endif
this->set_phase(ProtocolPhases::SENDING_INIT_1); this->set_phase(ProtocolPhases::SENDING_INIT_1);
break; break;
} }
} }
haier_protocol::HaierMessage HonClimate::get_power_message(bool state) {
if (state) {
static haier_protocol::HaierMessage power_on_message(
haier_protocol::FrameType::CONTROL, ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1,
std::initializer_list<uint8_t>({0x00, 0x01}).begin(), 2);
return power_on_message;
} else {
static haier_protocol::HaierMessage power_off_message(
haier_protocol::FrameType::CONTROL, ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1,
std::initializer_list<uint8_t>({0x00, 0x00}).begin(), 2);
return power_off_message;
}
}
haier_protocol::HaierMessage HonClimate::get_control_message() { haier_protocol::HaierMessage HonClimate::get_control_message() {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl)); memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
bool has_hvac_settings = false; bool has_hvac_settings = false;
if (this->hvac_settings_.valid) { if (this->current_hvac_settings_.valid) {
has_hvac_settings = true; has_hvac_settings = true;
HvacSettings climate_control; HvacSettings &climate_control = this->current_hvac_settings_;
climate_control = this->hvac_settings_;
if (climate_control.mode.has_value()) { if (climate_control.mode.has_value()) {
switch (climate_control.mode.value()) { switch (climate_control.mode.value()) {
case CLIMATE_MODE_OFF: case CLIMATE_MODE_OFF:
@ -535,7 +538,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
} }
if (climate_control.target_temperature.has_value()) { if (climate_control.target_temperature.has_value()) {
float target_temp = climate_control.target_temperature.value(); float target_temp = climate_control.target_temperature.value();
out_data->set_point = ((int) target_temp) - 16; // set the temperature at our offset, subtract 16. out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0;
} }
if (out_data->ac_power == 0) { if (out_data->ac_power == 0) {
@ -587,50 +590,28 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
control_out_buffer[4] = 0; // This byte should be cleared before setting values control_out_buffer[4] = 0; // This byte should be cleared before setting values
out_data->display_status = this->display_status_ ? 1 : 0; out_data->display_status = this->display_status_ ? 1 : 0;
out_data->health_mode = this->health_mode_ ? 1 : 0; out_data->health_mode = this->health_mode_ ? 1 : 0;
switch (this->action_request_) { return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
case ActionRequest::START_SELF_CLEAN:
this->action_request_ = ActionRequest::NO_ACTION;
out_data->self_cleaning_status = 1;
out_data->steri_clean = 0;
out_data->set_point = 0x06;
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
out_data->light_status = 0;
break;
case ActionRequest::START_STERI_CLEAN:
this->action_request_ = ActionRequest::NO_ACTION;
out_data->self_cleaning_status = 0;
out_data->steri_clean = 1;
out_data->set_point = 0x06;
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
out_data->light_status = 0;
break;
default:
// No change
break;
}
return haier_protocol::HaierMessage((uint8_t) hon_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS,
control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
} }
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
if (size < sizeof(hon_protocol::HaierStatus)) if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_)
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
hon_protocol::HaierStatus packet; struct {
if (size < sizeof(hon_protocol::HaierStatus)) hon_protocol::HaierPacketControl control;
size = sizeof(hon_protocol::HaierStatus); hon_protocol::HaierPacketSensors sensors;
memcpy(&packet, packet_buffer, size); } packet;
memcpy(&packet.control, packet_buffer + 2, sizeof(hon_protocol::HaierPacketControl));
memcpy(&packet.sensors,
packet_buffer + 2 + sizeof(hon_protocol::HaierPacketControl) + this->extra_control_packet_bytes_,
sizeof(hon_protocol::HaierPacketSensors));
if (packet.sensors.error_status != 0) { if (packet.sensors.error_status != 0) {
ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status); ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status);
} }
if ((this->outdoor_sensor_ != nullptr) && (got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) { if ((this->outdoor_sensor_ != nullptr) &&
got_valid_outdoor_temp_ = true; (this->got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) {
this->got_valid_outdoor_temp_ = true;
float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET); float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET);
if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp)) if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp))
this->outdoor_sensor_->publish_state(otemp); this->outdoor_sensor_->publish_state(otemp);
@ -703,7 +684,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
// Do something only if display status changed // Do something only if display status changed
if (this->mode == CLIMATE_MODE_OFF) { if (this->mode == CLIMATE_MODE_OFF) {
// AC just turned on from remote need to turn off display // AC just turned on from remote need to turn off display
this->set_force_send_control_(true); this->force_send_control_ = true;
} else { } else {
this->display_status_ = disp_status; this->display_status_ = disp_status;
} }
@ -732,7 +713,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning); ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning);
if (new_cleaning == CleaningState::NO_CLEANING) { if (new_cleaning == CleaningState::NO_CLEANING) {
// Turning AC off after cleaning // Turning AC off after cleaning
this->action_request_ = ActionRequest::TURN_POWER_OFF; this->action_request_ =
PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional<haier_protocol::HaierMessage>()});
} }
this->cleaning_status_ = new_cleaning; this->cleaning_status_ = new_cleaning;
} }
@ -783,51 +765,257 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
should_publish = should_publish || (old_swing_mode != this->swing_mode); should_publish = should_publish || (old_swing_mode != this->swing_mode);
} }
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
if (this->forced_publish_ || should_publish) { if (should_publish) {
#if (HAIER_LOG_LEVEL > 4)
std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now();
#endif
this->publish_state(); this->publish_state();
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGV(TAG, "Publish delay: %lld ms",
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() -
_publish_start)
.count());
#endif
this->forced_publish_ = false;
} }
if (should_publish) { if (should_publish) {
ESP_LOGI(TAG, "HVAC values changed"); ESP_LOGI(TAG, "HVAC values changed");
} }
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG;
"HVAC Mode = 0x%X", packet.control.ac_mode); esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, esp_log_printf_(log_level, TAG, __LINE__, "Fan speed Status = 0x%X", packet.control.fan_mode);
"Fan speed Status = 0x%X", packet.control.fan_mode); esp_log_printf_(log_level, TAG, __LINE__, "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, esp_log_printf_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode);
"Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode); esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
"Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
"Set Point Status = 0x%X", packet.control.set_point);
return haier_protocol::HandlerError::HANDLER_OK; return haier_protocol::HandlerError::HANDLER_OK;
} }
bool HonClimate::is_message_invalid(uint8_t message_type) { void HonClimate::fill_control_messages_queue_() {
return message_type == (uint8_t) hon_protocol::FrameType::INVALID; static uint8_t one_buf[] = {0x00, 0x01};
static uint8_t zero_buf[] = {0x00, 0x00};
if (!this->current_hvac_settings_.valid && !this->force_send_control_)
return;
this->clear_control_messages_queue_();
HvacSettings climate_control;
climate_control = this->current_hvac_settings_;
// Beeper command
{
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::BEEPER_STATUS,
this->beeper_status_ ? zero_buf : one_buf, 2));
} }
// Health mode
void HonClimate::process_pending_action() { {
switch (this->action_request_) { this->control_messages_queue_.push(
case ActionRequest::START_SELF_CLEAN: haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
case ActionRequest::START_STERI_CLEAN: (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
// Will reset action with control message sending (uint8_t) hon_protocol::DataParameters::HEALTH_MODE,
this->set_phase(ProtocolPhases::SENDING_CONTROL); this->health_mode_ ? one_buf : zero_buf, 2));
}
// Climate mode
bool new_power = this->mode != CLIMATE_MODE_OFF;
uint8_t fan_mode_buf[] = {0x00, 0xFF};
uint8_t quiet_mode_buf[] = {0x00, 0xFF};
if (climate_control.mode.has_value()) {
uint8_t buffer[2] = {0x00, 0x00};
switch (climate_control.mode.value()) {
case CLIMATE_MODE_OFF:
new_power = false;
break;
case CLIMATE_MODE_HEAT_COOL:
new_power = true;
buffer[1] = (uint8_t) hon_protocol::ConditioningMode::AUTO;
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::AC_MODE,
buffer, 2));
fan_mode_buf[1] = this->other_modes_fan_speed_;
break;
case CLIMATE_MODE_HEAT:
new_power = true;
buffer[1] = (uint8_t) hon_protocol::ConditioningMode::HEAT;
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::AC_MODE,
buffer, 2));
fan_mode_buf[1] = this->other_modes_fan_speed_;
break;
case CLIMATE_MODE_DRY:
new_power = true;
buffer[1] = (uint8_t) hon_protocol::ConditioningMode::DRY;
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::AC_MODE,
buffer, 2));
fan_mode_buf[1] = this->other_modes_fan_speed_;
break;
case CLIMATE_MODE_FAN_ONLY:
new_power = true;
buffer[1] = (uint8_t) hon_protocol::ConditioningMode::FAN;
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::AC_MODE,
buffer, 2));
fan_mode_buf[1] = this->other_modes_fan_speed_; // Auto doesn't work in fan only mode
// Disabling eco mode for Fan only
quiet_mode_buf[1] = 0;
break;
case CLIMATE_MODE_COOL:
new_power = true;
buffer[1] = (uint8_t) hon_protocol::ConditioningMode::COOL;
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::AC_MODE,
buffer, 2));
fan_mode_buf[1] = this->other_modes_fan_speed_;
break; break;
default: default:
HaierClimateBase::process_pending_action(); ESP_LOGE("Control", "Unsupported climate mode");
break; break;
} }
} }
// Climate power
{
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::AC_POWER,
new_power ? one_buf : zero_buf, 2));
}
// CLimate preset
{
uint8_t fast_mode_buf[] = {0x00, 0xFF};
if (!new_power) {
// If AC is off - no presets allowed
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
} else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE:
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode
quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
fast_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_BOOST:
quiet_mode_buf[1] = 0x00;
// Boost is not supported in Fan only mode
fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
break;
default:
ESP_LOGE("Control", "Unsupported preset");
break;
}
}
if (quiet_mode_buf[1] != 0xFF) {
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::QUIET_MODE,
quiet_mode_buf, 2));
}
if (fast_mode_buf[1] != 0xFF) {
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::FAST_MODE,
fast_mode_buf, 2));
}
}
// Target temperature
if (climate_control.target_temperature.has_value()) {
uint8_t buffer[2] = {0x00, 0x00};
buffer[1] = ((uint8_t) climate_control.target_temperature.value()) - 16;
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::SET_POINT,
buffer, 2));
}
// Fan mode
if (climate_control.fan_mode.has_value()) {
switch (climate_control.fan_mode.value()) {
case CLIMATE_FAN_LOW:
fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_LOW;
break;
case CLIMATE_FAN_MEDIUM:
fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_MID;
break;
case CLIMATE_FAN_HIGH:
fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_HIGH;
break;
case CLIMATE_FAN_AUTO:
if (mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode
fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
break;
default:
ESP_LOGE("Control", "Unsupported fan mode");
break;
}
if (fan_mode_buf[1] != 0xFF) {
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::FAN_MODE,
fan_mode_buf, 2));
}
}
}
void HonClimate::clear_control_messages_queue_() {
while (!this->control_messages_queue_.empty())
this->control_messages_queue_.pop();
}
bool HonClimate::prepare_pending_action() {
switch (this->action_request_.value().action) {
case ActionRequest::START_SELF_CLEAN: {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
out_data->self_cleaning_status = 1;
out_data->steri_clean = 0;
out_data->set_point = 0x06;
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
out_data->light_status = 0;
this->action_request_.value().message = haier_protocol::HaierMessage(
haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS,
control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
}
return true;
case ActionRequest::START_STERI_CLEAN: {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
out_data->self_cleaning_status = 0;
out_data->steri_clean = 1;
out_data->set_point = 0x06;
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
out_data->light_status = 0;
this->action_request_.value().message = haier_protocol::HaierMessage(
haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS,
control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
}
return true;
default:
return HaierClimateBase::prepare_pending_action();
}
}
void HonClimate::process_protocol_reset() {
HaierClimateBase::process_protocol_reset();
if (this->outdoor_sensor_ != nullptr) {
this->outdoor_sensor_->publish_state(NAN);
}
this->got_valid_outdoor_temp_ = false;
this->hvac_hardware_info_.reset();
}
} // namespace haier } // namespace haier
} // namespace esphome } // namespace esphome

View file

@ -30,6 +30,8 @@ enum class CleaningState : uint8_t {
STERI_CLEAN = 2, STERI_CLEAN = 2,
}; };
enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER };
class HonClimate : public HaierClimateBase { class HonClimate : public HaierClimateBase {
public: public:
HonClimate(); HonClimate();
@ -48,44 +50,57 @@ class HonClimate : public HaierClimateBase {
CleaningState get_cleaning_status() const; CleaningState get_cleaning_status() const;
void start_self_cleaning(); void start_self_cleaning();
void start_steri_cleaning(); void start_steri_cleaning();
void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; };
void set_control_method(HonControlMethod method) { this->control_method_ = method; };
protected: protected:
void set_handlers() override; void set_handlers() override;
void process_phase(std::chrono::steady_clock::time_point now) override; void process_phase(std::chrono::steady_clock::time_point now) override;
haier_protocol::HaierMessage get_control_message() override; haier_protocol::HaierMessage get_control_message() override;
bool is_message_invalid(uint8_t message_type) override; haier_protocol::HaierMessage get_power_message(bool state) override;
void process_pending_action() override; bool prepare_pending_action() override;
void process_protocol_reset() override;
// Answers handlers // Answers handlers
haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size); const uint8_t *data, size_t data_size);
haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError get_device_id_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size); const uint8_t *data, size_t data_size);
haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type, const uint8_t *data,
size_t data_size); size_t data_size);
haier_protocol::HandlerError get_management_information_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError get_management_information_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size); const uint8_t *data, size_t data_size);
haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type,
const uint8_t *data, size_t data_size); haier_protocol::FrameType message_type,
haier_protocol::HandlerError get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type,
const uint8_t *data, size_t data_size); const uint8_t *data, size_t data_size);
// Helper functions // Helper functions
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
std::unique_ptr<uint8_t[]> last_status_message_; void fill_control_messages_queue_();
void clear_control_messages_queue_();
struct HardwareInfo {
std::string protocol_version_;
std::string software_version_;
std::string hardware_version_;
std::string device_name_;
bool functions_[5];
};
bool beeper_status_; bool beeper_status_;
CleaningState cleaning_status_; CleaningState cleaning_status_;
bool got_valid_outdoor_temp_; bool got_valid_outdoor_temp_;
AirflowVerticalDirection vertical_direction_; AirflowVerticalDirection vertical_direction_;
AirflowHorizontalDirection horizontal_direction_; AirflowHorizontalDirection horizontal_direction_;
bool hvac_hardware_info_available_; esphome::optional<HardwareInfo> hvac_hardware_info_;
std::string hvac_protocol_version_;
std::string hvac_software_version_;
std::string hvac_hardware_version_;
std::string hvac_device_name_;
bool hvac_functions_[5];
bool &use_crc_;
uint8_t active_alarms_[8]; uint8_t active_alarms_[8];
int extra_control_packet_bytes_;
HonControlMethod control_method_;
esphome::sensor::Sensor *outdoor_sensor_; esphome::sensor::Sensor *outdoor_sensor_;
std::queue<haier_protocol::HaierMessage> control_messages_queue_;
}; };
} // namespace haier } // namespace haier

View file

@ -35,6 +35,20 @@ enum class ConditioningMode : uint8_t {
FAN = 0x06 FAN = 0x06
}; };
enum class DataParameters : uint8_t {
AC_POWER = 0x01,
SET_POINT = 0x02,
AC_MODE = 0x04,
FAN_MODE = 0x05,
USE_FAHRENHEIT = 0x07,
TEN_DEGREE = 0x0A,
HEALTH_MODE = 0x0B,
BEEPER_STATUS = 0x16,
LOCK_REMOTE = 0x17,
QUIET_MODE = 0x19,
FAST_MODE = 0x1A,
};
enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 }; enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 };
enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, FAN_AUTO = 0x05 }; enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, FAN_AUTO = 0x05 };
@ -124,11 +138,7 @@ struct HaierPacketSensors {
uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step) uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step)
}; };
struct HaierStatus { constexpr size_t HAIER_STATUS_FRAME_SIZE = 2 + sizeof(HaierPacketControl) + sizeof(HaierPacketSensors);
uint16_t subcommand;
HaierPacketControl control;
HaierPacketSensors sensors;
};
struct DeviceVersionAnswer { struct DeviceVersionAnswer {
char protocol_version[8]; char protocol_version[8];
@ -140,76 +150,6 @@ struct DeviceVersionAnswer {
uint8_t functions[2]; uint8_t functions[2];
}; };
// In this section comments:
// - module is the ESP32 control module (communication module in Haier protocol document)
// - device is the conditioner control board (network appliances in Haier protocol document)
enum class FrameType : uint8_t {
CONTROL = 0x01, // Requests or sets one or multiple parameters (module <-> device, required)
STATUS = 0x02, // Contains one or multiple parameters values, usually answer to control frame (module <-> device,
// required)
INVALID = 0x03, // Communication error indication (module <-> device, required)
ALARM_STATUS = 0x04, // Alarm status report (module <-> device, interactive, required)
CONFIRM = 0x05, // Acknowledgment, usually used to confirm reception of frame if there is no special answer (module
// <-> device, required)
REPORT = 0x06, // Report frame (module <-> device, interactive, required)
STOP_FAULT_ALARM = 0x09, // Stop fault alarm frame (module -> device, interactive, required)
SYSTEM_DOWNLINK = 0x11, // System downlink frame (module -> device, optional)
DEVICE_UPLINK = 0x12, // Device uplink frame (module <- device , interactive, optional)
SYSTEM_QUERY = 0x13, // System query frame (module -> device, optional)
SYSTEM_QUERY_RESPONSE = 0x14, // System query response frame (module <- device , optional)
DEVICE_QUERY = 0x15, // Device query frame (module <- device, optional)
DEVICE_QUERY_RESPONSE = 0x16, // Device query response frame (module -> device, optional)
GROUP_COMMAND = 0x60, // Group command frame (module -> device, interactive, optional)
GET_DEVICE_VERSION = 0x61, // Requests device version (module -> device, required)
GET_DEVICE_VERSION_RESPONSE = 0x62, // Device version answer (module <- device, required_
GET_ALL_ADDRESSES = 0x67, // Requests all devices addresses (module -> device, interactive, optional)
GET_ALL_ADDRESSES_RESPONSE =
0x68, // Answer to request of all devices addresses (module <- device , interactive, optional)
HANDSET_CHANGE_NOTIFICATION = 0x69, // Handset change notification frame (module <- device , interactive, optional)
GET_DEVICE_ID = 0x70, // Requests Device ID (module -> device, required)
GET_DEVICE_ID_RESPONSE = 0x71, // Response to device ID request (module <- device , required)
GET_ALARM_STATUS = 0x73, // Alarm status request (module -> device, required)
GET_ALARM_STATUS_RESPONSE = 0x74, // Response to alarm status request (module <- device, required)
GET_DEVICE_CONFIGURATION = 0x7C, // Requests device configuration (module -> device, interactive, required)
GET_DEVICE_CONFIGURATION_RESPONSE =
0x7D, // Response to device configuration request (module <- device, interactive, required)
DOWNLINK_TRANSPARENT_TRANSMISSION = 0x8C, // Downlink transparent transmission (proxy data Haier cloud -> device)
// (module -> device, interactive, optional)
UPLINK_TRANSPARENT_TRANSMISSION = 0x8D, // Uplink transparent transmission (proxy data device -> Haier cloud) (module
// <- device, interactive, optional)
START_DEVICE_UPGRADE = 0xE1, // Initiate device OTA upgrade (module -> device, OTA required)
START_DEVICE_UPGRADE_RESPONSE = 0xE2, // Response to initiate device upgrade command (module <- device, OTA required)
GET_FIRMWARE_CONTENT = 0xE5, // Requests to send firmware (module <- device, OTA required)
GET_FIRMWARE_CONTENT_RESPONSE =
0xE6, // Response to send firmware request (module -> device, OTA required) (multipacket?)
CHANGE_BAUD_RATE = 0xE7, // Requests to change port baud rate (module <- device, OTA required)
CHANGE_BAUD_RATE_RESPONSE = 0xE8, // Response to change port baud rate request (module -> device, OTA required)
GET_SUBBOARD_INFO = 0xE9, // Requests subboard information (module -> device, required)
GET_SUBBOARD_INFO_RESPONSE = 0xEA, // Response to subboard information request (module <- device, required)
GET_HARDWARE_INFO = 0xEB, // Requests information about device and subboard (module -> device, required)
GET_HARDWARE_INFO_RESPONSE = 0xEC, // Response to hardware information request (module <- device, required)
GET_UPGRADE_RESULT = 0xED, // Requests result of the firmware update (module <- device, OTA required)
GET_UPGRADE_RESULT_RESPONSE = 0xEF, // Response to firmware update results request (module -> device, OTA required)
GET_NETWORK_STATUS = 0xF0, // Requests network status (module <- device, interactive, optional)
GET_NETWORK_STATUS_RESPONSE = 0xF1, // Response to network status request (module -> device, interactive, optional)
START_WIFI_CONFIGURATION = 0xF2, // Starts WiFi configuration procedure (module <- device, interactive, required)
START_WIFI_CONFIGURATION_RESPONSE =
0xF3, // Response to start WiFi configuration request (module -> device, interactive, required)
STOP_WIFI_CONFIGURATION = 0xF4, // Stop WiFi configuration procedure (module <- device, interactive, required)
STOP_WIFI_CONFIGURATION_RESPONSE =
0xF5, // Response to stop WiFi configuration request (module -> device, interactive, required)
REPORT_NETWORK_STATUS = 0xF7, // Reports network status (module -> device, required)
CLEAR_CONFIGURATION = 0xF8, // Request to clear module configuration (module <- device, interactive, optional)
BIG_DATA_REPORT_CONFIGURATION =
0xFA, // Configuration for autoreport device full status (module -> device, interactive, optional)
BIG_DATA_REPORT_CONFIGURATION_RESPONSE =
0xFB, // Response to set big data configuration (module <- device, interactive, optional)
GET_MANAGEMENT_INFORMATION = 0xFC, // Request management information from device (module -> device, required)
GET_MANAGEMENT_INFORMATION_RESPONSE =
0xFD, // Response to management information request (module <- device, required)
WAKE_UP = 0xFE, // Request to wake up (module <-> device, optional)
};
enum class SubcommandsControl : uint16_t { enum class SubcommandsControl : uint16_t {
GET_PARAMETERS = 0x4C01, // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...) GET_PARAMETERS = 0x4C01, // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...)
GET_USER_DATA = 0x4D01, // Request all user data from device (packet content: None) GET_USER_DATA = 0x4D01, // Request all user data from device (packet content: None)

View file

@ -12,21 +12,28 @@ namespace haier {
static const char *const TAG = "haier.climate"; static const char *const TAG = "haier.climate";
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5;
constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500);
constexpr uint8_t INIT_REQUESTS_RETRY = 2;
constexpr std::chrono::milliseconds INIT_REQUESTS_RETRY_INTERVAL = std::chrono::milliseconds(2000);
Smartair2Climate::Smartair2Climate() Smartair2Climate::Smartair2Climate() {
: last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]), timeouts_counter_(0) {} last_status_message_ = std::unique_ptr<uint8_t[]>(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]);
}
haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError Smartair2Climate::status_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size) { const uint8_t *data, size_t data_size) {
haier_protocol::HandlerError result = haier_protocol::HandlerError result =
this->answer_preprocess_(request_type, (uint8_t) smartair2_protocol::FrameType::CONTROL, message_type, this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type,
(uint8_t) smartair2_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
if (result == haier_protocol::HandlerError::HANDLER_OK) { if (result == haier_protocol::HandlerError::HANDLER_OK) {
result = this->process_status_message_(data, data_size); result = this->process_status_message_(data, data_size);
if (result != haier_protocol::HandlerError::HANDLER_OK) { if (result != haier_protocol::HandlerError::HANDLER_OK) {
ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE this->reset_phase_();
: ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); this->action_request_.reset();
this->force_send_control_ = false;
} else { } else {
if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) { if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) {
memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl)); memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl));
@ -34,36 +41,45 @@ haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_t
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
sizeof(smartair2_protocol::HaierPacketControl)); sizeof(smartair2_protocol::HaierPacketControl));
} }
if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
ESP_LOGI(TAG, "First HVAC status received"); ESP_LOGI(TAG, "First HVAC status received");
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) { break;
case ProtocolPhases::SENDING_ACTION_COMMAND:
// Do nothing, phase will be changed in process_phase
break;
case ProtocolPhases::SENDING_STATUS_REQUEST:
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { break;
case ProtocolPhases::SENDING_CONTROL:
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
this->set_force_send_control_(false); this->force_send_control_ = false;
if (this->hvac_settings_.valid) if (this->current_hvac_settings_.valid)
this->hvac_settings_.reset(); this->current_hvac_settings_.reset();
break;
default:
break;
} }
} }
return result; return result;
} else { } else {
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE this->action_request_.reset();
: ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); this->force_send_control_ = false;
this->reset_phase_();
return result; return result;
} }
} }
haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_(uint8_t request_type, haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_(
uint8_t message_type, haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data,
const uint8_t *data,
size_t data_size) { size_t data_size) {
if (request_type != (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION) if (request_type != haier_protocol::FrameType::GET_DEVICE_VERSION)
return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
if (ProtocolPhases::WAITING_INIT_1_ANSWER != this->protocol_phase_) if (ProtocolPhases::SENDING_INIT_1 != this->protocol_phase_)
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
// Invalid packet is expected answer // Invalid packet is expected answer
if ((message_type == (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) && if ((message_type == haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) &&
((data[37] & 0x04) != 0)) { ((data[37] & 0x04) != 0)) {
ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the hOn protocol " ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the hOn protocol "
"instead of smartAir2"); "instead of smartAir2");
@ -72,58 +88,35 @@ haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler
return haier_protocol::HandlerError::HANDLER_OK; return haier_protocol::HandlerError::HANDLER_OK;
} }
haier_protocol::HandlerError Smartair2Climate::report_network_status_answer_handler_(uint8_t request_type, haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cycle_for_init_(
uint8_t message_type, haier_protocol::FrameType message_type) {
const uint8_t *data,
size_t data_size) {
haier_protocol::HandlerError result = this->answer_preprocess_(
request_type, (uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS, message_type,
(uint8_t) smartair2_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
this->set_phase(ProtocolPhases::IDLE);
return result;
}
haier_protocol::HandlerError Smartair2Climate::initial_messages_timeout_handler_(uint8_t message_type) {
if (this->protocol_phase_ >= ProtocolPhases::IDLE) if (this->protocol_phase_ >= ProtocolPhases::IDLE)
return HaierClimateBase::timeout_default_handler_(message_type); return HaierClimateBase::timeout_default_handler_(message_type);
this->timeouts_counter_++; ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type,
ESP_LOGI(TAG, "Answer timeout for command %02X, phase %d, timeout counter %d", message_type, phase_to_string_(this->protocol_phase_));
(int) this->protocol_phase_, this->timeouts_counter_);
if (this->timeouts_counter_ >= 3) {
ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1);
if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST)
new_phase = ProtocolPhases::SENDING_INIT_1; new_phase = ProtocolPhases::SENDING_INIT_1;
this->set_phase(new_phase); this->set_phase(new_phase);
} else {
// Returning to the previous state to try again
this->set_phase((ProtocolPhases) ((int) this->protocol_phase_ - 1));
}
return haier_protocol::HandlerError::HANDLER_OK; return haier_protocol::HandlerError::HANDLER_OK;
} }
void Smartair2Climate::set_handlers() { void Smartair2Climate::set_handlers() {
// Set handlers // Set handlers
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION), haier_protocol::FrameType::GET_DEVICE_VERSION,
std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1, std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (smartair2_protocol::FrameType::CONTROL), haier_protocol::FrameType::CONTROL,
std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4)); std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), haier_protocol::FrameType::REPORT_NETWORK_STATUS,
std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1, std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_timeout_handler( this->haier_protocol_.set_default_timeout_handler(
(uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_ID), std::bind(&Smartair2Climate::messages_timeout_handler_with_cycle_for_init_, this, std::placeholders::_1));
std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1));
this->haier_protocol_.set_timeout_handler(
(uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION),
std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1));
this->haier_protocol_.set_timeout_handler(
(uint8_t) (smartair2_protocol::FrameType::CONTROL),
std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1));
} }
void Smartair2Climate::dump_config() { void Smartair2Climate::dump_config() {
@ -134,9 +127,7 @@ void Smartair2Climate::dump_config() {
void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) { void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) {
switch (this->protocol_phase_) { switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_INIT_1: case ProtocolPhases::SENDING_INIT_1:
if (this->can_send_message() && if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) {
(((this->timeouts_counter_ == 0) && (this->is_protocol_initialisation_interval_exceeded_(now))) ||
((this->timeouts_counter_ > 0) && (this->is_message_interval_exceeded_(now))))) {
// Indicate device capabilities: // Indicate device capabilities:
// bit 0 - if 1 module support interactive mode // bit 0 - if 1 module support interactive mode
// bit 1 - if 1 module support controller-device mode // bit 1 - if 1 module support controller-device mode
@ -145,92 +136,65 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
// bit 4..bit 15 - not used // bit 4..bit 15 - not used
uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; uint8_t module_capabilities[2] = {0b00000000, 0b00000111};
static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST(
(uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities));
sizeof(module_capabilities)); this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL);
this->send_message_(DEVICE_VERSION_REQUEST, false);
this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER);
} }
break; break;
case ProtocolPhases::SENDING_INIT_2: case ProtocolPhases::SENDING_INIT_2:
case ProtocolPhases::WAITING_INIT_2_ANSWER:
this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
break; break;
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
case ProtocolPhases::SENDING_STATUS_REQUEST: case ProtocolPhases::SENDING_STATUS_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL, static const haier_protocol::HaierMessage STATUS_REQUEST(haier_protocol::FrameType::CONTROL, 0x4D01);
0x4D01); if (this->protocol_phase_ == ProtocolPhases::SENDING_FIRST_STATUS_REQUEST) {
this->send_message_(STATUS_REQUEST, false); this->send_message_(STATUS_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL);
} else {
this->send_message_(STATUS_REQUEST, this->use_crc_);
}
this->last_status_request_ = now; this->last_status_request_ = now;
this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1));
} }
break; break;
#ifdef USE_WIFI #ifdef USE_WIFI
case ProtocolPhases::SENDING_SIGNAL_LEVEL: case ProtocolPhases::SENDING_SIGNAL_LEVEL:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
this->send_message_( this->send_message_(this->get_wifi_signal_message_(), this->use_crc_);
this->get_wifi_signal_message_((uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), false);
this->last_signal_request_ = now; this->last_signal_request_ = now;
this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
} }
break; break;
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
break;
#else #else
case ProtocolPhases::SENDING_SIGNAL_LEVEL: case ProtocolPhases::SENDING_SIGNAL_LEVEL:
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
break; break;
#endif #endif
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL);
break; break;
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER:
this->set_phase(ProtocolPhases::SENDING_INIT_1); this->set_phase(ProtocolPhases::SENDING_INIT_1);
break; break;
case ProtocolPhases::SENDING_CONTROL: case ProtocolPhases::SENDING_CONTROL:
if (this->first_control_attempt_) { if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
this->control_request_timestamp_ = now; ESP_LOGI(TAG, "Sending control packet");
this->first_control_attempt_ = false; this->send_message_(get_control_message(), this->use_crc_, CONTROL_MESSAGE_RETRIES,
CONTROL_MESSAGE_RETRIES_INTERVAL);
} }
if (this->is_control_message_timeout_exceeded_(now)) { break;
ESP_LOGW(TAG, "Sending control packet timeout!"); case ProtocolPhases::SENDING_ACTION_COMMAND:
this->set_force_send_control_(false); if (this->action_request_.has_value()) {
if (this->hvac_settings_.valid) if (this->action_request_.value().message.has_value()) {
this->hvac_settings_.reset(); this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
this->forced_request_status_ = true; this->action_request_.value().message.reset();
this->forced_publish_ = true; } else {
// Message already sent, reseting request and return to idle
this->action_request_.reset();
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
} else if (this->can_send_message() && this->is_control_message_interval_exceeded_(
now)) // Using CONTROL_MESSAGES_INTERVAL_MS to speedup requests
{
haier_protocol::HaierMessage control_message = get_control_message();
this->send_message_(control_message, false);
ESP_LOGI(TAG, "Control packet sent");
this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER);
} }
break; } else {
case ProtocolPhases::SENDING_POWER_ON_COMMAND: ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!");
case ProtocolPhases::SENDING_POWER_OFF_COMMAND: this->set_phase(ProtocolPhases::IDLE);
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
haier_protocol::HaierMessage power_cmd(
(uint8_t) smartair2_protocol::FrameType::CONTROL,
this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND ? 0x4D02 : 0x4D03);
this->send_message_(power_cmd, false);
this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND
? ProtocolPhases::WAITING_POWER_ON_ANSWER
: ProtocolPhases::WAITING_POWER_OFF_ANSWER);
} }
break; break;
case ProtocolPhases::WAITING_INIT_1_ANSWER:
case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER:
case ProtocolPhases::WAITING_STATUS_ANSWER:
case ProtocolPhases::WAITING_CONTROL_ANSWER:
case ProtocolPhases::WAITING_POWER_ON_ANSWER:
case ProtocolPhases::WAITING_POWER_OFF_ANSWER:
break;
case ProtocolPhases::IDLE: { case ProtocolPhases::IDLE: {
if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST);
@ -245,55 +209,55 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
} break; } break;
default: default:
// Shouldn't get here // Shouldn't get here
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
#else
ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_);
#endif
this->set_phase(ProtocolPhases::SENDING_INIT_1); this->set_phase(ProtocolPhases::SENDING_INIT_1);
break; break;
} }
} }
haier_protocol::HaierMessage Smartair2Climate::get_power_message(bool state) {
if (state) {
static haier_protocol::HaierMessage power_on_message(haier_protocol::FrameType::CONTROL, 0x4D02);
return power_on_message;
} else {
static haier_protocol::HaierMessage power_off_message(haier_protocol::FrameType::CONTROL, 0x4D03);
return power_off_message;
}
}
haier_protocol::HaierMessage Smartair2Climate::get_control_message() { haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)]; uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(smartair2_protocol::HaierPacketControl)); memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(smartair2_protocol::HaierPacketControl));
smartair2_protocol::HaierPacketControl *out_data = (smartair2_protocol::HaierPacketControl *) control_out_buffer; smartair2_protocol::HaierPacketControl *out_data = (smartair2_protocol::HaierPacketControl *) control_out_buffer;
out_data->cntrl = 0; out_data->cntrl = 0;
if (this->hvac_settings_.valid) { if (this->current_hvac_settings_.valid) {
HvacSettings climate_control; HvacSettings &climate_control = this->current_hvac_settings_;
climate_control = this->hvac_settings_;
if (climate_control.mode.has_value()) { if (climate_control.mode.has_value()) {
switch (climate_control.mode.value()) { switch (climate_control.mode.value()) {
case CLIMATE_MODE_OFF: case CLIMATE_MODE_OFF:
out_data->ac_power = 0; out_data->ac_power = 0;
break; break;
case CLIMATE_MODE_HEAT_COOL: case CLIMATE_MODE_HEAT_COOL:
out_data->ac_power = 1; out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO;
out_data->fan_mode = this->other_modes_fan_speed_; out_data->fan_mode = this->other_modes_fan_speed_;
break; break;
case CLIMATE_MODE_HEAT: case CLIMATE_MODE_HEAT:
out_data->ac_power = 1; out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT;
out_data->fan_mode = this->other_modes_fan_speed_; out_data->fan_mode = this->other_modes_fan_speed_;
break; break;
case CLIMATE_MODE_DRY: case CLIMATE_MODE_DRY:
out_data->ac_power = 1; out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY;
out_data->fan_mode = this->other_modes_fan_speed_; out_data->fan_mode = this->other_modes_fan_speed_;
break; break;
case CLIMATE_MODE_FAN_ONLY: case CLIMATE_MODE_FAN_ONLY:
out_data->ac_power = 1; out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::FAN; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::FAN;
out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode
break; break;
case CLIMATE_MODE_COOL: case CLIMATE_MODE_COOL:
out_data->ac_power = 1; out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL;
@ -327,32 +291,49 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
} }
// Set swing mode // Set swing mode
if (climate_control.swing_mode.has_value()) { if (climate_control.swing_mode.has_value()) {
if (this->use_alternative_swing_control_) {
switch (climate_control.swing_mode.value()) {
case CLIMATE_SWING_OFF:
out_data->swing_mode = 0;
break;
case CLIMATE_SWING_VERTICAL:
out_data->swing_mode = 1;
break;
case CLIMATE_SWING_HORIZONTAL:
out_data->swing_mode = 2;
break;
case CLIMATE_SWING_BOTH:
out_data->swing_mode = 3;
break;
}
} else {
switch (climate_control.swing_mode.value()) { switch (climate_control.swing_mode.value()) {
case CLIMATE_SWING_OFF: case CLIMATE_SWING_OFF:
out_data->use_swing_bits = 0; out_data->use_swing_bits = 0;
out_data->swing_both = 0; out_data->swing_mode = 0;
break; break;
case CLIMATE_SWING_VERTICAL: case CLIMATE_SWING_VERTICAL:
out_data->swing_both = 0; out_data->swing_mode = 0;
out_data->vertical_swing = 1; out_data->vertical_swing = 1;
out_data->horizontal_swing = 0; out_data->horizontal_swing = 0;
break; break;
case CLIMATE_SWING_HORIZONTAL: case CLIMATE_SWING_HORIZONTAL:
out_data->swing_both = 0; out_data->swing_mode = 0;
out_data->vertical_swing = 0; out_data->vertical_swing = 0;
out_data->horizontal_swing = 1; out_data->horizontal_swing = 1;
break; break;
case CLIMATE_SWING_BOTH: case CLIMATE_SWING_BOTH:
out_data->swing_both = 1; out_data->swing_mode = 1;
out_data->use_swing_bits = 0; out_data->use_swing_bits = 0;
out_data->vertical_swing = 0; out_data->vertical_swing = 0;
out_data->horizontal_swing = 0; out_data->horizontal_swing = 0;
break; break;
} }
} }
}
if (climate_control.target_temperature.has_value()) { if (climate_control.target_temperature.has_value()) {
float target_temp = climate_control.target_temperature.value(); float target_temp = climate_control.target_temperature.value();
out_data->set_point = target_temp - 16; // set the temperature with offset 16 out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0;
} }
if (out_data->ac_power == 0) { if (out_data->ac_power == 0) {
@ -383,7 +364,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
} }
out_data->display_status = this->display_status_ ? 0 : 1; out_data->display_status = this->display_status_ ? 0 : 1;
out_data->health_mode = this->health_mode_ ? 1 : 0; out_data->health_mode = this->health_mode_ ? 1 : 0;
return haier_protocol::HaierMessage((uint8_t) smartair2_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer, return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer,
sizeof(smartair2_protocol::HaierPacketControl)); sizeof(smartair2_protocol::HaierPacketControl));
} }
@ -459,13 +440,19 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
// Do something only if display status changed // Do something only if display status changed
if (this->mode == CLIMATE_MODE_OFF) { if (this->mode == CLIMATE_MODE_OFF) {
// AC just turned on from remote need to turn off display // AC just turned on from remote need to turn off display
this->set_force_send_control_(true); this->force_send_control_ = true;
} else { } else {
this->display_status_ = disp_status; this->display_status_ = disp_status;
} }
} }
} }
} }
{
// Health mode
bool old_health_mode = this->health_mode_;
this->health_mode_ = packet.control.health_mode == 1;
should_publish = should_publish || (old_health_mode != this->health_mode_);
}
{ {
// Climate mode // Climate mode
ClimateMode old_mode = this->mode; ClimateMode old_mode = this->mode;
@ -493,16 +480,26 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
} }
should_publish = should_publish || (old_mode != this->mode); should_publish = should_publish || (old_mode != this->mode);
} }
{
// Health mode
bool old_health_mode = this->health_mode_;
this->health_mode_ = packet.control.health_mode == 1;
should_publish = should_publish || (old_health_mode != this->health_mode_);
}
{ {
// Swing mode // Swing mode
ClimateSwingMode old_swing_mode = this->swing_mode; ClimateSwingMode old_swing_mode = this->swing_mode;
if (packet.control.swing_both == 0) { if (this->use_alternative_swing_control_) {
switch (packet.control.swing_mode) {
case 1:
this->swing_mode = CLIMATE_SWING_VERTICAL;
break;
case 2:
this->swing_mode = CLIMATE_SWING_HORIZONTAL;
break;
case 3:
this->swing_mode = CLIMATE_SWING_BOTH;
break;
default:
this->swing_mode = CLIMATE_SWING_OFF;
break;
}
} else {
if (packet.control.swing_mode == 0) {
if (packet.control.vertical_swing != 0) { if (packet.control.vertical_swing != 0) {
this->swing_mode = CLIMATE_SWING_VERTICAL; this->swing_mode = CLIMATE_SWING_VERTICAL;
} else if (packet.control.horizontal_swing != 0) { } else if (packet.control.horizontal_swing != 0) {
@ -513,50 +510,27 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
} else { } else {
swing_mode = CLIMATE_SWING_BOTH; swing_mode = CLIMATE_SWING_BOTH;
} }
}
should_publish = should_publish || (old_swing_mode != this->swing_mode); should_publish = should_publish || (old_swing_mode != this->swing_mode);
} }
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
if (this->forced_publish_ || should_publish) { if (should_publish) {
#if (HAIER_LOG_LEVEL > 4)
std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now();
#endif
this->publish_state(); this->publish_state();
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGV(TAG, "Publish delay: %lld ms",
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() -
_publish_start)
.count());
#endif
this->forced_publish_ = false;
} }
if (should_publish) { if (should_publish) {
ESP_LOGI(TAG, "HVAC values changed"); ESP_LOGI(TAG, "HVAC values changed");
} }
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG;
"HVAC Mode = 0x%X", packet.control.ac_mode); esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, esp_log_printf_(log_level, TAG, __LINE__, "Fan speed Status = 0x%X", packet.control.fan_mode);
"Fan speed Status = 0x%X", packet.control.fan_mode); esp_log_printf_(log_level, TAG, __LINE__, "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, esp_log_printf_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing);
"Horizontal Swing Status = 0x%X", packet.control.horizontal_swing); esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
"Vertical Swing Status = 0x%X", packet.control.vertical_swing);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
"Set Point Status = 0x%X", packet.control.set_point);
return haier_protocol::HandlerError::HANDLER_OK; return haier_protocol::HandlerError::HANDLER_OK;
} }
bool Smartair2Climate::is_message_invalid(uint8_t message_type) { void Smartair2Climate::set_alternative_swing_control(bool swing_control) {
return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID; this->use_alternative_swing_control_ = swing_control;
}
void Smartair2Climate::set_phase(HaierClimateBase::ProtocolPhases phase) {
int old_phase = (int) this->protocol_phase_;
int new_phase = (int) phase;
int min_p = std::min(old_phase, new_phase);
int max_p = std::max(old_phase, new_phase);
if ((min_p % 2 != 0) || (max_p - min_p > 1))
this->timeouts_counter_ = 0;
HaierClimateBase::set_phase(phase);
} }
} // namespace haier } // namespace haier

View file

@ -13,27 +13,27 @@ class Smartair2Climate : public HaierClimateBase {
Smartair2Climate &operator=(const Smartair2Climate &) = delete; Smartair2Climate &operator=(const Smartair2Climate &) = delete;
~Smartair2Climate(); ~Smartair2Climate();
void dump_config() override; void dump_config() override;
void set_alternative_swing_control(bool swing_control);
protected: protected:
void set_handlers() override; void set_handlers() override;
void process_phase(std::chrono::steady_clock::time_point now) override; void process_phase(std::chrono::steady_clock::time_point now) override;
haier_protocol::HaierMessage get_power_message(bool state) override;
haier_protocol::HaierMessage get_control_message() override; haier_protocol::HaierMessage get_control_message() override;
bool is_message_invalid(uint8_t message_type) override; // Answer handlers
void set_phase(HaierClimateBase::ProtocolPhases phase) override; haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type,
// Answer and timeout handlers haier_protocol::FrameType message_type, const uint8_t *data,
haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data,
size_t data_size); size_t data_size);
haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size); const uint8_t *data, size_t data_size);
haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError get_device_id_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size); const uint8_t *data, size_t data_size);
haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError messages_timeout_handler_with_cycle_for_init_(haier_protocol::FrameType message_type);
const uint8_t *data, size_t data_size);
haier_protocol::HandlerError initial_messages_timeout_handler_(uint8_t message_type);
// Helper functions // Helper functions
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
std::unique_ptr<uint8_t[]> last_status_message_; bool use_alternative_swing_control_;
unsigned int timeouts_counter_;
}; };
} // namespace haier } // namespace haier

View file

@ -41,8 +41,9 @@ struct HaierPacketControl {
// 24 // 24
uint8_t : 8; uint8_t : 8;
// 25 // 25
uint8_t swing_both; // If 1 - swing both direction, if 0 - horizontal_swing and vertical_swing define uint8_t swing_mode; // In normal mode: If 1 - swing both direction, if 0 - horizontal_swing and
// vertical/horizontal/off // vertical_swing define vertical/horizontal/off
// In alternative mode: 0 - off, 01 - vertical, 02 - horizontal, 03 - both
// 26 // 26
uint8_t : 3; uint8_t : 3;
uint8_t use_fahrenheit : 1; uint8_t use_fahrenheit : 1;
@ -82,19 +83,6 @@ struct HaierStatus {
HaierPacketControl control; HaierPacketControl control;
}; };
enum class FrameType : uint8_t {
CONTROL = 0x01,
STATUS = 0x02,
INVALID = 0x03,
CONFIRM = 0x05,
GET_DEVICE_VERSION = 0x61,
GET_DEVICE_VERSION_RESPONSE = 0x62,
GET_DEVICE_ID = 0x70,
GET_DEVICE_ID_RESPONSE = 0x71,
REPORT_NETWORK_STATUS = 0xF7,
NO_COMMAND = 0xFF,
};
} // namespace smartair2_protocol } // namespace smartair2_protocol
} // namespace haier } // namespace haier
} // namespace esphome } // namespace esphome

View file

@ -3,6 +3,7 @@ import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.automation import maybe_simple_id from esphome.automation import maybe_simple_id
from esphome.components import fan, output from esphome.components import fan, output
from esphome.components.fan import validate_preset_modes
from esphome.const import ( from esphome.const import (
CONF_ID, CONF_ID,
CONF_DECAY_MODE, CONF_DECAY_MODE,
@ -10,6 +11,7 @@ from esphome.const import (
CONF_PIN_A, CONF_PIN_A,
CONF_PIN_B, CONF_PIN_B,
CONF_ENABLE_PIN, CONF_ENABLE_PIN,
CONF_PRESET_MODES,
) )
from .. import hbridge_ns from .. import hbridge_ns
@ -28,7 +30,6 @@ DECAY_MODE_OPTIONS = {
# Actions # Actions
BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action)
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
{ {
cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan),
@ -39,6 +40,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
), ),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
@ -69,3 +71,6 @@ async def to_code(config):
if CONF_ENABLE_PIN in config: if CONF_ENABLE_PIN in config:
enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN]) enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN])
cg.add(var.set_enable_pin(enable_pin)) cg.add(var.set_enable_pin(enable_pin))
if CONF_PRESET_MODES in config:
cg.add(var.set_preset_modes(config[CONF_PRESET_MODES]))

View file

@ -33,7 +33,12 @@ void HBridgeFan::setup() {
restore->apply(*this); restore->apply(*this);
this->write_state_(); this->write_state_();
} }
// Construct traits
this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
this->traits_.set_supported_preset_modes(this->preset_modes_);
} }
void HBridgeFan::dump_config() { void HBridgeFan::dump_config() {
LOG_FAN("", "H-Bridge Fan", this); LOG_FAN("", "H-Bridge Fan", this);
if (this->decay_mode_ == DECAY_MODE_SLOW) { if (this->decay_mode_ == DECAY_MODE_SLOW) {
@ -42,9 +47,7 @@ void HBridgeFan::dump_config() {
ESP_LOGCONFIG(TAG, " Decay Mode: Fast"); ESP_LOGCONFIG(TAG, " Decay Mode: Fast");
} }
} }
fan::FanTraits HBridgeFan::get_traits() {
return fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
}
void HBridgeFan::control(const fan::FanCall &call) { void HBridgeFan::control(const fan::FanCall &call) {
if (call.get_state().has_value()) if (call.get_state().has_value())
this->state = *call.get_state(); this->state = *call.get_state();
@ -54,10 +57,12 @@ void HBridgeFan::control(const fan::FanCall &call) {
this->oscillating = *call.get_oscillating(); this->oscillating = *call.get_oscillating();
if (call.get_direction().has_value()) if (call.get_direction().has_value())
this->direction = *call.get_direction(); this->direction = *call.get_direction();
this->preset_mode = call.get_preset_mode();
this->write_state_(); this->write_state_();
this->publish_state(); this->publish_state();
} }
void HBridgeFan::write_state_() { void HBridgeFan::write_state_() {
float speed = this->state ? static_cast<float>(this->speed) / static_cast<float>(this->speed_count_) : 0.0f; float speed = this->state ? static_cast<float>(this->speed) / static_cast<float>(this->speed_count_) : 0.0f;
if (speed == 0.0f) { // off means idle if (speed == 0.0f) { // off means idle

View file

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <set>
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/components/output/binary_output.h" #include "esphome/components/output/binary_output.h"
#include "esphome/components/output/float_output.h" #include "esphome/components/output/float_output.h"
@ -20,10 +22,11 @@ class HBridgeFan : public Component, public fan::Fan {
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
void set_preset_modes(const std::set<std::string> &presets) { preset_modes_ = presets; }
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
fan::FanTraits get_traits() override; fan::FanTraits get_traits() override { return this->traits_; }
fan::FanCall brake(); fan::FanCall brake();
@ -34,6 +37,8 @@ class HBridgeFan : public Component, public fan::Fan {
output::BinaryOutput *oscillating_{nullptr}; output::BinaryOutput *oscillating_{nullptr};
int speed_count_{}; int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW}; DecayMode decay_mode_{DECAY_MODE_SLOW};
fan::FanTraits traits_;
std::set<std::string> preset_modes_{};
void control(const fan::FanCall &call) override; void control(const fan::FanCall &call) override;
void write_state_(); void write_state_();

Some files were not shown because too many files have changed in this diff Show more