Merge branch 'dev' into optolink

This commit is contained in:
j0ta29 2023-11-23 21:51:49 +01:00 committed by GitHub
commit 212ca30505
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 4186 additions and 2604 deletions

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

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

View file

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

View file

@ -38,6 +38,7 @@ RUN \
openssh-client=1:9.2p1-2+deb12u1 \ openssh-client=1:9.2p1-2+deb12u1 \
python3-cffi=1.15.1-5 \ python3-cffi=1.15.1-5 \
libcairo2=1.16.0-7 \ libcairo2=1.16.0-7 \
libmagic1=1:5.44-3 \
patch=2.7.6-7; \ patch=2.7.6-7; \
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
@ -48,6 +49,8 @@ RUN \
libfreetype-dev=2.12.1+dfsg-5 \ libfreetype-dev=2.12.1+dfsg-5 \
libssl-dev=3.0.11-1~deb12u2 \ libssl-dev=3.0.11-1~deb12u2 \
libffi-dev=3.4.4-1 \ libffi-dev=3.4.4-1 \
libopenjp2-7=2.5.0-2 \
libtiff6=4.5.0-6 \
cargo=0.66.0+ds1-1 \ cargo=0.66.0+ds1-1 \
pkg-config=1.8.1-1 \ pkg-config=1.8.1-1 \
gcc-arm-linux-gnueabihf=4:12.2.0-3; \ gcc-arm-linux-gnueabihf=4:12.2.0-3; \
@ -68,7 +71,7 @@ ENV \
# See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian # See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian
RUN \ RUN \
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3; \ ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \
fi fi
RUN \ RUN \

View file

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

View file

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

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

@ -3,23 +3,26 @@ from typing import Union, Optional
from pathlib import Path from pathlib import Path
import logging import logging
import os import os
import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p
from esphome.const import ( from esphome.const import (
CONF_ADVANCED,
CONF_BOARD, CONF_BOARD,
CONF_COMPONENTS, CONF_COMPONENTS,
CONF_ESPHOME,
CONF_FRAMEWORK, CONF_FRAMEWORK,
CONF_IGNORE_EFUSE_MAC_CRC,
CONF_NAME, CONF_NAME,
CONF_PATH,
CONF_PLATFORMIO_OPTIONS,
CONF_REF,
CONF_REFRESH,
CONF_SOURCE, CONF_SOURCE,
CONF_TYPE, CONF_TYPE,
CONF_URL,
CONF_VARIANT, CONF_VARIANT,
CONF_VERSION, CONF_VERSION,
CONF_ADVANCED,
CONF_REFRESH,
CONF_PATH,
CONF_URL,
CONF_REF,
CONF_IGNORE_EFUSE_MAC_CRC,
KEY_CORE, KEY_CORE,
KEY_FRAMEWORK_VERSION, KEY_FRAMEWORK_VERSION,
KEY_NAME, KEY_NAME,
@ -327,6 +330,32 @@ def _detect_variant(value):
return value return value
def final_validate(config):
if CONF_PLATFORMIO_OPTIONS not in fv.full_config.get()[CONF_ESPHOME]:
return config
pio_flash_size_key = "board_upload.flash_size"
pio_partitions_key = "board_build.partitions"
if (
CONF_PARTITIONS in config
and pio_partitions_key
in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS]
):
raise cv.Invalid(
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
)
if (
pio_flash_size_key
in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS]
):
raise cv.Invalid(
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
)
return config
CONF_PLATFORM_VERSION = "platform_version" CONF_PLATFORM_VERSION = "platform_version"
ARDUINO_FRAMEWORK_SCHEMA = cv.All( ARDUINO_FRAMEWORK_SCHEMA = cv.All(
@ -386,10 +415,24 @@ FRAMEWORK_SCHEMA = cv.typed_schema(
) )
FLASH_SIZES = [
"2MB",
"4MB",
"8MB",
"16MB",
"32MB",
]
CONF_FLASH_SIZE = "flash_size"
CONF_PARTITIONS = "partitions"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.Required(CONF_BOARD): cv.string_strict, cv.Required(CONF_BOARD): cv.string_strict,
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
*FLASH_SIZES, upper=True
),
cv.Optional(CONF_PARTITIONS): cv.file_,
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA,
} }
@ -399,8 +442,12 @@ CONFIG_SCHEMA = cv.All(
) )
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
async def to_code(config): async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
cg.add_build_flag("-DUSE_ESP32") cg.add_build_flag("-DUSE_ESP32")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}")
@ -450,6 +497,9 @@ 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")
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
@ -495,6 +545,9 @@ async def to_code(config):
[f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"], [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"],
) )
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")
cg.add_define( cg.add_define(
@ -505,24 +558,47 @@ async def to_code(config):
) )
ARDUINO_PARTITIONS_CSV = """\ APP_PARTITION_SIZES = {
nvs, data, nvs, 0x009000, 0x005000, "2MB": 0x0C0000, # 768 KB
otadata, data, ota, 0x00e000, 0x002000, "4MB": 0x1C0000, # 1792 KB
app0, app, ota_0, 0x010000, 0x1C0000, "8MB": 0x3C0000, # 3840 KB
app1, app, ota_1, 0x1D0000, 0x1C0000, "16MB": 0x7C0000, # 7936 KB
eeprom, data, 0x99, 0x390000, 0x001000, "32MB": 0xFC0000, # 16128 KB
spiffs, data, spiffs, 0x391000, 0x00F000 }
def get_arduino_partition_csv(flash_size):
app_partition_size = APP_PARTITION_SIZES[flash_size]
eeprom_partition_size = 0x1000 # 4 KB
spiffs_partition_size = 0xF000 # 60 KB
app0_partition_start = 0x010000 # 64 KB
app1_partition_start = app0_partition_start + app_partition_size
eeprom_partition_start = app1_partition_start + app_partition_size
spiffs_partition_start = eeprom_partition_start + eeprom_partition_size
partition_csv = f"""\
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xE000, 0x2000,
app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X},
app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X},
eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X},
spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X}
""" """
return partition_csv
IDF_PARTITIONS_CSV = """\ def get_idf_partition_csv(flash_size):
# Name, Type, SubType, Offset, Size, Flags app_partition_size = APP_PARTITION_SIZES[flash_size]
partition_csv = f"""\
otadata, data, ota, , 0x2000, otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000, phy_init, data, phy, , 0x1000,
app0, app, ota_0, , 0x1C0000, app0, app, ota_0, , 0x{app_partition_size:X},
app1, app, ota_1, , 0x1C0000, app1, app, ota_1, , 0x{app_partition_size:X},
nvs, data, nvs, , 0x6d000, nvs, data, nvs, , 0x6D000,
""" """
return partition_csv
def _format_sdkconfig_val(value: SdkconfigValueType) -> str: def _format_sdkconfig_val(value: SdkconfigValueType) -> str:
@ -565,13 +641,17 @@ def copy_files():
if CORE.using_arduino: if CORE.using_arduino:
write_file_if_changed( write_file_if_changed(
CORE.relative_build_path("partitions.csv"), CORE.relative_build_path("partitions.csv"),
ARDUINO_PARTITIONS_CSV, get_arduino_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
) )
if CORE.using_esp_idf: if CORE.using_esp_idf:
_write_sdkconfig() _write_sdkconfig()
write_file_if_changed( write_file_if_changed(
CORE.relative_build_path("partitions.csv"), CORE.relative_build_path("partitions.csv"),
IDF_PARTITIONS_CSV, get_idf_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
) )
# IDF build scripts look for version string to put in the build. # IDF build scripts look for version string to put in the build.
# However, if the build path does not have an initialized git repo, # However, if the build path does not have an initialized git repo,

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

@ -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)
switch (request) { 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);
}
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,50 +765,256 @@ 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_)
void HonClimate::process_pending_action() { return;
switch (this->action_request_) { this->clear_control_messages_queue_();
case ActionRequest::START_SELF_CLEAN: HvacSettings climate_control;
case ActionRequest::START_STERI_CLEAN: climate_control = this->current_hvac_settings_;
// Will reset action with control message sending // Beeper command
this->set_phase(ProtocolPhases::SENDING_CONTROL); {
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
{
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::HEALTH_MODE,
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

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,7 +3,6 @@
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#include "esphome/components/remote_base/remote_base.h" #include "esphome/components/remote_base/remote_base.h"
#include "esphome/components/remote_transmitter/remote_transmitter.h"
#include <IRSender.h> // arduino-heatpump library #include <IRSender.h> // arduino-heatpump library
namespace esphome { namespace esphome {
@ -11,14 +10,13 @@ namespace heatpumpir {
class IRSenderESPHome : public IRSender { class IRSenderESPHome : public IRSender {
public: public:
IRSenderESPHome(remote_transmitter::RemoteTransmitterComponent *transmitter) IRSenderESPHome(remote_base::RemoteTransmitterBase *transmitter) : IRSender(0), transmit_(transmitter->transmit()){};
: IRSender(0), transmit_(transmitter->transmit()){};
void setFrequency(int frequency) override; // NOLINT(readability-identifier-naming) void setFrequency(int frequency) override; // NOLINT(readability-identifier-naming)
void space(int space_length) override; void space(int space_length) override;
void mark(int mark_length) override; void mark(int mark_length) override;
protected: protected:
remote_transmitter::RemoteTransmitterComponent::TransmitCall transmit_; remote_base::RemoteTransmitterBase::TransmitCall transmit_;
}; };
} // namespace heatpumpir } // namespace heatpumpir

View file

@ -1,15 +1,23 @@
from __future__ import annotations
import logging import logging
import hashlib
import io import io
from pathlib import Path from pathlib import Path
import re import re
import requests import requests
from magic import Magic
from PIL import Image
from esphome import core from esphome import core
from esphome.components import font from esphome.components import font
from esphome import external_files
import esphome.config_validation as cv import esphome.config_validation as cv
import esphome.codegen as cg import esphome.codegen as cg
from esphome.const import ( from esphome.const import (
__version__,
CONF_DITHER, CONF_DITHER,
CONF_FILE, CONF_FILE,
CONF_ICON, CONF_ICON,
@ -19,6 +27,7 @@ from esphome.const import (
CONF_RESIZE, CONF_RESIZE,
CONF_SOURCE, CONF_SOURCE,
CONF_TYPE, CONF_TYPE,
CONF_URL,
) )
from esphome.core import CORE, HexInt from esphome.core import CORE, HexInt
@ -43,34 +52,74 @@ IMAGE_TYPE = {
CONF_USE_TRANSPARENCY = "use_transparency" CONF_USE_TRANSPARENCY = "use_transparency"
# If the MDI file cannot be downloaded within this time, abort. # If the MDI file cannot be downloaded within this time, abort.
MDI_DOWNLOAD_TIMEOUT = 30 # seconds IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds
SOURCE_LOCAL = "local" SOURCE_LOCAL = "local"
SOURCE_MDI = "mdi" SOURCE_MDI = "mdi"
SOURCE_WEB = "web"
Image_ = image_ns.class_("Image") Image_ = image_ns.class_("Image")
def _compute_local_icon_path(value) -> Path: def _compute_local_icon_path(value: dict) -> Path:
base_dir = Path(CORE.data_dir) / DOMAIN / "mdi" base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi"
return base_dir / f"{value[CONF_ICON]}.svg" return base_dir / f"{value[CONF_ICON]}.svg"
def download_mdi(value): def _compute_local_image_path(value: dict) -> Path:
mdi_id = value[CONF_ICON] url = value[CONF_URL]
path = _compute_local_icon_path(value) h = hashlib.new("sha256")
if path.is_file(): h.update(url.encode())
return value key = h.hexdigest()[:8]
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" base_dir = external_files.compute_local_file_dir(DOMAIN)
_LOGGER.debug("Downloading %s MDI image from %s", mdi_id, url) return base_dir / key
def download_content(url: str, path: Path) -> None:
if not external_files.has_remote_file_changed(url, path):
_LOGGER.debug("Remote file has not changed %s", url)
return
_LOGGER.debug(
"Remote file has changed, downloading from %s to %s",
url,
path,
)
try: try:
req = requests.get(url, timeout=MDI_DOWNLOAD_TIMEOUT) req = requests.get(
url,
timeout=IMAGE_DOWNLOAD_TIMEOUT,
headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"},
)
req.raise_for_status() req.raise_for_status()
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
raise cv.Invalid(f"Could not download MDI image {mdi_id} from {url}: {e}") raise cv.Invalid(f"Could not download from {url}: {e}")
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(req.content) path.write_bytes(req.content)
def download_mdi(value):
validate_cairosvg_installed(value)
mdi_id = value[CONF_ICON]
path = _compute_local_icon_path(value)
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg"
download_content(url, path)
return value
def download_image(value):
url = value[CONF_URL]
path = _compute_local_image_path(value)
download_content(url, path)
return value return value
@ -139,6 +188,13 @@ def validate_file_shorthand(value):
CONF_ICON: icon, CONF_ICON: icon,
} }
) )
if value.startswith("http://") or value.startswith("https://"):
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_WEB,
CONF_URL: value,
}
)
return FILE_SCHEMA( return FILE_SCHEMA(
{ {
CONF_SOURCE: SOURCE_LOCAL, CONF_SOURCE: SOURCE_LOCAL,
@ -160,10 +216,18 @@ MDI_SCHEMA = cv.All(
download_mdi, download_mdi,
) )
WEB_SCHEMA = cv.All(
{
cv.Required(CONF_URL): cv.string,
},
download_image,
)
TYPED_FILE_SCHEMA = cv.typed_schema( TYPED_FILE_SCHEMA = cv.typed_schema(
{ {
SOURCE_LOCAL: LOCAL_SCHEMA, SOURCE_LOCAL: LOCAL_SCHEMA,
SOURCE_MDI: MDI_SCHEMA, SOURCE_MDI: MDI_SCHEMA,
SOURCE_WEB: WEB_SCHEMA,
}, },
key=CONF_SOURCE, key=CONF_SOURCE,
) )
@ -201,9 +265,7 @@ IMAGE_SCHEMA = cv.Schema(
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA)
def load_svg_image(file: str, resize: tuple[int, int]): def load_svg_image(file: bytes, resize: tuple[int, int]):
from PIL import Image
# This import is only needed in case of SVG images; adding it # This import is only needed in case of SVG images; adding it
# to the top would force configurations not using SVG to also have it # to the top would force configurations not using SVG to also have it
# installed for no reason. # installed for no reason.
@ -212,19 +274,17 @@ def load_svg_image(file: str, resize: tuple[int, int]):
if resize: if resize:
req_width, req_height = resize req_width, req_height = resize
svg_image = svg2png( svg_image = svg2png(
url=file, file,
output_width=req_width, output_width=req_width,
output_height=req_height, output_height=req_height,
) )
else: else:
svg_image = svg2png(url=file) svg_image = svg2png(file)
return Image.open(io.BytesIO(svg_image)) return Image.open(io.BytesIO(svg_image))
async def to_code(config): async def to_code(config):
from PIL import Image
conf_file = config[CONF_FILE] conf_file = config[CONF_FILE]
if conf_file[CONF_SOURCE] == SOURCE_LOCAL: if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
@ -233,17 +293,26 @@ async def to_code(config):
elif conf_file[CONF_SOURCE] == SOURCE_MDI: elif conf_file[CONF_SOURCE] == SOURCE_MDI:
path = _compute_local_icon_path(conf_file).as_posix() path = _compute_local_icon_path(conf_file).as_posix()
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
path = _compute_local_image_path(conf_file).as_posix()
try: try:
resize = config.get(CONF_RESIZE) with open(path, "rb") as f:
if path.lower().endswith(".svg"): file_contents = f.read()
image = load_svg_image(path, resize)
else:
image = Image.open(path)
if resize:
image.thumbnail(resize)
except Exception as e: except Exception as e:
raise core.EsphomeError(f"Could not load image file {path}: {e}") raise core.EsphomeError(f"Could not load image file {path}: {e}")
mime = Magic(mime=True)
file_type = mime.from_buffer(file_contents)
resize = config.get(CONF_RESIZE)
if "svg" in file_type:
image = load_svg_image(file_contents, resize)
else:
image = Image.open(io.BytesIO(file_contents))
if resize:
image.thumbnail(resize)
width, height = image.size width, height = image.size
if CONF_RESIZE not in config and (width > 500 or height > 500): if CONF_RESIZE not in config and (width > 500 or height > 500):

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 automation from esphome import automation
from esphome.const import CONF_ID, CONF_INTERVAL from esphome.const import CONF_ID, CONF_INTERVAL, CONF_STARTUP_DELAY
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
interval_ns = cg.esphome_ns.namespace("interval") interval_ns = cg.esphome_ns.namespace("interval")
@ -13,6 +13,9 @@ CONFIG_SCHEMA = automation.validate_automation(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(IntervalTrigger), cv.GenerateID(): cv.declare_id(IntervalTrigger),
cv.Optional(
CONF_STARTUP_DELAY, default="0s"
): cv.positive_time_period_milliseconds,
cv.Required(CONF_INTERVAL): cv.positive_time_period_milliseconds, cv.Required(CONF_INTERVAL): cv.positive_time_period_milliseconds,
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
@ -26,3 +29,4 @@ async def to_code(config):
await automation.build_automation(var, [], conf) await automation.build_automation(var, [], conf)
cg.add(var.set_update_interval(conf[CONF_INTERVAL])) cg.add(var.set_update_interval(conf[CONF_INTERVAL]))
cg.add(var.set_startup_delay(conf[CONF_STARTUP_DELAY]))

View file

@ -8,8 +8,26 @@ namespace interval {
class IntervalTrigger : public Trigger<>, public PollingComponent { class IntervalTrigger : public Trigger<>, public PollingComponent {
public: public:
void update() override { this->trigger(); } void update() override {
if (this->started_)
this->trigger();
}
void setup() override {
if (this->startup_delay_ == 0) {
this->started_ = true;
} else {
this->set_timeout(this->startup_delay_, [this] { this->started_ = true; });
}
}
void set_startup_delay(const uint32_t startup_delay) { this->startup_delay_ = startup_delay; }
float get_setup_priority() const override { return setup_priority::DATA; } float get_setup_priority() const override { return setup_priority::DATA; }
protected:
uint32_t startup_delay_{0};
bool started_{false};
}; };
} // namespace interval } // namespace interval

View file

@ -147,7 +147,7 @@ void MQTTClientComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Availability: '%s'", this->availability_.topic.c_str()); ESP_LOGCONFIG(TAG, " Availability: '%s'", this->availability_.topic.c_str());
} }
} }
bool MQTTClientComponent::can_proceed() { return this->is_connected(); } bool MQTTClientComponent::can_proceed() { return network::is_disabled() || this->is_connected(); }
void MQTTClientComponent::start_dnslookup_() { void MQTTClientComponent::start_dnslookup_() {
for (auto &subscription : this->subscriptions_) { for (auto &subscription : this->subscriptions_) {

View file

@ -40,6 +40,8 @@ const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (this->lock_->traits.get_assumed_state()) if (this->lock_->traits.get_assumed_state())
root[MQTT_OPTIMISTIC] = true; root[MQTT_OPTIMISTIC] = true;
if (this->lock_->traits.get_supports_open())
root[MQTT_PAYLOAD_OPEN] = "OPEN";
} }
bool MQTTLockComponent::send_initial_state() { return this->publish_state(); } bool MQTTLockComponent::send_initial_state() { return this->publish_state(); }

View file

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

View file

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

View file

@ -29,6 +29,14 @@ bool is_connected() {
return false; return false;
} }
bool is_disabled() {
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr)
return wifi::global_wifi_component->is_disabled();
#endif
return false;
}
network::IPAddress get_ip_address() { network::IPAddress get_ip_address() {
#ifdef USE_ETHERNET #ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr) if (ethernet::global_eth_component != nullptr)

View file

@ -8,6 +8,8 @@ namespace network {
/// Return whether the node is connected to the network (through wifi, eth, ...) /// Return whether the node is connected to the network (through wifi, eth, ...)
bool is_connected(); bool is_connected();
/// Return whether the network is disabled (only wifi for now)
bool is_disabled();
/// Get the active network hostname /// Get the active network hostname
std::string get_use_address(); std::string get_use_address();
IPAddress get_ip_address(); IPAddress get_ip_address();

View file

@ -52,8 +52,9 @@ RemoteReceiverTrigger = ns.class_(
"RemoteReceiverTrigger", automation.Trigger, RemoteReceiverListener "RemoteReceiverTrigger", automation.Trigger, RemoteReceiverListener
) )
RemoteTransmitterDumper = ns.class_("RemoteTransmitterDumper") RemoteTransmitterDumper = ns.class_("RemoteTransmitterDumper")
RemoteTransmittable = ns.class_("RemoteTransmittable")
RemoteTransmitterActionBase = ns.class_( RemoteTransmitterActionBase = ns.class_(
"RemoteTransmitterActionBase", automation.Action "RemoteTransmitterActionBase", RemoteTransmittable, automation.Action
) )
RemoteReceiverBase = ns.class_("RemoteReceiverBase") RemoteReceiverBase = ns.class_("RemoteReceiverBase")
RemoteTransmitterBase = ns.class_("RemoteTransmitterBase") RemoteTransmitterBase = ns.class_("RemoteTransmitterBase")
@ -68,11 +69,30 @@ def templatize(value):
return cv.Schema(ret) return cv.Schema(ret)
REMOTE_LISTENER_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase),
}
)
REMOTE_TRANSMITTABLE_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase),
}
)
async def register_listener(var, config): async def register_listener(var, config):
receiver = await cg.get_variable(config[CONF_RECEIVER_ID]) receiver = await cg.get_variable(config[CONF_RECEIVER_ID])
cg.add(receiver.register_listener(var)) cg.add(receiver.register_listener(var))
async def register_transmittable(var, config):
transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter_))
def register_binary_sensor(name, type, schema): def register_binary_sensor(name, type, schema):
return BINARY_SENSOR_REGISTRY.register(name, type, schema) return BINARY_SENSOR_REGISTRY.register(name, type, schema)
@ -129,10 +149,9 @@ def validate_repeat(value):
BASE_REMOTE_TRANSMITTER_SCHEMA = cv.Schema( BASE_REMOTE_TRANSMITTER_SCHEMA = cv.Schema(
{ {
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase),
cv.Optional(CONF_REPEAT): validate_repeat, cv.Optional(CONF_REPEAT): validate_repeat,
} }
) ).extend(REMOTE_TRANSMITTABLE_SCHEMA)
def register_action(name, type_, schema): def register_action(name, type_, schema):
@ -143,9 +162,8 @@ def register_action(name, type_, schema):
def decorator(func): def decorator(func):
async def new_func(config, action_id, template_arg, args): async def new_func(config, action_id, template_arg, args):
transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID])
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
cg.add(var.set_parent(transmitter)) await register_transmittable(var, config)
if CONF_REPEAT in config: if CONF_REPEAT in config:
conf = config[CONF_REPEAT] conf = config[CONF_REPEAT]
template_ = await cg.templatable(conf[CONF_TIMES], args, cg.uint32) template_ = await cg.templatable(conf[CONF_TIMES], args, cg.uint32)
@ -1539,7 +1557,7 @@ MIDEA_SCHEMA = cv.Schema(
@register_binary_sensor("midea", MideaBinarySensor, MIDEA_SCHEMA) @register_binary_sensor("midea", MideaBinarySensor, MIDEA_SCHEMA)
def midea_binary_sensor(var, config): def midea_binary_sensor(var, config):
cg.add(var.set_code(config[CONF_CODE])) cg.add(var.set_data(config[CONF_CODE]))
@register_trigger("midea", MideaTrigger, MideaData) @register_trigger("midea", MideaTrigger, MideaData)

View file

@ -67,20 +67,7 @@ class MideaProtocol : public RemoteProtocol<MideaData> {
void dump(const MideaData &data) override; void dump(const MideaData &data) override;
}; };
class MideaBinarySensor : public RemoteReceiverBinarySensorBase { DECLARE_REMOTE_PROTOCOL(Midea)
public:
bool matches(RemoteReceiveData src) override {
auto data = MideaProtocol().decode(src);
return data.has_value() && data.value() == this->data_;
}
void set_code(const std::vector<uint8_t> &code) { this->data_ = code; }
protected:
MideaData data_;
};
using MideaTrigger = RemoteReceiverTrigger<MideaProtocol, MideaData>;
using MideaDumper = RemoteReceiverDumper<MideaProtocol, MideaData>;
template<typename... Ts> class MideaAction : public RemoteTransmitterActionBase<Ts...> { template<typename... Ts> class MideaAction : public RemoteTransmitterActionBase<Ts...> {
TEMPLATABLE_VALUE(std::vector<uint8_t>, code) TEMPLATABLE_VALUE(std::vector<uint8_t>, code)

View file

@ -15,6 +15,8 @@ struct RCSwitchData {
class RCSwitchBase { class RCSwitchBase {
public: public:
using ProtocolData = RCSwitchData;
RCSwitchBase() = default; RCSwitchBase() = default;
RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, uint32_t one_high, RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, uint32_t one_high,
uint32_t one_low, bool inverted); uint32_t one_low, bool inverted);
@ -213,7 +215,7 @@ class RCSwitchDumper : public RemoteReceiverDumperBase {
bool dump(RemoteReceiveData src) override; bool dump(RemoteReceiveData src) override;
}; };
using RCSwitchTrigger = RemoteReceiverTrigger<RCSwitchBase, RCSwitchData>; using RCSwitchTrigger = RemoteReceiverTrigger<RCSwitchBase>;
} // namespace remote_base } // namespace remote_base
} // namespace esphome } // namespace esphome

View file

@ -127,6 +127,14 @@ class RemoteTransmitterBase : public RemoteComponentBase {
this->temp_.reset(); this->temp_.reset();
return TransmitCall(this); return TransmitCall(this);
} }
template<typename Protocol>
void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
auto call = this->transmit();
Protocol().encode(call.get_data(), data);
call.set_send_times(send_times);
call.set_send_wait(send_wait);
call.perform();
}
protected: protected:
void send_(uint32_t send_times, uint32_t send_wait); void send_(uint32_t send_times, uint32_t send_wait);
@ -184,12 +192,13 @@ class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensorInitial
template<typename T> class RemoteProtocol { template<typename T> class RemoteProtocol {
public: public:
virtual void encode(RemoteTransmitData *dst, const T &data) = 0; using ProtocolData = T;
virtual optional<T> decode(RemoteReceiveData src) = 0; virtual void encode(RemoteTransmitData *dst, const ProtocolData &data) = 0;
virtual void dump(const T &data) = 0; virtual optional<ProtocolData> decode(RemoteReceiveData src) = 0;
virtual void dump(const ProtocolData &data) = 0;
}; };
template<typename T, typename D> class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase { template<typename T> class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase {
public: public:
RemoteReceiverBinarySensor() : RemoteReceiverBinarySensorBase() {} RemoteReceiverBinarySensor() : RemoteReceiverBinarySensorBase() {}
@ -201,13 +210,14 @@ template<typename T, typename D> class RemoteReceiverBinarySensor : public Remot
} }
public: public:
void set_data(D data) { data_ = data; } void set_data(typename T::ProtocolData data) { data_ = data; }
protected: protected:
D data_; typename T::ProtocolData data_;
}; };
template<typename T, typename D> class RemoteReceiverTrigger : public Trigger<D>, public RemoteReceiverListener { template<typename T>
class RemoteReceiverTrigger : public Trigger<typename T::ProtocolData>, public RemoteReceiverListener {
protected: protected:
bool on_receive(RemoteReceiveData src) override { bool on_receive(RemoteReceiveData src) override {
auto proto = T(); auto proto = T();
@ -220,28 +230,36 @@ template<typename T, typename D> class RemoteReceiverTrigger : public Trigger<D>
} }
}; };
template<typename... Ts> class RemoteTransmitterActionBase : public Action<Ts...> { class RemoteTransmittable {
public: public:
void set_parent(RemoteTransmitterBase *parent) { this->parent_ = parent; } RemoteTransmittable() {}
RemoteTransmittable(RemoteTransmitterBase *transmitter) : transmitter_(transmitter) {}
void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
TEMPLATABLE_VALUE(uint32_t, send_times); protected:
TEMPLATABLE_VALUE(uint32_t, send_wait); template<typename Protocol>
void transmit_(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
this->transmitter_->transmit<Protocol>(data, send_times, send_wait);
}
RemoteTransmitterBase *transmitter_;
};
template<typename... Ts> class RemoteTransmitterActionBase : public RemoteTransmittable, public Action<Ts...> {
TEMPLATABLE_VALUE(uint32_t, send_times)
TEMPLATABLE_VALUE(uint32_t, send_wait)
protected:
void play(Ts... x) override { void play(Ts... x) override {
auto call = this->parent_->transmit(); auto call = this->transmitter_->transmit();
this->encode(call.get_data(), x...); this->encode(call.get_data(), x...);
call.set_send_times(this->send_times_.value_or(x..., 1)); call.set_send_times(this->send_times_.value_or(x..., 1));
call.set_send_wait(this->send_wait_.value_or(x..., 0)); call.set_send_wait(this->send_wait_.value_or(x..., 0));
call.perform(); call.perform();
} }
protected:
virtual void encode(RemoteTransmitData *dst, Ts... x) = 0; virtual void encode(RemoteTransmitData *dst, Ts... x) = 0;
RemoteTransmitterBase *parent_{};
}; };
template<typename T, typename D> class RemoteReceiverDumper : public RemoteReceiverDumperBase { template<typename T> class RemoteReceiverDumper : public RemoteReceiverDumperBase {
public: public:
bool dump(RemoteReceiveData src) override { bool dump(RemoteReceiveData src) override {
auto proto = T(); auto proto = T();
@ -254,9 +272,9 @@ template<typename T, typename D> class RemoteReceiverDumper : public RemoteRecei
}; };
#define DECLARE_REMOTE_PROTOCOL_(prefix) \ #define DECLARE_REMOTE_PROTOCOL_(prefix) \
using prefix##BinarySensor = RemoteReceiverBinarySensor<prefix##Protocol, prefix##Data>; \ using prefix##BinarySensor = RemoteReceiverBinarySensor<prefix##Protocol>; \
using prefix##Trigger = RemoteReceiverTrigger<prefix##Protocol, prefix##Data>; \ using prefix##Trigger = RemoteReceiverTrigger<prefix##Protocol>; \
using prefix##Dumper = RemoteReceiverDumper<prefix##Protocol, prefix##Data>; using prefix##Dumper = RemoteReceiverDumper<prefix##Protocol>;
#define DECLARE_REMOTE_PROTOCOL(prefix) DECLARE_REMOTE_PROTOCOL_(prefix) #define DECLARE_REMOTE_PROTOCOL(prefix) DECLARE_REMOTE_PROTOCOL_(prefix)
} // namespace remote_base } // namespace remote_base

View file

@ -74,12 +74,12 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
# The default/recommended arduino framework version # The default/recommended arduino framework version
# - https://github.com/earlephilhower/arduino-pico/releases # - https://github.com/earlephilhower/arduino-pico/releases
# - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico # - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 4, 0) RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 6, 0)
# The platformio/raspberrypi version to use for arduino frameworks # The platformio/raspberrypi version to use for arduino frameworks
# - https://github.com/platformio/platform-raspberrypi/releases # - https://github.com/platformio/platform-raspberrypi/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/raspberrypi # - https://api.registry.platformio.org/v3/packages/platformio/platform/raspberrypi
ARDUINO_PLATFORM_VERSION = cv.Version(1, 9, 0) ARDUINO_PLATFORM_VERSION = cv.Version(1, 10, 0)
def _arduino_check_versions(value): def _arduino_check_versions(value):

View file

@ -201,13 +201,19 @@ void SEN5XComponent::setup() {
ESP_LOGE(TAG, "Failed to read RHT Acceleration mode"); ESP_LOGE(TAG, "Failed to read RHT Acceleration mode");
} }
} }
if (this->voc_tuning_params_.has_value()) if (this->voc_tuning_params_.has_value()) {
this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value()); this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value());
if (this->nox_tuning_params_.has_value()) delay(20);
}
if (this->nox_tuning_params_.has_value()) {
this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value()); this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
delay(20);
}
if (this->temperature_compensation_.has_value()) if (this->temperature_compensation_.has_value()) {
this->write_temperature_compensation_(this->temperature_compensation_.value()); this->write_temperature_compensation_(this->temperature_compensation_.value());
delay(20);
}
// Finally start sensor measurements // Finally start sensor measurements
auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY; auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY;

View file

@ -41,8 +41,8 @@ struct GasTuning {
}; };
struct TemperatureCompensation { struct TemperatureCompensation {
uint16_t offset; int16_t offset;
uint16_t normalized_offset_slope; int16_t normalized_offset_slope;
uint16_t time_constant; uint16_t time_constant;
}; };
@ -70,27 +70,33 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t std_initial, uint16_t gain_factor) { uint16_t std_initial, uint16_t gain_factor) {
voc_tuning_params_.value().index_offset = index_offset; GasTuning tuning_params;
voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; tuning_params.index_offset = index_offset;
voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; tuning_params.learning_time_offset_hours = learning_time_offset_hours;
voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; tuning_params.learning_time_gain_hours = learning_time_gain_hours;
voc_tuning_params_.value().std_initial = std_initial; tuning_params.gating_max_duration_minutes = gating_max_duration_minutes;
voc_tuning_params_.value().gain_factor = gain_factor; tuning_params.std_initial = std_initial;
tuning_params.gain_factor = gain_factor;
voc_tuning_params_ = tuning_params;
} }
void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t gain_factor) { uint16_t gain_factor) {
nox_tuning_params_.value().index_offset = index_offset; GasTuning tuning_params;
nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; tuning_params.index_offset = index_offset;
nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; tuning_params.learning_time_offset_hours = learning_time_offset_hours;
nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; tuning_params.learning_time_gain_hours = learning_time_gain_hours;
nox_tuning_params_.value().std_initial = 50; tuning_params.gating_max_duration_minutes = gating_max_duration_minutes;
nox_tuning_params_.value().gain_factor = gain_factor; tuning_params.std_initial = 50;
tuning_params.gain_factor = gain_factor;
nox_tuning_params_ = tuning_params;
} }
void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) { void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) {
temperature_compensation_.value().offset = offset * 200; TemperatureCompensation temp_comp;
temperature_compensation_.value().normalized_offset_slope = normalized_offset_slope * 100; temp_comp.offset = offset * 200;
temperature_compensation_.value().time_constant = time_constant; temp_comp.normalized_offset_slope = normalized_offset_slope * 10000;
temp_comp.time_constant = time_constant;
temperature_compensation_ = temp_comp;
} }
bool start_fan_cleaning(); bool start_fan_cleaning();

View file

@ -88,6 +88,15 @@ GAS_SENSOR = cv.Schema(
} }
) )
def float_previously_pct(value):
if isinstance(value, str) and "%" in value:
raise cv.Invalid(
f"The value '{value}' is a percentage. Suggested value: {float(value.strip('%')) / 100}"
)
return value
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.Schema( cv.Schema(
{ {
@ -151,7 +160,9 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_TEMPERATURE_COMPENSATION): cv.Schema( cv.Optional(CONF_TEMPERATURE_COMPENSATION): cv.Schema(
{ {
cv.Optional(CONF_OFFSET, default=0): cv.float_, cv.Optional(CONF_OFFSET, default=0): cv.float_,
cv.Optional(CONF_NORMALIZED_OFFSET_SLOPE, default=0): cv.percentage, cv.Optional(CONF_NORMALIZED_OFFSET_SLOPE, default=0): cv.All(
float_previously_pct, cv.float_
),
cv.Optional(CONF_TIME_CONSTANT, default=0): cv.int_, cv.Optional(CONF_TIME_CONSTANT, default=0): cv.int_,
} }
), ),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,20 +18,27 @@ DEPENDENCIES = ["api", "microphone"]
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
CONF_SILENCE_DETECTION = "silence_detection"
CONF_ON_LISTENING = "on_listening"
CONF_ON_START = "on_start"
CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected"
CONF_ON_STT_END = "on_stt_end"
CONF_ON_TTS_START = "on_tts_start"
CONF_ON_TTS_END = "on_tts_end"
CONF_ON_END = "on_end" CONF_ON_END = "on_end"
CONF_ON_ERROR = "on_error" CONF_ON_ERROR = "on_error"
CONF_ON_INTENT_END = "on_intent_end"
CONF_ON_INTENT_START = "on_intent_start"
CONF_ON_LISTENING = "on_listening"
CONF_ON_START = "on_start"
CONF_ON_STT_END = "on_stt_end"
CONF_ON_STT_VAD_END = "on_stt_vad_end"
CONF_ON_STT_VAD_START = "on_stt_vad_start"
CONF_ON_TTS_END = "on_tts_end"
CONF_ON_TTS_START = "on_tts_start"
CONF_ON_TTS_STREAM_START = "on_tts_stream_start"
CONF_ON_TTS_STREAM_END = "on_tts_stream_end"
CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected"
CONF_SILENCE_DETECTION = "silence_detection"
CONF_USE_WAKE_WORD = "use_wake_word" CONF_USE_WAKE_WORD = "use_wake_word"
CONF_VAD_THRESHOLD = "vad_threshold" CONF_VAD_THRESHOLD = "vad_threshold"
CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level"
CONF_AUTO_GAIN = "auto_gain" CONF_AUTO_GAIN = "auto_gain"
CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level"
CONF_VOLUME_MULTIPLIER = "volume_multiplier" CONF_VOLUME_MULTIPLIER = "volume_multiplier"
@ -51,6 +58,17 @@ IsRunningCondition = voice_assistant_ns.class_(
"IsRunningCondition", automation.Condition, cg.Parented.template(VoiceAssistant) "IsRunningCondition", automation.Condition, cg.Parented.template(VoiceAssistant)
) )
def tts_stream_validate(config):
if CONF_SPEAKER not in config and (
CONF_ON_TTS_STREAM_START in config or CONF_ON_TTS_STREAM_END in config
):
raise cv.Invalid(
f"{CONF_SPEAKER} is required when using {CONF_ON_TTS_STREAM_START} and/or {CONF_ON_TTS_STREAM_END}"
)
return config
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
@ -88,8 +106,27 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation(
single=True single=True
), ),
cv.Optional(CONF_ON_INTENT_START): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_INTENT_END): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_STT_VAD_START): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_STT_VAD_END): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_TTS_STREAM_START): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_TTS_STREAM_END): automation.validate_automation(
single=True
),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
tts_stream_validate,
) )
@ -177,6 +214,48 @@ async def to_code(config):
config[CONF_ON_CLIENT_DISCONNECTED], config[CONF_ON_CLIENT_DISCONNECTED],
) )
if CONF_ON_INTENT_START in config:
await automation.build_automation(
var.get_intent_start_trigger(),
[],
config[CONF_ON_INTENT_START],
)
if CONF_ON_INTENT_END in config:
await automation.build_automation(
var.get_intent_end_trigger(),
[],
config[CONF_ON_INTENT_END],
)
if CONF_ON_STT_VAD_START in config:
await automation.build_automation(
var.get_stt_vad_start_trigger(),
[],
config[CONF_ON_STT_VAD_START],
)
if CONF_ON_STT_VAD_END in config:
await automation.build_automation(
var.get_stt_vad_end_trigger(),
[],
config[CONF_ON_STT_VAD_END],
)
if CONF_ON_TTS_STREAM_START in config:
await automation.build_automation(
var.get_tts_stream_start_trigger(),
[],
config[CONF_ON_TTS_STREAM_START],
)
if CONF_ON_TTS_STREAM_END in config:
await automation.build_automation(
var.get_tts_stream_end_trigger(),
[],
config[CONF_ON_TTS_STREAM_END],
)
cg.add_define("USE_VOICE_ASSISTANT") cg.add_define("USE_VOICE_ASSISTANT")

View file

@ -31,7 +31,7 @@ void VoiceAssistant::setup() {
this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (socket_ == nullptr) { if (socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket."); ESP_LOGW(TAG, "Could not create socket");
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -69,7 +69,7 @@ void VoiceAssistant::setup() {
ExternalRAMAllocator<uint8_t> speaker_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); ExternalRAMAllocator<uint8_t> speaker_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE); this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE);
if (this->speaker_buffer_ == nullptr) { if (this->speaker_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate speaker buffer."); ESP_LOGW(TAG, "Could not allocate speaker buffer");
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -79,7 +79,7 @@ void VoiceAssistant::setup() {
ExternalRAMAllocator<int16_t> allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE); ExternalRAMAllocator<int16_t> allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE); this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE);
if (this->input_buffer_ == nullptr) { if (this->input_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate input buffer."); ESP_LOGW(TAG, "Could not allocate input buffer");
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -89,7 +89,7 @@ void VoiceAssistant::setup() {
this->ring_buffer_ = rb_create(BUFFER_SIZE, sizeof(int16_t)); this->ring_buffer_ = rb_create(BUFFER_SIZE, sizeof(int16_t));
if (this->ring_buffer_ == nullptr) { if (this->ring_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate ring buffer."); ESP_LOGW(TAG, "Could not allocate ring buffer");
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -98,7 +98,7 @@ void VoiceAssistant::setup() {
ExternalRAMAllocator<uint8_t> send_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); ExternalRAMAllocator<uint8_t> send_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE); this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE);
if (send_buffer_ == nullptr) { if (send_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate send buffer."); ESP_LOGW(TAG, "Could not allocate send buffer");
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -221,8 +221,8 @@ void VoiceAssistant::loop() {
msg.audio_settings = audio_settings; msg.audio_settings = audio_settings;
if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) { if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) {
ESP_LOGW(TAG, "Could not request start."); ESP_LOGW(TAG, "Could not request start");
this->error_trigger_->trigger("not-connected", "Could not request start."); this->error_trigger_->trigger("not-connected", "Could not request start");
this->continuous_ = false; this->continuous_ = false;
this->set_state_(State::IDLE, State::IDLE); this->set_state_(State::IDLE, State::IDLE);
break; break;
@ -280,7 +280,7 @@ void VoiceAssistant::loop() {
this->speaker_buffer_size_ += len; this->speaker_buffer_size_ += len;
} }
} else { } else {
ESP_LOGW(TAG, "Receive buffer full."); ESP_LOGW(TAG, "Receive buffer full");
} }
if (this->speaker_buffer_size_ > 0) { if (this->speaker_buffer_size_ > 0) {
size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_);
@ -290,7 +290,7 @@ void VoiceAssistant::loop() {
this->speaker_buffer_index_ -= written; this->speaker_buffer_index_ -= written;
this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); }); this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); });
} else { } else {
ESP_LOGW(TAG, "Speaker buffer full."); ESP_LOGW(TAG, "Speaker buffer full");
} }
} }
if (this->wait_for_stream_end_) { if (this->wait_for_stream_end_) {
@ -513,7 +513,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
break; break;
} }
case api::enums::VOICE_ASSISTANT_STT_START: case api::enums::VOICE_ASSISTANT_STT_START:
ESP_LOGD(TAG, "STT Started"); ESP_LOGD(TAG, "STT started");
this->listening_trigger_->trigger(); this->listening_trigger_->trigger();
break; break;
case api::enums::VOICE_ASSISTANT_STT_END: { case api::enums::VOICE_ASSISTANT_STT_END: {
@ -525,19 +525,24 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
} }
} }
if (text.empty()) { if (text.empty()) {
ESP_LOGW(TAG, "No text in STT_END event."); ESP_LOGW(TAG, "No text in STT_END event");
return; return;
} }
ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str());
this->stt_end_trigger_->trigger(text); this->stt_end_trigger_->trigger(text);
break; break;
} }
case api::enums::VOICE_ASSISTANT_INTENT_START:
ESP_LOGD(TAG, "Intent started");
this->intent_start_trigger_->trigger();
break;
case api::enums::VOICE_ASSISTANT_INTENT_END: { case api::enums::VOICE_ASSISTANT_INTENT_END: {
for (auto arg : msg.data) { for (auto arg : msg.data) {
if (arg.name == "conversation_id") { if (arg.name == "conversation_id") {
this->conversation_id_ = std::move(arg.value); this->conversation_id_ = std::move(arg.value);
} }
} }
this->intent_end_trigger_->trigger();
break; break;
} }
case api::enums::VOICE_ASSISTANT_TTS_START: { case api::enums::VOICE_ASSISTANT_TTS_START: {
@ -548,7 +553,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
} }
} }
if (text.empty()) { if (text.empty()) {
ESP_LOGW(TAG, "No text in TTS_START event."); ESP_LOGW(TAG, "No text in TTS_START event");
return; return;
} }
ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); ESP_LOGD(TAG, "Response: \"%s\"", text.c_str());
@ -566,7 +571,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
} }
} }
if (url.empty()) { if (url.empty()) {
ESP_LOGW(TAG, "No url in TTS_END event."); ESP_LOGW(TAG, "No url in TTS_END event");
return; return;
} }
ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str());
@ -610,6 +615,11 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
if (code == "wake-word-timeout" || code == "wake_word_detection_aborted") { if (code == "wake-word-timeout" || code == "wake_word_detection_aborted") {
// Don't change state here since either the "tts-end" or "run-end" events will do it. // Don't change state here since either the "tts-end" or "run-end" events will do it.
return; return;
} else if (code == "wake-provider-missing" || code == "wake-engine-missing") {
// Wake word is not set up or not ready on Home Assistant so stop and do not retry until user starts again.
this->request_stop();
this->error_trigger_->trigger(code, message);
return;
} }
ESP_LOGE(TAG, "Error: %s - %s", code.c_str(), message.c_str()); ESP_LOGE(TAG, "Error: %s - %s", code.c_str(), message.c_str());
if (this->state_ != State::IDLE) { if (this->state_ != State::IDLE) {
@ -622,13 +632,27 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: {
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
this->wait_for_stream_end_ = true; this->wait_for_stream_end_ = true;
ESP_LOGD(TAG, "TTS stream start");
this->tts_stream_start_trigger_->trigger();
#endif #endif
break; break;
} }
case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: {
this->set_state_(State::RESPONSE_FINISHED, State::IDLE); this->set_state_(State::RESPONSE_FINISHED, State::IDLE);
#ifdef USE_SPEAKER
ESP_LOGD(TAG, "TTS stream end");
this->tts_stream_end_trigger_->trigger();
#endif
break; break;
} }
case api::enums::VOICE_ASSISTANT_STT_VAD_START:
ESP_LOGD(TAG, "Starting STT by VAD");
this->stt_vad_start_trigger_->trigger();
break;
case api::enums::VOICE_ASSISTANT_STT_VAD_END:
ESP_LOGD(TAG, "STT by VAD end");
this->stt_vad_end_trigger_->trigger();
break;
default: default:
ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type); ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type);
break; break;

View file

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

View file

@ -403,6 +403,10 @@ async def to_code(config):
lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_config))), lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_config))),
) )
cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT]))
cg.add_define("USE_WIFI_AP")
elif CORE.is_esp32 and CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False)
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))

View file

@ -82,6 +82,7 @@ void WiFiComponent::start() {
} else { } else {
this->start_scanning(); this->start_scanning();
} }
#ifdef USE_WIFI_AP
} else if (this->has_ap()) { } else if (this->has_ap()) {
this->setup_ap_config_(); this->setup_ap_config_();
if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
@ -94,6 +95,7 @@ void WiFiComponent::start() {
captive_portal::global_captive_portal->start(); captive_portal::global_captive_portal->start();
} }
#endif #endif
#endif // USE_WIFI_AP
} }
#ifdef USE_IMPROV #ifdef USE_IMPROV
if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) { if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) {
@ -160,6 +162,7 @@ void WiFiComponent::loop() {
return; return;
} }
#ifdef USE_WIFI_AP
if (this->has_ap() && !this->ap_setup_) { if (this->has_ap() && !this->ap_setup_) {
if (now - this->last_connected_ > this->ap_timeout_) { if (now - this->last_connected_ > this->ap_timeout_) {
ESP_LOGI(TAG, "Starting fallback AP!"); ESP_LOGI(TAG, "Starting fallback AP!");
@ -170,6 +173,7 @@ void WiFiComponent::loop() {
#endif #endif
} }
} }
#endif // USE_WIFI_AP
#ifdef USE_IMPROV #ifdef USE_IMPROV
if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) {
@ -199,11 +203,16 @@ void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ =
void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; } void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; }
#endif #endif
network::IPAddress WiFiComponent::get_ip_address() { network::IPAddress WiFiComponent::get_ip_address() {
if (this->has_sta()) if (this->has_sta())
return this->wifi_sta_ip(); return this->wifi_sta_ip();
#ifdef USE_WIFI_AP
if (this->has_ap()) if (this->has_ap())
return this->wifi_soft_ap_ip(); return this->wifi_soft_ap_ip();
#endif // USE_WIFI_AP
return {}; return {};
} }
network::IPAddress WiFiComponent::get_dns_address(int num) { network::IPAddress WiFiComponent::get_dns_address(int num) {
@ -218,6 +227,8 @@ std::string WiFiComponent::get_use_address() const {
return this->use_address_; return this->use_address_;
} }
void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; }
#ifdef USE_WIFI_AP
void WiFiComponent::setup_ap_config_() { void WiFiComponent::setup_ap_config_() {
this->wifi_mode_({}, true); this->wifi_mode_({}, true);
@ -255,13 +266,16 @@ void WiFiComponent::setup_ap_config_() {
} }
} }
float WiFiComponent::get_loop_priority() const {
return 10.0f; // before other loop components
}
void WiFiComponent::set_ap(const WiFiAP &ap) { void WiFiComponent::set_ap(const WiFiAP &ap) {
this->ap_ = ap; this->ap_ = ap;
this->has_ap_ = true; this->has_ap_ = true;
} }
#endif // USE_WIFI_AP
float WiFiComponent::get_loop_priority() const {
return 10.0f; // before other loop components
}
void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
void WiFiComponent::set_sta(const WiFiAP &ap) { void WiFiComponent::set_sta(const WiFiAP &ap) {
this->clear_sta(); this->clear_sta();
@ -389,6 +403,10 @@ void WiFiComponent::print_connect_params_() {
bssid_t bssid = wifi_bssid(); bssid_t bssid = wifi_bssid();
ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str());
if (this->is_disabled()) {
ESP_LOGCONFIG(TAG, " WiFi is disabled!");
return;
}
ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), wifi_ssid().c_str()); ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), wifi_ssid().c_str());
ESP_LOGCONFIG(TAG, " IP Address: %s", wifi_sta_ip().str().c_str()); ESP_LOGCONFIG(TAG, " IP Address: %s", wifi_sta_ip().str().c_str());
ESP_LOGCONFIG(TAG, " BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X"), bssid[0], bssid[1], bssid[2], bssid[3], ESP_LOGCONFIG(TAG, " BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X"), bssid[0], bssid[1], bssid[2], bssid[3],

View file

@ -194,6 +194,7 @@ class WiFiComponent : public Component {
void add_sta(const WiFiAP &ap); void add_sta(const WiFiAP &ap);
void clear_sta(); void clear_sta();
#ifdef USE_WIFI_AP
/** Setup an Access Point that should be created if no connection to a station can be made. /** Setup an Access Point that should be created if no connection to a station can be made.
* *
* This can also be used without set_sta(). Then the AP will always be active. * This can also be used without set_sta(). Then the AP will always be active.
@ -203,6 +204,7 @@ class WiFiComponent : public Component {
*/ */
void set_ap(const WiFiAP &ap); void set_ap(const WiFiAP &ap);
WiFiAP get_ap() { return this->ap_; } WiFiAP get_ap() { return this->ap_; }
#endif // USE_WIFI_AP
void enable(); void enable();
void disable(); void disable();
@ -299,7 +301,11 @@ class WiFiComponent : public Component {
protected: protected:
static std::string format_mac_addr(const uint8_t mac[6]); static std::string format_mac_addr(const uint8_t mac[6]);
#ifdef USE_WIFI_AP
void setup_ap_config_(); void setup_ap_config_();
#endif // USE_WIFI_AP
void print_connect_params_(); void print_connect_params_();
void wifi_loop_(); void wifi_loop_();
@ -313,8 +319,12 @@ class WiFiComponent : public Component {
void wifi_pre_setup_(); void wifi_pre_setup_();
WiFiSTAConnectStatus wifi_sta_connect_status_(); WiFiSTAConnectStatus wifi_sta_connect_status_();
bool wifi_scan_start_(bool passive); bool wifi_scan_start_(bool passive);
#ifdef USE_WIFI_AP
bool wifi_ap_ip_config_(optional<ManualIP> manual_ip); bool wifi_ap_ip_config_(optional<ManualIP> manual_ip);
bool wifi_start_ap_(const WiFiAP &ap); bool wifi_start_ap_(const WiFiAP &ap);
#endif // USE_WIFI_AP
bool wifi_disconnect_(); bool wifi_disconnect_();
int32_t wifi_channel_(); int32_t wifi_channel_();
network::IPAddress wifi_subnet_mask_(); network::IPAddress wifi_subnet_mask_();

View file

@ -597,6 +597,8 @@ void WiFiComponent::wifi_scan_done_callback_() {
WiFi.scanDelete(); WiFi.scanDelete();
this->scan_done_ = true; this->scan_done_ = true;
} }
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) { bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
esp_err_t err; esp_err_t err;
@ -654,6 +656,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
return true; return true;
} }
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
// enable AP // enable AP
if (!this->wifi_mode_({}, true)) if (!this->wifi_mode_({}, true))
@ -692,11 +695,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return true; return true;
} }
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
tcpip_adapter_ip_info_t ip; tcpip_adapter_ip_info_t ip;
tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip); tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip);
return network::IPAddress(&ip.ip); return network::IPAddress(&ip.ip);
} }
#endif // USE_WIFI_AP
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); } bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
bssid_t WiFiComponent::wifi_bssid() { bssid_t WiFiComponent::wifi_bssid() {

View file

@ -688,6 +688,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
} }
this->scan_done_ = true; this->scan_done_ = true;
} }
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) { bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
// enable AP // enable AP
if (!this->wifi_mode_({}, true)) if (!this->wifi_mode_({}, true))
@ -753,6 +755,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
return true; return true;
} }
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
// enable AP // enable AP
if (!this->wifi_mode_({}, true)) if (!this->wifi_mode_({}, true))
@ -790,11 +793,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return true; return true;
} }
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
struct ip_info ip {}; struct ip_info ip {};
wifi_get_ip_info(SOFTAP_IF, &ip); wifi_get_ip_info(SOFTAP_IF, &ip);
return network::IPAddress(&ip.ip); return network::IPAddress(&ip.ip);
} }
#endif // USE_WIFI_AP
bssid_t WiFiComponent::wifi_bssid() { bssid_t WiFiComponent::wifi_bssid() {
bssid_t bssid{}; bssid_t bssid{};
uint8_t *raw_bssid = WiFi.BSSID(); uint8_t *raw_bssid = WiFi.BSSID();

View file

@ -17,7 +17,11 @@
#ifdef USE_WIFI_WPA2_EAP #ifdef USE_WIFI_WPA2_EAP
#include <esp_wpa2.h> #include <esp_wpa2.h>
#endif #endif
#ifdef USE_WIFI_AP
#include "dhcpserver/dhcpserver.h" #include "dhcpserver/dhcpserver.h"
#endif // USE_WIFI_AP
#include "lwip/err.h" #include "lwip/err.h"
#include "lwip/dns.h" #include "lwip/dns.h"
@ -35,7 +39,11 @@ static const char *const TAG = "wifi_esp32";
static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static QueueHandle_t s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static QueueHandle_t s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifdef USE_WIFI_AP
static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#endif // USE_WIFI_AP
static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_got_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_got_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@ -159,7 +167,11 @@ void WiFiComponent::wifi_pre_setup_() {
} }
s_sta_netif = esp_netif_create_default_wifi_sta(); s_sta_netif = esp_netif_create_default_wifi_sta();
#ifdef USE_WIFI_AP
s_ap_netif = esp_netif_create_default_wifi_ap(); s_ap_netif = esp_netif_create_default_wifi_ap();
#endif // USE_WIFI_AP
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
// cfg.nvs_enable = false; // cfg.nvs_enable = false;
err = esp_wifi_init(&cfg); err = esp_wifi_init(&cfg);
@ -674,6 +686,11 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
return; return;
} }
if (it.number == 0) {
// no results
return;
}
uint16_t number = it.number; uint16_t number = it.number;
std::vector<wifi_ap_record_t> records(number); std::vector<wifi_ap_record_t> records(number);
err = esp_wifi_scan_get_ap_records(&number, records.data()); err = esp_wifi_scan_get_ap_records(&number, records.data());
@ -761,6 +778,8 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
scan_done_ = false; scan_done_ = false;
return true; return true;
} }
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) { bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
esp_err_t err; esp_err_t err;
@ -816,6 +835,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
return true; return true;
} }
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
// enable AP // enable AP
if (!this->wifi_mode_({}, true)) if (!this->wifi_mode_({}, true))
@ -853,6 +873,8 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return true; return true;
} }
#endif // USE_WIFI_AP
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
esp_netif_ip_info_t ip; esp_netif_ip_info_t ip;
esp_netif_get_ip_info(s_sta_netif, &ip); esp_netif_get_ip_info(s_sta_netif, &ip);

View file

@ -412,6 +412,8 @@ void WiFiComponent::wifi_scan_done_callback_() {
WiFi.scanDelete(); WiFi.scanDelete();
this->scan_done_ = true; this->scan_done_ = true;
} }
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) { bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
// enable AP // enable AP
if (!this->wifi_mode_({}, true)) if (!this->wifi_mode_({}, true))
@ -423,6 +425,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
return WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0)); return WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0));
} }
} }
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
// enable AP // enable AP
if (!this->wifi_mode_({}, true)) if (!this->wifi_mode_({}, true))
@ -438,7 +441,10 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(),
ap.get_channel().value_or(1), ap.get_hidden()); ap.get_channel().value_or(1), ap.get_hidden());
} }
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; } network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; }
#endif // USE_WIFI_AP
bool WiFiComponent::wifi_disconnect_() { return WiFi.disconnect(); } bool WiFiComponent::wifi_disconnect_() { return WiFi.disconnect(); }
bssid_t WiFiComponent::wifi_bssid() { bssid_t WiFiComponent::wifi_bssid() {

View file

@ -138,6 +138,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
return true; return true;
} }
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) { bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
// TODO: // TODO:
return false; return false;
@ -151,7 +152,9 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return true; return true;
} }
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.localIP()}; } network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.localIP()}; }
#endif // USE_WIFI_AP
bool WiFiComponent::wifi_disconnect_() { bool WiFiComponent::wifi_disconnect_() {
int err = cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); int err = cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA);

View file

@ -50,6 +50,7 @@
#define USE_TOUCHSCREEN #define USE_TOUCHSCREEN
#define USE_UART_DEBUGGER #define USE_UART_DEBUGGER
#define USE_WIFI #define USE_WIFI
#define USE_WIFI_AP
// Arduino-specific feature flags // Arduino-specific feature flags
#ifdef USE_ARDUINO #ifdef USE_ARDUINO

View file

@ -0,0 +1,8 @@
from __future__ import annotations
EVENT_ENTRY_ADDED = "entry_added"
EVENT_ENTRY_REMOVED = "entry_removed"
EVENT_ENTRY_UPDATED = "entry_updated"
EVENT_ENTRY_STATE_CHANGED = "entry_state_changed"
SENTINEL = object()

135
esphome/dashboard/core.py Normal file
View file

@ -0,0 +1,135 @@
from __future__ import annotations
import asyncio
import logging
import threading
from dataclasses import dataclass
from functools import partial
from typing import TYPE_CHECKING, Any, Callable
from ..zeroconf import DiscoveredImport
from .entries import DashboardEntries
from .settings import DashboardSettings
if TYPE_CHECKING:
from .status.mdns import MDNSStatus
_LOGGER = logging.getLogger(__name__)
@dataclass
class Event:
"""Dashboard Event."""
event_type: str
data: dict[str, Any]
class EventBus:
"""Dashboard event bus."""
def __init__(self) -> None:
"""Initialize the Dashboard event bus."""
self._listeners: dict[str, set[Callable[[Event], None]]] = {}
def async_add_listener(
self, event_type: str, listener: Callable[[Event], None]
) -> Callable[[], None]:
"""Add a listener to the event bus."""
self._listeners.setdefault(event_type, set()).add(listener)
return partial(self._async_remove_listener, event_type, listener)
def _async_remove_listener(
self, event_type: str, listener: Callable[[Event], None]
) -> None:
"""Remove a listener from the event bus."""
self._listeners[event_type].discard(listener)
def async_fire(self, event_type: str, event_data: dict[str, Any]) -> None:
"""Fire an event."""
event = Event(event_type, event_data)
_LOGGER.debug("Firing event: %s", event)
for listener in self._listeners.get(event_type, set()):
listener(event)
class ESPHomeDashboard:
"""Class that represents the dashboard."""
__slots__ = (
"bus",
"entries",
"loop",
"import_result",
"stop_event",
"ping_request",
"mqtt_ping_request",
"mdns_status",
"settings",
)
def __init__(self) -> None:
"""Initialize the ESPHomeDashboard."""
self.bus = EventBus()
self.entries: DashboardEntries | None = None
self.loop: asyncio.AbstractEventLoop | None = None
self.import_result: dict[str, DiscoveredImport] = {}
self.stop_event = threading.Event()
self.ping_request: asyncio.Event | None = None
self.mqtt_ping_request = threading.Event()
self.mdns_status: MDNSStatus | None = None
self.settings: DashboardSettings = DashboardSettings()
async def async_setup(self) -> None:
"""Setup the dashboard."""
self.loop = asyncio.get_running_loop()
self.ping_request = asyncio.Event()
self.entries = DashboardEntries(self)
async def async_run(self) -> None:
"""Run the dashboard."""
settings = self.settings
mdns_task: asyncio.Task | None = None
ping_status_task: asyncio.Task | None = None
await self.entries.async_update_entries()
if settings.status_use_ping:
from .status.ping import PingStatus
ping_status = PingStatus()
ping_status_task = asyncio.create_task(ping_status.async_run())
else:
from .status.mdns import MDNSStatus
mdns_status = MDNSStatus()
await mdns_status.async_refresh_hosts()
self.mdns_status = mdns_status
mdns_task = asyncio.create_task(mdns_status.async_run())
if settings.status_use_mqtt:
from .status.mqtt import MqttStatusThread
status_thread_mqtt = MqttStatusThread()
status_thread_mqtt.start()
shutdown_event = asyncio.Event()
try:
await shutdown_event.wait()
finally:
_LOGGER.info("Shutting down...")
self.stop_event.set()
self.ping_request.set()
if ping_status_task:
ping_status_task.cancel()
if mdns_task:
mdns_task.cancel()
if settings.status_use_mqtt:
status_thread_mqtt.join()
self.mqtt_ping_request.set()
await asyncio.sleep(0)
DASHBOARD = ESPHomeDashboard()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,387 @@
from __future__ import annotations
import asyncio
import logging
import os
from collections import defaultdict
from typing import TYPE_CHECKING, Any
from esphome import const, util
from esphome.storage_json import StorageJSON, ext_storage_path
from .const import (
EVENT_ENTRY_ADDED,
EVENT_ENTRY_REMOVED,
EVENT_ENTRY_STATE_CHANGED,
EVENT_ENTRY_UPDATED,
)
from .enum import StrEnum
if TYPE_CHECKING:
from .core import ESPHomeDashboard
_LOGGER = logging.getLogger(__name__)
DashboardCacheKeyType = tuple[int, int, float, int]
# Currently EntryState is a simple
# online/offline/unknown enum, but in the future
# it may be expanded to include more states
class EntryState(StrEnum):
ONLINE = "online"
OFFLINE = "offline"
UNKNOWN = "unknown"
_BOOL_TO_ENTRY_STATE = {
True: EntryState.ONLINE,
False: EntryState.OFFLINE,
None: EntryState.UNKNOWN,
}
_ENTRY_STATE_TO_BOOL = {
EntryState.ONLINE: True,
EntryState.OFFLINE: False,
EntryState.UNKNOWN: None,
}
def bool_to_entry_state(value: bool) -> EntryState:
"""Convert a bool to an entry state."""
return _BOOL_TO_ENTRY_STATE[value]
def entry_state_to_bool(value: EntryState) -> bool | None:
"""Convert an entry state to a bool."""
return _ENTRY_STATE_TO_BOOL[value]
class DashboardEntries:
"""Represents all dashboard entries."""
__slots__ = (
"_dashboard",
"_loop",
"_config_dir",
"_entries",
"_entry_states",
"_loaded_entries",
"_update_lock",
"_name_to_entry",
)
def __init__(self, dashboard: ESPHomeDashboard) -> None:
"""Initialize the DashboardEntries."""
self._dashboard = dashboard
self._loop = asyncio.get_running_loop()
self._config_dir = dashboard.settings.config_dir
# Entries are stored as
# {
# "path/to/file.yaml": DashboardEntry,
# ...
# }
self._entries: dict[str, DashboardEntry] = {}
self._loaded_entries = False
self._update_lock = asyncio.Lock()
self._name_to_entry: dict[str, set[DashboardEntry]] = defaultdict(set)
def get(self, path: str) -> DashboardEntry | None:
"""Get an entry by path."""
return self._entries.get(path)
def get_by_name(self, name: str) -> set[DashboardEntry] | None:
"""Get an entry by name."""
return self._name_to_entry.get(name)
async def _async_all(self) -> list[DashboardEntry]:
"""Return all entries."""
return list(self._entries.values())
def all(self) -> list[DashboardEntry]:
"""Return all entries."""
return asyncio.run_coroutine_threadsafe(self._async_all, self._loop).result()
def async_all(self) -> list[DashboardEntry]:
"""Return all entries."""
return list(self._entries.values())
def set_state(self, entry: DashboardEntry, state: EntryState) -> None:
"""Set the state for an entry."""
asyncio.run_coroutine_threadsafe(
self._async_set_state(entry, state), self._loop
).result()
async def _async_set_state(self, entry: DashboardEntry, state: EntryState) -> None:
"""Set the state for an entry."""
self.async_set_state(entry, state)
def async_set_state(self, entry: DashboardEntry, state: EntryState) -> None:
"""Set the state for an entry."""
if entry.state == state:
return
entry.state = state
self._dashboard.bus.async_fire(
EVENT_ENTRY_STATE_CHANGED, {"entry": entry, "state": state}
)
async def async_request_update_entries(self) -> None:
"""Request an update of the dashboard entries from disk.
If an update is already in progress, this will do nothing.
"""
if self._update_lock.locked():
_LOGGER.debug("Dashboard entries are already being updated")
return
await self.async_update_entries()
async def async_update_entries(self) -> None:
"""Update the dashboard entries from disk."""
async with self._update_lock:
await self._async_update_entries()
def _load_entries(
self, entries: dict[DashboardEntry, DashboardCacheKeyType]
) -> None:
"""Load all entries from disk."""
for entry, cache_key in entries.items():
_LOGGER.debug(
"Loading dashboard entry %s because cache key changed: %s",
entry.path,
cache_key,
)
entry.load_from_disk(cache_key)
async def _async_update_entries(self) -> list[DashboardEntry]:
"""Sync the dashboard entries from disk."""
_LOGGER.debug("Updating dashboard entries")
# At some point it would be nice to use watchdog to avoid polling
path_to_cache_key = await self._loop.run_in_executor(
None, self._get_path_to_cache_key
)
entries = self._entries
name_to_entry = self._name_to_entry
added: dict[DashboardEntry, DashboardCacheKeyType] = {}
updated: dict[DashboardEntry, DashboardCacheKeyType] = {}
removed: set[DashboardEntry] = {
entry
for filename, entry in entries.items()
if filename not in path_to_cache_key
}
original_names: dict[DashboardEntry, str] = {}
for path, cache_key in path_to_cache_key.items():
if not (entry := entries.get(path)):
entry = DashboardEntry(path, cache_key)
added[entry] = cache_key
continue
if entry.cache_key != cache_key:
updated[entry] = cache_key
original_names[entry] = entry.name
if added or updated:
await self._loop.run_in_executor(
None, self._load_entries, {**added, **updated}
)
bus = self._dashboard.bus
for entry in added:
entries[entry.path] = entry
name_to_entry[entry.name].add(entry)
bus.async_fire(EVENT_ENTRY_ADDED, {"entry": entry})
for entry in removed:
del entries[entry.path]
name_to_entry[entry.name].discard(entry)
bus.async_fire(EVENT_ENTRY_REMOVED, {"entry": entry})
for entry in updated:
if (original_name := original_names[entry]) != (current_name := entry.name):
name_to_entry[original_name].discard(entry)
name_to_entry[current_name].add(entry)
bus.async_fire(EVENT_ENTRY_UPDATED, {"entry": entry})
def _get_path_to_cache_key(self) -> dict[str, DashboardCacheKeyType]:
"""Return a dict of path to cache key."""
path_to_cache_key: dict[str, DashboardCacheKeyType] = {}
#
# The cache key is (inode, device, mtime, size)
# which allows us to avoid locking since it ensures
# every iteration of this call will always return the newest
# items from disk at the cost of a stat() call on each
# file which is much faster than reading the file
# for the cache hit case which is the common case.
#
for file in util.list_yaml_files([self._config_dir]):
try:
# Prefer the json storage path if it exists
stat = os.stat(ext_storage_path(os.path.basename(file)))
except OSError:
try:
# Fallback to the yaml file if the storage
# file does not exist or could not be generated
stat = os.stat(file)
except OSError:
# File was deleted, ignore
continue
path_to_cache_key[file] = (
stat.st_ino,
stat.st_dev,
stat.st_mtime,
stat.st_size,
)
return path_to_cache_key
class DashboardEntry:
"""Represents a single dashboard entry.
This class is thread-safe and read-only.
"""
__slots__ = (
"path",
"filename",
"_storage_path",
"cache_key",
"storage",
"state",
"_to_dict",
)
def __init__(self, path: str, cache_key: DashboardCacheKeyType) -> None:
"""Initialize the DashboardEntry."""
self.path = path
self.filename: str = os.path.basename(path)
self._storage_path = ext_storage_path(self.filename)
self.cache_key = cache_key
self.storage: StorageJSON | None = None
self.state = EntryState.UNKNOWN
self._to_dict: dict[str, Any] | None = None
def __repr__(self):
"""Return the representation of this entry."""
return (
f"DashboardEntry(path={self.path} "
f"address={self.address} "
f"web_port={self.web_port} "
f"name={self.name} "
f"no_mdns={self.no_mdns} "
f"state={self.state} "
")"
)
def to_dict(self) -> dict[str, Any]:
"""Return a dict representation of this entry.
The dict includes the loaded configuration but not
the current state of the entry.
"""
if self._to_dict is None:
self._to_dict = {
"name": self.name,
"friendly_name": self.friendly_name,
"configuration": self.filename,
"loaded_integrations": sorted(self.loaded_integrations),
"deployed_version": self.update_old,
"current_version": self.update_new,
"path": self.path,
"comment": self.comment,
"address": self.address,
"web_port": self.web_port,
"target_platform": self.target_platform,
}
return self._to_dict
def load_from_disk(self, cache_key: DashboardCacheKeyType | None = None) -> None:
"""Load this entry from disk."""
self.storage = StorageJSON.load(self._storage_path)
self._to_dict = None
#
# Currently StorageJSON.load() will return None if the file does not exist
#
# StorageJSON currently does not provide an updated cache key so we use the
# one that is passed in.
#
# The cache key was read from the disk moments ago and may be stale but
# it does not matter since we are polling anyways, and the next call to
# async_update_entries() will load it again in the extremely rare case that
# it changed between the two calls.
#
if cache_key:
self.cache_key = cache_key
@property
def address(self) -> str | None:
"""Return the address of this entry."""
if self.storage is None:
return None
return self.storage.address
@property
def no_mdns(self) -> bool | None:
"""Return the no_mdns of this entry."""
if self.storage is None:
return None
return self.storage.no_mdns
@property
def web_port(self) -> int | None:
"""Return the web port of this entry."""
if self.storage is None:
return None
return self.storage.web_port
@property
def name(self) -> str:
"""Return the name of this entry."""
if self.storage is None:
return self.filename.replace(".yml", "").replace(".yaml", "")
return self.storage.name
@property
def friendly_name(self) -> str:
"""Return the friendly name of this entry."""
if self.storage is None:
return self.name
return self.storage.friendly_name
@property
def comment(self) -> str | None:
"""Return the comment of this entry."""
if self.storage is None:
return None
return self.storage.comment
@property
def target_platform(self) -> str | None:
"""Return the target platform of this entry."""
if self.storage is None:
return None
return self.storage.target_platform
@property
def update_available(self) -> bool:
"""Return if an update is available for this entry."""
if self.storage is None:
return True
return self.update_old != self.update_new
@property
def update_old(self) -> str:
if self.storage is None:
return ""
return self.storage.esphome_version or ""
@property
def update_new(self) -> str:
return const.__version__
@property
def loaded_integrations(self) -> set[str]:
if self.storage is None:
return []
return self.storage.loaded_integrations

19
esphome/dashboard/enum.py Normal file
View file

@ -0,0 +1,19 @@
"""Enum backports from standard lib."""
from __future__ import annotations
from enum import Enum
from typing import Any
class StrEnum(str, Enum):
"""Partial backport of Python 3.11's StrEnum for our basic use cases."""
def __new__(cls, value: str, *args: Any, **kwargs: Any) -> StrEnum:
"""Create a new StrEnum instance."""
if not isinstance(value, str):
raise TypeError(f"{value!r} is not a string")
return super().__new__(cls, value, *args, **kwargs)
def __str__(self) -> str:
"""Return self.value."""
return str(self.value)

View file

@ -0,0 +1,78 @@
from __future__ import annotations
import hmac
import os
from pathlib import Path
from typing import Any
from esphome.core import CORE
from esphome.helpers import get_bool_env
from .util.password import password_hash
class DashboardSettings:
"""Settings for the dashboard."""
def __init__(self) -> None:
self.config_dir: str = ""
self.password_hash: str = ""
self.username: str = ""
self.using_password: bool = False
self.on_ha_addon: bool = False
self.cookie_secret: str | None = None
self.absolute_config_dir: Path | None = None
def parse_args(self, args):
self.on_ha_addon: bool = args.ha_addon
password: str = args.password or os.getenv("PASSWORD", "")
if not self.on_ha_addon:
self.username: str = args.username or os.getenv("USERNAME", "")
self.using_password = bool(password)
if self.using_password:
self.password_hash = password_hash(password)
self.config_dir: str = args.configuration
self.absolute_config_dir: Path = Path(self.config_dir).resolve()
CORE.config_path = os.path.join(self.config_dir, ".")
@property
def relative_url(self):
return os.getenv("ESPHOME_DASHBOARD_RELATIVE_URL", "/")
@property
def status_use_ping(self):
return get_bool_env("ESPHOME_DASHBOARD_USE_PING")
@property
def status_use_mqtt(self):
return get_bool_env("ESPHOME_DASHBOARD_USE_MQTT")
@property
def using_ha_addon_auth(self):
if not self.on_ha_addon:
return False
return not get_bool_env("DISABLE_HA_AUTHENTICATION")
@property
def using_auth(self):
return self.using_password or self.using_ha_addon_auth
@property
def streamer_mode(self):
return get_bool_env("ESPHOME_STREAMER_MODE")
def check_password(self, username, password):
if not self.using_auth:
return True
if username != self.username:
return False
# Compare password in constant running time (to prevent timing attacks)
return hmac.compare_digest(self.password_hash, password_hash(password))
def rel_path(self, *args: Any) -> str:
"""Return a path relative to the ESPHome config folder."""
joined_path = os.path.join(self.config_dir, *args)
# Raises ValueError if not relative to ESPHome config folder
Path(joined_path).resolve().relative_to(self.absolute_config_dir)
return joined_path

View file

View file

@ -0,0 +1,98 @@
from __future__ import annotations
import asyncio
from esphome.zeroconf import (
ESPHOME_SERVICE_TYPE,
AsyncEsphomeZeroconf,
DashboardBrowser,
DashboardImportDiscovery,
DashboardStatus,
)
from ..const import SENTINEL
from ..core import DASHBOARD
from ..entries import DashboardEntry, bool_to_entry_state
class MDNSStatus:
"""Class that updates the mdns status."""
def __init__(self) -> None:
"""Initialize the MDNSStatus class."""
super().__init__()
self.aiozc: AsyncEsphomeZeroconf | None = None
# This is the current mdns state for each host (True, False, None)
self.host_mdns_state: dict[str, bool | None] = {}
self._loop = asyncio.get_running_loop()
async def async_resolve_host(self, host_name: str) -> str | None:
"""Resolve a host name to an address in a thread-safe manner."""
if aiozc := self.aiozc:
return await aiozc.async_resolve_host(host_name)
return None
async def async_refresh_hosts(self):
"""Refresh the hosts to track."""
dashboard = DASHBOARD
host_mdns_state = self.host_mdns_state
entries = dashboard.entries
poll_names: dict[str, set[DashboardEntry]] = {}
for entry in entries.async_all():
if entry.no_mdns:
continue
# If we just adopted/imported this host, we likely
# already have a state for it, so we should make sure
# to set it so the dashboard shows it as online
if entry.loaded_integrations and "api" not in entry.loaded_integrations:
# No api available so we have to poll since
# the device won't respond to a request to ._esphomelib._tcp.local.
poll_names.setdefault(entry.name, set()).add(entry)
elif (online := host_mdns_state.get(entry.name, SENTINEL)) != SENTINEL:
entries.async_set_state(entry, bool_to_entry_state(online))
if poll_names and self.aiozc:
results = await asyncio.gather(
*(self.aiozc.async_resolve_host(name) for name in poll_names)
)
for name, address in zip(poll_names, results):
result = bool(address)
host_mdns_state[name] = result
for entry in poll_names[name]:
entries.async_set_state(entry, bool_to_entry_state(result))
async def async_run(self) -> None:
dashboard = DASHBOARD
entries = dashboard.entries
aiozc = AsyncEsphomeZeroconf()
self.aiozc = aiozc
host_mdns_state = self.host_mdns_state
def on_update(dat: dict[str, bool | None]) -> None:
"""Update the entry state."""
for name, result in dat.items():
host_mdns_state[name] = result
if matching_entries := entries.get_by_name(name):
for entry in matching_entries:
if not entry.no_mdns:
entries.async_set_state(entry, bool_to_entry_state(result))
stat = DashboardStatus(on_update)
imports = DashboardImportDiscovery()
dashboard.import_result = imports.import_state
browser = DashboardBrowser(
aiozc.zeroconf,
ESPHOME_SERVICE_TYPE,
[stat.browser_callback, imports.browser_callback],
)
ping_request = dashboard.ping_request
while not dashboard.stop_event.is_set():
await self.async_refresh_hosts()
await ping_request.wait()
ping_request.clear()
await browser.async_cancel()
await aiozc.async_close()
self.aiozc = None

View file

@ -0,0 +1,67 @@
from __future__ import annotations
import binascii
import json
import os
import threading
from esphome import mqtt
from ..core import DASHBOARD
from ..entries import EntryState
class MqttStatusThread(threading.Thread):
"""Status thread to get the status of the devices via MQTT."""
def run(self) -> None:
"""Run the status thread."""
dashboard = DASHBOARD
entries = dashboard.entries
current_entries = entries.all()
config = mqtt.config_from_env()
topic = "esphome/discover/#"
def on_message(client, userdata, msg):
nonlocal current_entries
payload = msg.payload.decode(errors="backslashreplace")
if len(payload) > 0:
data = json.loads(payload)
if "name" not in data:
return
for entry in current_entries:
if entry.name == data["name"]:
entries.set_state(entry, EntryState.ONLINE)
return
def on_connect(client, userdata, flags, return_code):
client.publish("esphome/discover", None, retain=False)
mqttid = str(binascii.hexlify(os.urandom(6)).decode())
client = mqtt.prepare(
config,
[topic],
on_message,
on_connect,
None,
None,
f"esphome-dashboard-{mqttid}",
)
client.loop_start()
while not dashboard.stop_event.wait(2):
current_entries = entries.all()
# will be set to true on on_message
for entry in current_entries:
if entry.no_mdns:
entries.set_state(entry, EntryState.OFFLINE)
client.publish("esphome/discover", None, retain=False)
dashboard.mqtt_ping_request.wait()
dashboard.mqtt_ping_request.clear()
client.disconnect()
client.loop_stop()

View file

@ -0,0 +1,49 @@
from __future__ import annotations
import asyncio
import os
from typing import cast
from ..core import DASHBOARD
from ..entries import DashboardEntry, bool_to_entry_state
from ..util.itertools import chunked
from ..util.subprocess import async_system_command_status
async def _async_ping_host(host: str) -> bool:
"""Ping a host."""
return await async_system_command_status(
["ping", "-n" if os.name == "nt" else "-c", "1", host]
)
class PingStatus:
def __init__(self) -> None:
"""Initialize the PingStatus class."""
super().__init__()
self._loop = asyncio.get_running_loop()
async def async_run(self) -> None:
"""Run the ping status."""
dashboard = DASHBOARD
entries = dashboard.entries
while not dashboard.stop_event.is_set():
# Only ping if the dashboard is open
await dashboard.ping_request.wait()
current_entries = dashboard.entries.async_all()
to_ping: list[DashboardEntry] = [
entry for entry in current_entries if entry.address is not None
]
for ping_group in chunked(to_ping, 16):
ping_group = cast(list[DashboardEntry], ping_group)
results = await asyncio.gather(
*(_async_ping_host(entry.address) for entry in ping_group),
return_exceptions=True,
)
for entry, result in zip(ping_group, results):
if isinstance(result, Exception):
result = False
elif isinstance(result, BaseException):
raise result
entries.async_set_state(entry, bool_to_entry_state(result))

View file

View file

@ -0,0 +1,55 @@
import logging
import os
import tempfile
from pathlib import Path
_LOGGER = logging.getLogger(__name__)
def write_utf8_file(
filename: Path,
utf8_str: str,
private: bool = False,
) -> None:
"""Write a file and rename it into place.
Writes all or nothing.
"""
write_file(filename, utf8_str.encode("utf-8"), private)
# from https://github.com/home-assistant/core/blob/dev/homeassistant/util/file.py
def write_file(
filename: Path,
utf8_data: bytes,
private: bool = False,
) -> None:
"""Write a file and rename it into place.
Writes all or nothing.
"""
tmp_filename = ""
try:
# Modern versions of Python tempfile create this file with mode 0o600
with tempfile.NamedTemporaryFile(
mode="wb", dir=os.path.dirname(filename), delete=False
) as fdesc:
fdesc.write(utf8_data)
tmp_filename = fdesc.name
if not private:
os.fchmod(fdesc.fileno(), 0o644)
os.replace(tmp_filename, filename)
finally:
if os.path.exists(tmp_filename):
try:
os.remove(tmp_filename)
except OSError as err:
# If we are cleaning up then something else went wrong, so
# we should suppress likely follow-on errors in the cleanup
_LOGGER.error(
"File replacement cleanup failed for %s while saving %s: %s",
tmp_filename,
filename,
err,
)

View file

@ -0,0 +1,22 @@
from __future__ import annotations
from collections.abc import Iterable
from functools import partial
from itertools import islice
from typing import Any
def take(take_num: int, iterable: Iterable) -> list[Any]:
"""Return first n items of the iterable as a list.
From itertools recipes
"""
return list(islice(iterable, take_num))
def chunked(iterable: Iterable, chunked_num: int) -> Iterable[Any]:
"""Break *iterable* into lists of length *n*.
From more-itertools
"""
return iter(partial(take, chunked_num, iter(iterable)), [])

View file

@ -0,0 +1,11 @@
from __future__ import annotations
import hashlib
def password_hash(password: str) -> bytes:
"""Create a hash of a password to transform it to a fixed-length digest.
Note this is not meant for secure storage, but for securely comparing passwords.
"""
return hashlib.sha256(password.encode()).digest()

View file

@ -0,0 +1,31 @@
from __future__ import annotations
import asyncio
from collections.abc import Iterable
async def async_system_command_status(command: Iterable[str]) -> bool:
"""Run a system command checking only the status."""
process = await asyncio.create_subprocess_exec(
*command,
stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
close_fds=False,
)
await process.wait()
return process.returncode == 0
async def async_run_system_command(command: Iterable[str]) -> tuple[bool, bytes, bytes]:
"""Run a system command and return a tuple of returncode, stdout, stderr."""
process = await asyncio.create_subprocess_exec(
*command,
stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
close_fds=False,
)
stdout, stderr = await process.communicate()
await process.wait()
return process.returncode, stdout, stderr

View file

@ -1,17 +1,10 @@
import hashlib from __future__ import annotations
import unicodedata import unicodedata
from esphome.const import ALLOWED_NAME_CHARS from esphome.const import ALLOWED_NAME_CHARS
def password_hash(password: str) -> bytes:
"""Create a hash of a password to transform it to a fixed-length digest.
Note this is not meant for secure storage, but for securely comparing passwords.
"""
return hashlib.sha256(password.encode()).digest()
def strip_accents(value): def strip_accents(value):
return "".join( return "".join(
c c

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,13 @@
from __future__ import annotations
import gzip
import hashlib import hashlib
import io
import logging import logging
import random import random
import socket import socket
import sys import sys
import time import time
import gzip
from esphome.core import EsphomeError from esphome.core import EsphomeError
from esphome.helpers import is_ip_address, resolve_ip_address from esphome.helpers import is_ip_address, resolve_ip_address
@ -40,6 +43,10 @@ MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45]
FEATURE_SUPPORTS_COMPRESSION = 0x01 FEATURE_SUPPORTS_COMPRESSION = 0x01
UPLOAD_BLOCK_SIZE = 8192
UPLOAD_BUFFER_SIZE = UPLOAD_BLOCK_SIZE * 8
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -184,7 +191,9 @@ def send_check(sock, data, msg):
raise OTAError(f"Error sending {msg}: {err}") from err raise OTAError(f"Error sending {msg}: {err}") from err
def perform_ota(sock, password, file_handle, filename): def perform_ota(
sock: socket.socket, password: str, file_handle: io.IOBase, filename: str
) -> None:
file_contents = file_handle.read() file_contents = file_handle.read()
file_size = len(file_contents) file_size = len(file_contents)
_LOGGER.info("Uploading %s (%s bytes)", filename, file_size) _LOGGER.info("Uploading %s (%s bytes)", filename, file_size)
@ -254,14 +263,16 @@ def perform_ota(sock, password, file_handle, filename):
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0)
# Limit send buffer (usually around 100kB) in order to have progress bar # Limit send buffer (usually around 100kB) in order to have progress bar
# show the actual progress # show the actual progress
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 8192)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, UPLOAD_BUFFER_SIZE)
# Set higher timeout during upload # Set higher timeout during upload
sock.settimeout(20.0) sock.settimeout(30.0)
start_time = time.perf_counter()
offset = 0 offset = 0
progress = ProgressBar() progress = ProgressBar()
while True: while True:
chunk = upload_contents[offset : offset + 1024] chunk = upload_contents[offset : offset + UPLOAD_BLOCK_SIZE]
if not chunk: if not chunk:
break break
offset += len(chunk) offset += len(chunk)
@ -277,8 +288,9 @@ def perform_ota(sock, password, file_handle, filename):
# Enable nodelay for last checks # Enable nodelay for last checks
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
duration = time.perf_counter() - start_time
_LOGGER.info("Waiting for result...") _LOGGER.info("Upload took %.2f seconds, waiting for result...", duration)
receive_exactly(sock, 1, "receive OK", RESPONSE_RECEIVE_OK) receive_exactly(sock, 1, "receive OK", RESPONSE_RECEIVE_OK)
receive_exactly(sock, 1, "Update end", RESPONSE_UPDATE_END_OK) receive_exactly(sock, 1, "Update end", RESPONSE_UPDATE_END_OK)

75
esphome/external_files.py Normal file
View file

@ -0,0 +1,75 @@
from __future__ import annotations
import logging
from pathlib import Path
import os
from datetime import datetime
import requests
import esphome.config_validation as cv
from esphome.core import CORE, TimePeriodSeconds
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@landonr"]
NETWORK_TIMEOUT = 30
IF_MODIFIED_SINCE = "If-Modified-Since"
CACHE_CONTROL = "Cache-Control"
CACHE_CONTROL_MAX_AGE = "max-age="
CONTENT_DISPOSITION = "content-disposition"
TEMP_DIR = "temp"
def has_remote_file_changed(url, local_file_path):
if os.path.exists(local_file_path):
_LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path)
try:
local_modification_time = os.path.getmtime(local_file_path)
local_modification_time_str = datetime.utcfromtimestamp(
local_modification_time
).strftime("%a, %d %b %Y %H:%M:%S GMT")
headers = {
IF_MODIFIED_SINCE: local_modification_time_str,
CACHE_CONTROL: CACHE_CONTROL_MAX_AGE + "3600",
}
response = requests.head(url, headers=headers, timeout=NETWORK_TIMEOUT)
_LOGGER.debug(
"has_remote_file_changed: File %s, Local modified %s, response code %d",
local_file_path,
local_modification_time_str,
response.status_code,
)
if response.status_code == 304:
_LOGGER.debug(
"has_remote_file_changed: File not modified since %s",
local_modification_time_str,
)
return False
_LOGGER.debug("has_remote_file_changed: File modified")
return True
except requests.exceptions.RequestException as e:
raise cv.Invalid(
f"Could not check if {url} has changed, please check if file exists "
f"({e})"
)
_LOGGER.debug("has_remote_file_changed: File doesn't exists at %s", local_file_path)
return True
def is_file_recent(file_path: str, refresh: TimePeriodSeconds) -> bool:
if os.path.exists(file_path):
creation_time = os.path.getctime(file_path)
current_time = datetime.now().timestamp()
return current_time - creation_time <= refresh.total_seconds
return False
def compute_local_file_dir(domain: str) -> Path:
base_directory = Path(CORE.data_dir) / domain
base_directory.mkdir(parents=True, exist_ok=True)
return base_directory

View file

@ -1,21 +1,15 @@
from __future__ import annotations
import binascii import binascii
import codecs import codecs
from datetime import datetime
import json import json
import logging import logging
import os import os
from typing import Optional from datetime import datetime
from esphome import const from esphome import const
from esphome.const import CONF_DISABLED, CONF_MDNS
from esphome.core import CORE from esphome.core import CORE
from esphome.helpers import write_file_if_changed from esphome.helpers import write_file_if_changed
from esphome.const import (
CONF_MDNS,
CONF_DISABLED,
)
from esphome.types import CoreType from esphome.types import CoreType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -40,48 +34,47 @@ def trash_storage_path() -> str:
class StorageJSON: class StorageJSON:
def __init__( def __init__(
self, self,
storage_version, storage_version: int,
name, name: str,
friendly_name, friendly_name: str,
comment, comment: str,
esphome_version, esphome_version: str,
src_version, src_version: int | None,
address, address: str,
web_port, web_port: int | None,
target_platform, target_platform: str,
build_path, build_path: str,
firmware_bin_path, firmware_bin_path: str,
loaded_integrations, loaded_integrations: set[str],
no_mdns, no_mdns: bool,
): ) -> None:
# Version of the storage JSON schema # Version of the storage JSON schema
assert storage_version is None or isinstance(storage_version, int) assert storage_version is None or isinstance(storage_version, int)
self.storage_version: int = storage_version self.storage_version = storage_version
# The name of the node # The name of the node
self.name: str = name self.name = name
# The friendly name of the node # The friendly name of the node
self.friendly_name: str = friendly_name self.friendly_name = friendly_name
# The comment of the node # The comment of the node
self.comment: str = comment self.comment = comment
# The esphome version this was compiled with # The esphome version this was compiled with
self.esphome_version: str = esphome_version self.esphome_version = esphome_version
# The version of the file in src/main.cpp - Used to migrate the file # The version of the file in src/main.cpp - Used to migrate the file
assert src_version is None or isinstance(src_version, int) assert src_version is None or isinstance(src_version, int)
self.src_version: int = src_version self.src_version = src_version
# Address of the ESP, for example livingroom.local or a static IP # Address of the ESP, for example livingroom.local or a static IP
self.address: str = address self.address = address
# Web server port of the ESP, for example 80 # Web server port of the ESP, for example 80
assert web_port is None or isinstance(web_port, int) assert web_port is None or isinstance(web_port, int)
self.web_port: int = web_port self.web_port = web_port
# The type of hardware in use, like "ESP32", "ESP32C3", "ESP8266", etc. # The type of hardware in use, like "ESP32", "ESP32C3", "ESP8266", etc.
self.target_platform: str = target_platform self.target_platform = target_platform
# The absolute path to the platformio project # The absolute path to the platformio project
self.build_path: str = build_path self.build_path = build_path
# The absolute path to the firmware binary # The absolute path to the firmware binary
self.firmware_bin_path: str = firmware_bin_path self.firmware_bin_path = firmware_bin_path
# A list of strings of names of loaded integrations # A set of strings of names of loaded integrations
self.loaded_integrations: list[str] = loaded_integrations self.loaded_integrations = loaded_integrations
self.loaded_integrations.sort()
# Is mDNS disabled # Is mDNS disabled
self.no_mdns = no_mdns self.no_mdns = no_mdns
@ -98,7 +91,7 @@ class StorageJSON:
"esp_platform": self.target_platform, "esp_platform": self.target_platform,
"build_path": self.build_path, "build_path": self.build_path,
"firmware_bin_path": self.firmware_bin_path, "firmware_bin_path": self.firmware_bin_path,
"loaded_integrations": self.loaded_integrations, "loaded_integrations": sorted(self.loaded_integrations),
"no_mdns": self.no_mdns, "no_mdns": self.no_mdns,
} }
@ -109,9 +102,7 @@ class StorageJSON:
write_file_if_changed(path, self.to_json()) write_file_if_changed(path, self.to_json())
@staticmethod @staticmethod
def from_esphome_core( def from_esphome_core(esph: CoreType, old: StorageJSON | None) -> StorageJSON:
esph: CoreType, old: Optional["StorageJSON"]
) -> "StorageJSON":
hardware = esph.target_platform.upper() hardware = esph.target_platform.upper()
if esph.is_esp32: if esph.is_esp32:
from esphome.components import esp32 from esphome.components import esp32
@ -129,7 +120,7 @@ class StorageJSON:
target_platform=hardware, target_platform=hardware,
build_path=esph.build_path, build_path=esph.build_path,
firmware_bin_path=esph.firmware_bin, firmware_bin_path=esph.firmware_bin,
loaded_integrations=list(esph.loaded_integrations), loaded_integrations=esph.loaded_integrations,
no_mdns=( no_mdns=(
CONF_MDNS in esph.config CONF_MDNS in esph.config
and CONF_DISABLED in esph.config[CONF_MDNS] and CONF_DISABLED in esph.config[CONF_MDNS]
@ -140,7 +131,7 @@ class StorageJSON:
@staticmethod @staticmethod
def from_wizard( def from_wizard(
name: str, friendly_name: str, address: str, platform: str name: str, friendly_name: str, address: str, platform: str
) -> "StorageJSON": ) -> StorageJSON:
return StorageJSON( return StorageJSON(
storage_version=1, storage_version=1,
name=name, name=name,
@ -153,12 +144,12 @@ class StorageJSON:
target_platform=platform, target_platform=platform,
build_path=None, build_path=None,
firmware_bin_path=None, firmware_bin_path=None,
loaded_integrations=[], loaded_integrations=set(),
no_mdns=False, no_mdns=False,
) )
@staticmethod @staticmethod
def _load_impl(path: str) -> Optional["StorageJSON"]: def _load_impl(path: str) -> StorageJSON | None:
with codecs.open(path, "r", encoding="utf-8") as f_handle: with codecs.open(path, "r", encoding="utf-8") as f_handle:
storage = json.load(f_handle) storage = json.load(f_handle)
storage_version = storage["storage_version"] storage_version = storage["storage_version"]
@ -174,7 +165,7 @@ class StorageJSON:
esp_platform = storage.get("esp_platform") esp_platform = storage.get("esp_platform")
build_path = storage.get("build_path") build_path = storage.get("build_path")
firmware_bin_path = storage.get("firmware_bin_path") firmware_bin_path = storage.get("firmware_bin_path")
loaded_integrations = storage.get("loaded_integrations", []) loaded_integrations = set(storage.get("loaded_integrations", []))
no_mdns = storage.get("no_mdns", False) no_mdns = storage.get("no_mdns", False)
return StorageJSON( return StorageJSON(
storage_version, storage_version,
@ -193,7 +184,7 @@ class StorageJSON:
) )
@staticmethod @staticmethod
def load(path: str) -> Optional["StorageJSON"]: def load(path: str) -> StorageJSON | None:
try: try:
return StorageJSON._load_impl(path) return StorageJSON._load_impl(path)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
@ -215,7 +206,7 @@ class EsphomeStorageJSON:
# The last time ESPHome checked for an update as an isoformat encoded str # The last time ESPHome checked for an update as an isoformat encoded str
self.last_update_check_str: str = last_update_check self.last_update_check_str: str = last_update_check
# Cache of the version gotten in the last version check # Cache of the version gotten in the last version check
self.remote_version: Optional[str] = remote_version self.remote_version: str | None = remote_version
def as_dict(self) -> dict: def as_dict(self) -> dict:
return { return {
@ -226,7 +217,7 @@ class EsphomeStorageJSON:
} }
@property @property
def last_update_check(self) -> Optional[datetime]: def last_update_check(self) -> datetime | None:
try: try:
return datetime.strptime(self.last_update_check_str, "%Y-%m-%dT%H:%M:%S") return datetime.strptime(self.last_update_check_str, "%Y-%m-%dT%H:%M:%S")
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
@ -243,7 +234,7 @@ class EsphomeStorageJSON:
write_file_if_changed(path, self.to_json()) write_file_if_changed(path, self.to_json())
@staticmethod @staticmethod
def _load_impl(path: str) -> Optional["EsphomeStorageJSON"]: def _load_impl(path: str) -> EsphomeStorageJSON | None:
with codecs.open(path, "r", encoding="utf-8") as f_handle: with codecs.open(path, "r", encoding="utf-8") as f_handle:
storage = json.load(f_handle) storage = json.load(f_handle)
storage_version = storage["storage_version"] storage_version = storage["storage_version"]
@ -255,14 +246,14 @@ class EsphomeStorageJSON:
) )
@staticmethod @staticmethod
def load(path: str) -> Optional["EsphomeStorageJSON"]: def load(path: str) -> EsphomeStorageJSON | None:
try: try:
return EsphomeStorageJSON._load_impl(path) return EsphomeStorageJSON._load_impl(path)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
return None return None
@staticmethod @staticmethod
def get_default() -> "EsphomeStorageJSON": def get_default() -> EsphomeStorageJSON:
return EsphomeStorageJSON( return EsphomeStorageJSON(
storage_version=1, storage_version=1,
cookie_secret=binascii.hexlify(os.urandom(64)).decode(), cookie_secret=binascii.hexlify(os.urandom(64)).decode(),

View file

@ -1,22 +1,21 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Callable from typing import Callable
from zeroconf import ( from zeroconf import IPVersion, ServiceInfo, ServiceStateChange, Zeroconf
IPVersion, from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf
ServiceBrowser,
ServiceInfo,
ServiceStateChange,
Zeroconf,
)
from esphome.storage_json import StorageJSON, ext_storage_path from esphome.storage_json import StorageJSON, ext_storage_path
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_BACKGROUND_TASKS: set[asyncio.Task] = set()
class HostResolver(ServiceInfo): class HostResolver(ServiceInfo):
"""Resolve a host name to an IP address.""" """Resolve a host name to an IP address."""
@ -65,7 +64,7 @@ class DiscoveredImport:
network: str network: str
class DashboardBrowser(ServiceBrowser): class DashboardBrowser(AsyncServiceBrowser):
"""A class to browse for ESPHome nodes.""" """A class to browse for ESPHome nodes."""
@ -94,7 +93,28 @@ class DashboardImportDiscovery:
# Ignore updates for devices that are not in the import state # Ignore updates for devices that are not in the import state
return return
info = zeroconf.get_service_info(service_type, name) info = AsyncServiceInfo(
service_type,
name,
)
if info.load_from_cache(zeroconf):
self._process_service_info(name, info)
return
task = asyncio.create_task(
self._async_process_service_info(zeroconf, info, service_type, name)
)
_BACKGROUND_TASKS.add(task)
task.add_done_callback(_BACKGROUND_TASKS.discard)
async def _async_process_service_info(
self, zeroconf: Zeroconf, info: AsyncServiceInfo, service_type: str, name: str
) -> None:
"""Process a service info."""
if await info.async_request(zeroconf):
self._process_service_info(name, info)
def _process_service_info(self, name: str, info: ServiceInfo) -> None:
"""Process a service info."""
_LOGGER.debug("-> resolved info: %s", info) _LOGGER.debug("-> resolved info: %s", info)
if info is None: if info is None:
return return
@ -146,14 +166,34 @@ class DashboardImportDiscovery:
) )
def _make_host_resolver(host: str) -> HostResolver:
"""Create a new HostResolver for the given host name."""
name = host.partition(".")[0]
info = HostResolver(
ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}", server=f"{name}.local."
)
return info
class EsphomeZeroconf(Zeroconf): class EsphomeZeroconf(Zeroconf):
def resolve_host(self, host: str, timeout: float = 3.0) -> str | None: def resolve_host(self, host: str, timeout: float = 3.0) -> str | None:
"""Resolve a host name to an IP address.""" """Resolve a host name to an IP address."""
name = host.partition(".")[0] info = _make_host_resolver(host)
info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}")
if ( if (
info.load_from_cache(self) info.load_from_cache(self)
or (timeout and info.request(self, timeout * 1000)) or (timeout and info.request(self, timeout * 1000))
) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)): ) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)):
return str(addresses[0]) return str(addresses[0])
return None return None
class AsyncEsphomeZeroconf(AsyncZeroconf):
async def async_resolve_host(self, host: str, timeout: float = 3.0) -> str | None:
"""Resolve a host name to an IP address."""
info = _make_host_resolver(host)
if (
info.load_from_cache(self.zeroconf)
or (timeout and await info.async_request(self.zeroconf, timeout * 1000))
) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)):
return str(addresses[0])
return None

View file

@ -39,7 +39,7 @@ lib_deps =
bblanchon/ArduinoJson@6.18.5 ; json bblanchon/ArduinoJson@6.18.5 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
pavlodn/HaierProtocol@0.9.20 ; haier pavlodn/HaierProtocol@0.9.24 ; haier
; This is using the repository until a new release is published to PlatformIO ; This is using the repository until a new release is published to PlatformIO
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
build_flags = build_flags =
@ -160,7 +160,7 @@ board_build.filesystem_size = 0.5m
platform = https://github.com/maxgerhardt/platform-raspberrypi.git platform = https://github.com/maxgerhardt/platform-raspberrypi.git
platform_packages = platform_packages =
; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted
earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.4.0/rp2040-3.4.0.zip earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.6.0/rp2040-3.6.0.zip
framework = arduino framework = arduino
lib_deps = lib_deps =

View file

@ -10,8 +10,9 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile
esptool==4.6.2 esptool==4.6.2
click==8.1.7 click==8.1.7
esphome-dashboard==20231107.0 esphome-dashboard==20231107.0
aioesphomeapi==18.4.0 aioesphomeapi==18.5.5
zeroconf==0.122.3 zeroconf==0.127.0
python-magic==0.4.27
# esp-idf requires this, but doesn't bundle it by default # esp-idf requires this, but doesn't bundle it by default
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24

View file

@ -1,3 +1,3 @@
pillow==10.0.1 pillow==10.1.0
cairosvg==2.7.1 cairosvg==2.7.1
cryptography==41.0.4 cryptography==41.0.4

View file

@ -45,7 +45,7 @@ def sub(path, pattern, repl, expected_count=1):
content, count = re.subn(pattern, repl, content, flags=re.MULTILINE) content, count = re.subn(pattern, repl, content, flags=re.MULTILINE)
if expected_count is not None: if expected_count is not None:
assert count == expected_count, f"Pattern {pattern} replacement failed!" assert count == expected_count, f"Pattern {pattern} replacement failed!"
with open(path, "wt") as fh: with open(path, "w") as fh:
fh.write(content) fh.write(content)

View file

View file

View file

@ -0,0 +1,53 @@
import os
from pathlib import Path
from unittest.mock import patch
import py
import pytest
from esphome.dashboard.util.file import write_file, write_utf8_file
def test_write_utf8_file(tmp_path: Path) -> None:
write_utf8_file(tmp_path.joinpath("foo.txt"), "foo")
assert tmp_path.joinpath("foo.txt").read_text() == "foo"
with pytest.raises(OSError):
write_utf8_file(Path("/not-writable"), "bar")
def test_write_file(tmp_path: Path) -> None:
write_file(tmp_path.joinpath("foo.txt"), b"foo")
assert tmp_path.joinpath("foo.txt").read_text() == "foo"
def test_write_utf8_file_fails_at_rename(
tmpdir: py.path.local, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that if rename fails not not remove, we do not log the failed cleanup."""
test_dir = tmpdir.mkdir("files")
test_file = Path(test_dir / "test.json")
with pytest.raises(OSError), patch(
"esphome.dashboard.util.file.os.replace", side_effect=OSError
):
write_utf8_file(test_file, '{"some":"data"}', False)
assert not os.path.exists(test_file)
assert "File replacement cleanup failed" not in caplog.text
def test_write_utf8_file_fails_at_rename_and_remove(
tmpdir: py.path.local, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that if rename and remove both fail, we log the failed cleanup."""
test_dir = tmpdir.mkdir("files")
test_file = Path(test_dir / "test.json")
with pytest.raises(OSError), patch(
"esphome.dashboard.util.file.os.remove", side_effect=OSError
), patch("esphome.dashboard.util.file.os.replace", side_effect=OSError):
write_utf8_file(test_file, '{"some":"data"}', False)
assert "File replacement cleanup failed" in caplog.text

View file

@ -3072,6 +3072,9 @@ remote_receiver:
on_coolix: on_coolix:
then: then:
delay: !lambda "return x.first + x.second;" delay: !lambda "return x.first + x.second;"
on_rc_switch:
then:
delay: !lambda "return uint32_t(x.code) + x.protocol;"
status_led: status_led:
pin: GPIO2 pin: GPIO2

View file

@ -718,6 +718,7 @@ stepper:
interval: interval:
interval: 5s interval: 5s
startup_delay: 10s
then: then:
- logger.log: Interval Run - logger.log: Interval Run
@ -752,6 +753,18 @@ image:
file: pnglogo.png file: pnglogo.png
type: RGB565 type: RGB565
use_transparency: no use_transparency: no
- id: web_svg_image
file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
resize: 256x48
type: TRANSPARENT_BINARY
- id: web_tiff_image
file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
type: RGB24
resize: 48x48
- id: web_redirect_image
file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
type: RGB24
resize: 48x48
- id: mdi_alert - id: mdi_alert
file: mdi:alert-circle-outline file: mdi:alert-circle-outline

View file

@ -1,4 +1,4 @@
from typing import Iterator from collections.abc import Iterator
import math import math

View file

@ -1,5 +1,5 @@
import pytest import pytest
from mock import Mock from unittest.mock import Mock
from esphome import cpp_helpers as ch from esphome import cpp_helpers as ch
from esphome import const from esphome import const