mirror of
https://github.com/esphome/esphome.git
synced 2024-12-12 00:14:53 +01:00
Merge branch 'dev' into optolink
This commit is contained in:
commit
212ca30505
87 changed files with 4186 additions and 2604 deletions
97
.github/actions/build-image/action.yaml
vendored
Normal file
97
.github/actions/build-image/action.yaml
vendored
Normal 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
|
130
.github/workflows/release.yml
vendored
130
.github/workflows/release.yml
vendored
|
@ -63,30 +63,20 @@ jobs:
|
|||
run: twine upload dist/*
|
||||
|
||||
deploy-docker:
|
||||
name: Build and publish ESPHome ${{ matrix.image.title}}
|
||||
name: Build ESPHome ${{ matrix.platform }}
|
||||
if: github.repository == 'esphome/esphome'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: ${{ matrix.image.title == 'lint' }}
|
||||
needs: [init]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image:
|
||||
- title: "ha-addon"
|
||||
suffix: "hassio"
|
||||
target: "hassio"
|
||||
baseimg: "hassio"
|
||||
- title: "docker"
|
||||
suffix: ""
|
||||
target: "docker"
|
||||
baseimg: "docker"
|
||||
- title: "lint"
|
||||
suffix: "lint"
|
||||
target: "lint"
|
||||
baseimg: "docker"
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm/v7
|
||||
- linux/arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python
|
||||
|
@ -97,6 +87,7 @@ jobs:
|
|||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- name: Set up QEMU
|
||||
if: matrix.platform != 'linux/amd64'
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
|
@ -111,34 +102,105 @@ jobs:
|
|||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build docker
|
||||
uses: ./.github/actions/build-image
|
||||
with:
|
||||
platform: ${{ matrix.platform }}
|
||||
target: docker
|
||||
baseimg: docker
|
||||
suffix: ""
|
||||
version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Build ha-addon
|
||||
uses: ./.github/actions/build-image
|
||||
with:
|
||||
platform: ${{ matrix.platform }}
|
||||
target: hassio
|
||||
baseimg: hassio
|
||||
suffix: "hassio"
|
||||
version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Build lint
|
||||
uses: ./.github/actions/build-image
|
||||
with:
|
||||
platform: ${{ matrix.platform }}
|
||||
target: lint
|
||||
baseimg: docker
|
||||
suffix: lint
|
||||
version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
deploy-manifest:
|
||||
name: Publish ESPHome ${{ matrix.image.title }} to ${{ matrix.registry }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- init
|
||||
- deploy-docker
|
||||
if: github.repository == 'esphome/esphome'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image:
|
||||
- title: "ha-addon"
|
||||
target: "hassio"
|
||||
suffix: "hassio"
|
||||
- title: "docker"
|
||||
target: "docker"
|
||||
suffix: ""
|
||||
- title: "lint"
|
||||
target: "lint"
|
||||
suffix: "lint"
|
||||
registry:
|
||||
- ghcr
|
||||
- dockerhub
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v3.0.2
|
||||
with:
|
||||
name: digests-${{ matrix.image.target }}-${{ matrix.registry }}
|
||||
path: /tmp/digests
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
if: matrix.registry == 'ghcr'
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate short tags
|
||||
id: tags
|
||||
run: |
|
||||
docker/generate_tags.py \
|
||||
output=$(docker/generate_tags.py \
|
||||
--tag "${{ needs.init.outputs.tag }}" \
|
||||
--suffix "${{ matrix.image.suffix }}"
|
||||
--suffix "${{ matrix.image.suffix }}" \
|
||||
--registry "${{ matrix.registry }}")
|
||||
echo $output
|
||||
for l in $output; do
|
||||
echo $l >> $GITHUB_OUTPUT
|
||||
done
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
target: ${{ matrix.image.target }}
|
||||
push: true
|
||||
# yamllint disable rule:line-length
|
||||
cache-from: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }}
|
||||
cache-to: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }},mode=max
|
||||
# yamllint enable rule:line-length
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
build-args: |
|
||||
BASEIMGTYPE=${{ matrix.image.baseimg }}
|
||||
BUILD_VERSION=${{ needs.init.outputs.tag }}
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
|
||||
$(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)
|
||||
|
||||
deploy-ha-addon-repo:
|
||||
if: github.repository == 'esphome/esphome' && github.event_name == 'release'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [deploy-docker]
|
||||
needs: [deploy-manifest]
|
||||
steps:
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@v6.4.1
|
||||
|
|
|
@ -38,6 +38,7 @@ RUN \
|
|||
openssh-client=1:9.2p1-2+deb12u1 \
|
||||
python3-cffi=1.15.1-5 \
|
||||
libcairo2=1.16.0-7 \
|
||||
libmagic1=1:5.44-3 \
|
||||
patch=2.7.6-7; \
|
||||
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
|
||||
apt-get install -y --no-install-recommends \
|
||||
|
@ -48,6 +49,8 @@ RUN \
|
|||
libfreetype-dev=2.12.1+dfsg-5 \
|
||||
libssl-dev=3.0.11-1~deb12u2 \
|
||||
libffi-dev=3.4.4-1 \
|
||||
libopenjp2-7=2.5.0-2 \
|
||||
libtiff6=4.5.0-6 \
|
||||
cargo=0.66.0+ds1-1 \
|
||||
pkg-config=1.8.1-1 \
|
||||
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
|
||||
RUN \
|
||||
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
|
||||
ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3; \
|
||||
ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \
|
||||
fi
|
||||
|
||||
RUN \
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
#!/usr/bin/env python3
|
||||
import re
|
||||
import os
|
||||
import argparse
|
||||
import json
|
||||
|
||||
CHANNEL_DEV = "dev"
|
||||
CHANNEL_BETA = "beta"
|
||||
CHANNEL_RELEASE = "release"
|
||||
|
||||
GHCR = "ghcr"
|
||||
DOCKERHUB = "dockerhub"
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--tag",
|
||||
|
@ -21,21 +22,31 @@ parser.add_argument(
|
|||
required=True,
|
||||
help="The suffix of the tag.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--registry",
|
||||
type=str,
|
||||
choices=[GHCR, DOCKERHUB],
|
||||
required=False,
|
||||
action="append",
|
||||
help="The registry to build tags for.",
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
# detect channel from tag
|
||||
match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag)
|
||||
match = re.match(r"^(\d+\.\d+)(?:\.\d+)(?:(b\d+)|(-dev\d+))?$", args.tag)
|
||||
major_minor_version = None
|
||||
if match is None:
|
||||
if match is None: # eg 2023.12.0-dev20231109-testbranch
|
||||
channel = None # Ran with custom tag for a branch etc
|
||||
elif match.group(3) is not None: # eg 2023.12.0-dev20231109
|
||||
channel = CHANNEL_DEV
|
||||
elif match.group(2) is None:
|
||||
elif match.group(2) is not None: # eg 2023.12.0b1
|
||||
channel = CHANNEL_BETA
|
||||
else: # eg 2023.12.0
|
||||
major_minor_version = match.group(1)
|
||||
channel = CHANNEL_RELEASE
|
||||
else:
|
||||
channel = CHANNEL_BETA
|
||||
|
||||
tags_to_push = [args.tag]
|
||||
if channel == CHANNEL_DEV:
|
||||
|
@ -53,15 +64,28 @@ def main():
|
|||
|
||||
suffix = f"-{args.suffix}" if args.suffix else ""
|
||||
|
||||
with open(os.environ["GITHUB_OUTPUT"], "w") as f:
|
||||
print(f"channel={channel}", file=f)
|
||||
print(f"image=esphome/esphome{suffix}", file=f)
|
||||
image_name = f"esphome/esphome{suffix}"
|
||||
|
||||
print(f"channel={channel}")
|
||||
|
||||
if args.registry is None:
|
||||
args.registry = [GHCR, DOCKERHUB]
|
||||
elif len(args.registry) == 1:
|
||||
if GHCR in args.registry:
|
||||
print(f"image=ghcr.io/{image_name}")
|
||||
if DOCKERHUB in args.registry:
|
||||
print(f"image=docker.io/{image_name}")
|
||||
|
||||
print(f"image_name={image_name}")
|
||||
|
||||
full_tags = []
|
||||
|
||||
for tag in tags_to_push:
|
||||
full_tags += [f"ghcr.io/esphome/esphome{suffix}:{tag}"]
|
||||
full_tags += [f"esphome/esphome{suffix}:{tag}"]
|
||||
print(f"tags={','.join(full_tags)}", file=f)
|
||||
if GHCR in args.registry:
|
||||
full_tags += [f"ghcr.io/{image_name}:{tag}"]
|
||||
if DOCKERHUB in args.registry:
|
||||
full_tags += [f"docker.io/{image_name}:{tag}"]
|
||||
print(f"tags={','.join(full_tags)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -514,7 +514,7 @@ def command_clean(args, config):
|
|||
def command_dashboard(args):
|
||||
from esphome.dashboard import dashboard
|
||||
|
||||
return dashboard.start_web_server(args)
|
||||
return dashboard.start_dashboard(args)
|
||||
|
||||
|
||||
def command_update_all(args):
|
||||
|
|
|
@ -8,7 +8,6 @@ from typing import Any
|
|||
from aioesphomeapi import APIClient
|
||||
from aioesphomeapi.api_pb2 import SubscribeLogsResponse
|
||||
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.core import CORE
|
||||
|
@ -18,24 +17,22 @@ from . import CONF_ENCRYPTION
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_run_logs(config, address):
|
||||
async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
"""Run the logs command in the event loop."""
|
||||
conf = config["api"]
|
||||
name = config["esphome"]["name"]
|
||||
port: int = int(conf[CONF_PORT])
|
||||
password: str = conf[CONF_PASSWORD]
|
||||
noise_psk: str | None = None
|
||||
if CONF_ENCRYPTION in conf:
|
||||
noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
|
||||
_LOGGER.info("Starting log output from %s using esphome API", address)
|
||||
aiozc = AsyncZeroconf()
|
||||
|
||||
cli = APIClient(
|
||||
address,
|
||||
port,
|
||||
password,
|
||||
client_info=f"ESPHome Logs {__version__}",
|
||||
noise_psk=noise_psk,
|
||||
zeroconf_instance=aiozc.zeroconf,
|
||||
)
|
||||
dashboard = CORE.dashboard
|
||||
|
||||
|
@ -48,12 +45,10 @@ async def async_run_logs(config, address):
|
|||
text = text.replace("\033", "\\033")
|
||||
print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}")
|
||||
|
||||
stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc)
|
||||
stop = await async_run(cli, on_log, name=name)
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(60)
|
||||
await asyncio.Event().wait()
|
||||
finally:
|
||||
await aiozc.async_close()
|
||||
await stop()
|
||||
|
||||
|
||||
|
|
|
@ -1,38 +1,37 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import (
|
||||
climate,
|
||||
remote_transmitter,
|
||||
remote_receiver,
|
||||
sensor,
|
||||
remote_base,
|
||||
)
|
||||
from esphome.components.remote_base import CONF_RECEIVER_ID, CONF_TRANSMITTER_ID
|
||||
from esphome.components import climate, sensor, remote_base
|
||||
from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR
|
||||
|
||||
DEPENDENCIES = ["remote_transmitter"]
|
||||
AUTO_LOAD = ["sensor", "remote_base"]
|
||||
CODEOWNERS = ["@glmnet"]
|
||||
|
||||
climate_ir_ns = cg.esphome_ns.namespace("climate_ir")
|
||||
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_HEAT, default=True): cv.boolean,
|
||||
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(
|
||||
{
|
||||
cv.Optional(CONF_RECEIVER_ID): cv.use_id(
|
||||
remote_receiver.RemoteReceiverComponent
|
||||
cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id(
|
||||
remote_base.RemoteReceiverBase
|
||||
),
|
||||
}
|
||||
)
|
||||
|
@ -41,15 +40,11 @@ CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend(
|
|||
async def register_climate_ir(var, config):
|
||||
await cg.register_component(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_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):
|
||||
sens = await cg.get_variable(sensor_id)
|
||||
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))
|
||||
|
|
|
@ -18,7 +18,10 @@ namespace climate_ir {
|
|||
Likewise to decode a IR into the AC state, implement
|
||||
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:
|
||||
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 = {},
|
||||
|
@ -35,9 +38,6 @@ class ClimateIR : public climate::Climate, public Component, public remote_base:
|
|||
|
||||
void setup() 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_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||
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::ClimatePreset> presets_ = {};
|
||||
|
||||
remote_transmitter::RemoteTransmitterComponent *transmitter_;
|
||||
sensor::Sensor *sensor_{nullptr};
|
||||
};
|
||||
|
||||
|
|
|
@ -102,11 +102,7 @@ void CoolixClimate::transmit_state() {
|
|||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "Sending coolix code: 0x%06" PRIX32, remote_state);
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
remote_base::CoolixProtocol().encode(data, remote_state);
|
||||
transmit.perform();
|
||||
this->transmit_<remote_base::CoolixProtocol>(remote_state);
|
||||
}
|
||||
|
||||
bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data) {
|
||||
|
|
|
@ -3,23 +3,26 @@ from typing import Union, Optional
|
|||
from pathlib import Path
|
||||
import logging
|
||||
import os
|
||||
import esphome.final_validate as fv
|
||||
|
||||
from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p
|
||||
from esphome.const import (
|
||||
CONF_ADVANCED,
|
||||
CONF_BOARD,
|
||||
CONF_COMPONENTS,
|
||||
CONF_ESPHOME,
|
||||
CONF_FRAMEWORK,
|
||||
CONF_IGNORE_EFUSE_MAC_CRC,
|
||||
CONF_NAME,
|
||||
CONF_PATH,
|
||||
CONF_PLATFORMIO_OPTIONS,
|
||||
CONF_REF,
|
||||
CONF_REFRESH,
|
||||
CONF_SOURCE,
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
CONF_VARIANT,
|
||||
CONF_VERSION,
|
||||
CONF_ADVANCED,
|
||||
CONF_REFRESH,
|
||||
CONF_PATH,
|
||||
CONF_URL,
|
||||
CONF_REF,
|
||||
CONF_IGNORE_EFUSE_MAC_CRC,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
KEY_NAME,
|
||||
|
@ -327,6 +330,32 @@ def _detect_variant(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"
|
||||
|
||||
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(
|
||||
cv.Schema(
|
||||
{
|
||||
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_FRAMEWORK, default={}): FRAMEWORK_SCHEMA,
|
||||
}
|
||||
|
@ -399,8 +442,12 @@ CONFIG_SCHEMA = cv.All(
|
|||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
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_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||
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_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")
|
||||
|
||||
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]}"],
|
||||
)
|
||||
|
||||
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_define(
|
||||
|
@ -505,24 +558,47 @@ async def to_code(config):
|
|||
)
|
||||
|
||||
|
||||
ARDUINO_PARTITIONS_CSV = """\
|
||||
nvs, data, nvs, 0x009000, 0x005000,
|
||||
otadata, data, ota, 0x00e000, 0x002000,
|
||||
app0, app, ota_0, 0x010000, 0x1C0000,
|
||||
app1, app, ota_1, 0x1D0000, 0x1C0000,
|
||||
eeprom, data, 0x99, 0x390000, 0x001000,
|
||||
spiffs, data, spiffs, 0x391000, 0x00F000
|
||||
APP_PARTITION_SIZES = {
|
||||
"2MB": 0x0C0000, # 768 KB
|
||||
"4MB": 0x1C0000, # 1792 KB
|
||||
"8MB": 0x3C0000, # 3840 KB
|
||||
"16MB": 0x7C0000, # 7936 KB
|
||||
"32MB": 0xFC0000, # 16128 KB
|
||||
}
|
||||
|
||||
|
||||
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 = """\
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
def get_idf_partition_csv(flash_size):
|
||||
app_partition_size = APP_PARTITION_SIZES[flash_size]
|
||||
|
||||
partition_csv = f"""\
|
||||
otadata, data, ota, , 0x2000,
|
||||
phy_init, data, phy, , 0x1000,
|
||||
app0, app, ota_0, , 0x1C0000,
|
||||
app1, app, ota_1, , 0x1C0000,
|
||||
nvs, data, nvs, , 0x6d000,
|
||||
app0, app, ota_0, , 0x{app_partition_size:X},
|
||||
app1, app, ota_1, , 0x{app_partition_size:X},
|
||||
nvs, data, nvs, , 0x6D000,
|
||||
"""
|
||||
return partition_csv
|
||||
|
||||
|
||||
def _format_sdkconfig_val(value: SdkconfigValueType) -> str:
|
||||
|
@ -565,13 +641,17 @@ def copy_files():
|
|||
if CORE.using_arduino:
|
||||
write_file_if_changed(
|
||||
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:
|
||||
_write_sdkconfig()
|
||||
write_file_if_changed(
|
||||
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.
|
||||
# However, if the build path does not have an initialized git repo,
|
||||
|
|
|
@ -67,13 +67,13 @@ def validate_pillow_installed(value):
|
|||
except ImportError as err:
|
||||
raise cv.Invalid(
|
||||
"Please install the pillow python package to use this feature. "
|
||||
'(pip install "pillow==10.0.1")'
|
||||
'(pip install "pillow==10.1.0")'
|
||||
) 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(
|
||||
"Please update your pillow installation to 10.0.1. "
|
||||
'(pip install "pillow==10.0.1")'
|
||||
"Please update your pillow installation to 10.1.0. "
|
||||
'(pip install "pillow==10.1.0")'
|
||||
)
|
||||
|
||||
return value
|
||||
|
|
|
@ -38,16 +38,20 @@ PROTOCOL_MIN_TEMPERATURE = 16.0
|
|||
PROTOCOL_MAX_TEMPERATURE = 30.0
|
||||
PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0
|
||||
PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5
|
||||
PROTOCOL_CONTROL_PACKET_SIZE = 10
|
||||
|
||||
CODEOWNERS = ["@paveldn"]
|
||||
AUTO_LOAD = ["sensor"]
|
||||
DEPENDENCIES = ["climate", "uart"]
|
||||
CONF_WIFI_SIGNAL = "wifi_signal"
|
||||
CONF_ALTERNATIVE_SWING_CONTROL = "alternative_swing_control"
|
||||
CONF_ANSWER_TIMEOUT = "answer_timeout"
|
||||
CONF_CONTROL_METHOD = "control_method"
|
||||
CONF_CONTROL_PACKET_SIZE = "control_packet_size"
|
||||
CONF_DISPLAY = "display"
|
||||
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
|
||||
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
|
||||
CONF_VERTICAL_AIRFLOW = "vertical_airflow"
|
||||
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
|
||||
CONF_WIFI_SIGNAL = "wifi_signal"
|
||||
|
||||
PROTOCOL_HON = "HON"
|
||||
PROTOCOL_SMARTAIR2 = "SMARTAIR2"
|
||||
|
@ -107,6 +111,13 @@ SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = {
|
|||
"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):
|
||||
if CONF_VISUAL in config:
|
||||
|
@ -184,6 +195,9 @@ CONFIG_SCHEMA = cv.All(
|
|||
PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Smartair2Climate),
|
||||
cv.Optional(
|
||||
CONF_ALTERNATIVE_SWING_CONTROL, default=False
|
||||
): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_SUPPORTED_PRESETS,
|
||||
default=list(
|
||||
|
@ -197,7 +211,15 @@ CONFIG_SCHEMA = cv.All(
|
|||
PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend(
|
||||
{
|
||||
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_CONTROL_PACKET_SIZE, default=PROTOCOL_CONTROL_PACKET_SIZE
|
||||
): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50),
|
||||
cv.Optional(
|
||||
CONF_SUPPORTED_PRESETS,
|
||||
default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()),
|
||||
|
@ -408,6 +430,8 @@ async def to_code(config):
|
|||
await climate.register_climate(var, config)
|
||||
|
||||
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:
|
||||
cg.add(var.set_beeper_state(config[CONF_BEEPER]))
|
||||
if CONF_DISPLAY in config:
|
||||
|
@ -423,5 +447,15 @@ async def to_code(config):
|
|||
cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS]))
|
||||
if CONF_ANSWER_TIMEOUT in config:
|
||||
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
|
||||
cg.add_library("pavlodn/HaierProtocol", "0.9.20")
|
||||
cg.add_library("pavlodn/HaierProtocol", "0.9.24")
|
||||
|
|
|
@ -19,56 +19,45 @@ constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000;
|
|||
constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000;
|
||||
constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000;
|
||||
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) {
|
||||
static const char *phase_names[] = {
|
||||
"SENDING_INIT_1",
|
||||
"WAITING_INIT_1_ANSWER",
|
||||
"SENDING_INIT_2",
|
||||
"WAITING_INIT_2_ANSWER",
|
||||
"SENDING_FIRST_STATUS_REQUEST",
|
||||
"WAITING_FIRST_STATUS_ANSWER",
|
||||
"SENDING_ALARM_STATUS_REQUEST",
|
||||
"WAITING_ALARM_STATUS_ANSWER",
|
||||
"IDLE",
|
||||
"UNKNOWN",
|
||||
"SENDING_STATUS_REQUEST",
|
||||
"WAITING_STATUS_ANSWER",
|
||||
"SENDING_UPDATE_SIGNAL_REQUEST",
|
||||
"WAITING_UPDATE_SIGNAL_ANSWER",
|
||||
"SENDING_SIGNAL_LEVEL",
|
||||
"WAITING_SIGNAL_LEVEL_ANSWER",
|
||||
"SENDING_CONTROL",
|
||||
"WAITING_CONTROL_ANSWER",
|
||||
"SENDING_POWER_ON_COMMAND",
|
||||
"WAITING_POWER_ON_ANSWER",
|
||||
"SENDING_POWER_OFF_COMMAND",
|
||||
"WAITING_POWER_OFF_ANSWER",
|
||||
"SENDING_ACTION_COMMAND",
|
||||
"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;
|
||||
if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0))
|
||||
phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES;
|
||||
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()
|
||||
: haier_protocol_(*this),
|
||||
protocol_phase_(ProtocolPhases::SENDING_INIT_1),
|
||||
action_request_(ActionRequest::NO_ACTION),
|
||||
display_status_(true),
|
||||
health_mode_(false),
|
||||
force_send_control_(false),
|
||||
forced_publish_(false),
|
||||
forced_request_status_(false),
|
||||
first_control_attempt_(false),
|
||||
reset_protocol_request_(false),
|
||||
send_wifi_signal_(true) {
|
||||
send_wifi_signal_(true),
|
||||
use_crc_(false) {
|
||||
this->traits_ = climate::ClimateTraits();
|
||||
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,
|
||||
|
@ -84,42 +73,43 @@ HaierClimateBase::~HaierClimateBase() {}
|
|||
|
||||
void HaierClimateBase::set_phase(ProtocolPhases 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));
|
||||
#else
|
||||
ESP_LOGV(TAG, "Phase transition: %d => %d", (int) this->protocol_phase_, (int) phase);
|
||||
#endif
|
||||
this->protocol_phase_ = phase;
|
||||
}
|
||||
}
|
||||
|
||||
bool HaierClimateBase::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;
|
||||
void HaierClimateBase::reset_phase_() {
|
||||
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||
: 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) {
|
||||
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) {
|
||||
return this->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);
|
||||
return check_timeout(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS);
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
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};
|
||||
if (wifi::global_wifi_component->is_connected()) {
|
||||
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[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
|
||||
|
||||
|
@ -140,7 +131,7 @@ bool HaierClimateBase::get_display_state() const { return this->display_status_;
|
|||
void HaierClimateBase::set_display_state(bool state) {
|
||||
if (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) {
|
||||
if (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) {
|
||||
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);
|
||||
}
|
||||
|
||||
void HaierClimateBase::set_answer_timeout(uint32_t timeout) {
|
||||
this->answer_timeout_ = std::chrono::milliseconds(timeout);
|
||||
}
|
||||
void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); }
|
||||
|
||||
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &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; }
|
||||
|
||||
haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type,
|
||||
uint8_t expected_request_message_type,
|
||||
uint8_t answer_message_type,
|
||||
uint8_t expected_answer_message_type,
|
||||
void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) {
|
||||
this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message});
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
if (is_message_invalid(answer_message_type))
|
||||
if (answer_message_type == haier_protocol::FrameType::INVALID)
|
||||
result = haier_protocol::HandlerError::INVALID_ANSWER;
|
||||
return result;
|
||||
}
|
||||
|
||||
haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) {
|
||||
#if (HAIER_LOG_LEVEL > 4)
|
||||
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_));
|
||||
#else
|
||||
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_);
|
||||
#endif
|
||||
haier_protocol::HandlerError HaierClimateBase::report_network_status_answer_handler_(
|
||||
haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data,
|
||||
size_t data_size) {
|
||||
haier_protocol::HandlerError result =
|
||||
this->answer_preprocess_(request_type, haier_protocol::FrameType::REPORT_NETWORK_STATUS, message_type,
|
||||
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) {
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
} else {
|
||||
|
@ -219,79 +230,95 @@ void HaierClimateBase::setup() {
|
|||
// Set timestamp here to give AC time to boot
|
||||
this->last_request_timestamp_ = std::chrono::steady_clock::now();
|
||||
this->set_phase(ProtocolPhases::SENDING_INIT_1);
|
||||
this->set_handlers();
|
||||
this->haier_protocol_.set_default_timeout_handler(
|
||||
std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1));
|
||||
this->set_handlers();
|
||||
}
|
||||
|
||||
void HaierClimateBase::dump_config() {
|
||||
LOG_CLIMATE("", "Haier Climate", this);
|
||||
ESP_LOGCONFIG(TAG, " Device communication status: %s",
|
||||
(this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none");
|
||||
ESP_LOGCONFIG(TAG, " Device communication status: %s", this->valid_connection() ? "established" : "none");
|
||||
}
|
||||
|
||||
void HaierClimateBase::loop() {
|
||||
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() >
|
||||
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) {
|
||||
// No status too long, reseting protocol
|
||||
// No need to reset protocol if we didn't pass initialization phase
|
||||
if (this->reset_protocol_request_) {
|
||||
this->reset_protocol_request_ = false;
|
||||
ESP_LOGW(TAG, "Protocol reset requested");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Communication timeout, reseting protocol");
|
||||
}
|
||||
this->last_valid_status_timestamp_ = now;
|
||||
this->set_force_send_control_(false);
|
||||
if (this->hvac_settings_.valid)
|
||||
this->hvac_settings_.reset();
|
||||
this->set_phase(ProtocolPhases::SENDING_INIT_1);
|
||||
this->process_protocol_reset();
|
||||
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_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
|
||||
// procedure or waiting for an answer
|
||||
if (this->action_request_ != ActionRequest::NO_ACTION) {
|
||||
this->process_pending_action();
|
||||
} else if (this->hvac_settings_.valid || this->force_send_control_) {
|
||||
if (this->action_request_.has_value() && this->prepare_pending_action()) {
|
||||
this->set_phase(ProtocolPhases::SENDING_ACTION_COMMAND);
|
||||
} else if (this->next_hvac_settings_.valid || this->force_send_control_) {
|
||||
ESP_LOGV(TAG, "Control packet is pending...");
|
||||
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->haier_protocol_.loop();
|
||||
}
|
||||
|
||||
void HaierClimateBase::process_pending_action() {
|
||||
ActionRequest request = this->action_request_;
|
||||
if (this->action_request_ == ActionRequest::TOGGLE_POWER) {
|
||||
request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF;
|
||||
void HaierClimateBase::process_protocol_reset() {
|
||||
this->force_send_control_ = false;
|
||||
if (this->current_hvac_settings_.valid)
|
||||
this->current_hvac_settings_.reset();
|
||||
if (this->next_hvac_settings_.valid)
|
||||
this->next_hvac_settings_.reset();
|
||||
this->mode = CLIMATE_MODE_OFF;
|
||||
this->current_temperature = NAN;
|
||||
this->target_temperature = NAN;
|
||||
this->fan_mode.reset();
|
||||
this->preset.reset();
|
||||
this->publish_state();
|
||||
this->set_phase(ProtocolPhases::SENDING_INIT_1);
|
||||
}
|
||||
switch (request) {
|
||||
|
||||
bool HaierClimateBase::prepare_pending_action() {
|
||||
if (this->action_request_.has_value()) {
|
||||
switch (this->action_request_.value().action) {
|
||||
case ActionRequest::SEND_CUSTOM_COMMAND:
|
||||
return true;
|
||||
case ActionRequest::TURN_POWER_ON:
|
||||
this->set_phase(ProtocolPhases::SENDING_POWER_ON_COMMAND);
|
||||
break;
|
||||
this->action_request_.value().message = this->get_power_message(true);
|
||||
return true;
|
||||
case ActionRequest::TURN_POWER_OFF:
|
||||
this->set_phase(ProtocolPhases::SENDING_POWER_OFF_COMMAND);
|
||||
break;
|
||||
this->action_request_.value().message = this->get_power_message(false);
|
||||
return true;
|
||||
case ActionRequest::TOGGLE_POWER:
|
||||
case ActionRequest::NO_ACTION:
|
||||
// shouldn't get here, do nothing
|
||||
break;
|
||||
this->action_request_.value().message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF);
|
||||
return true;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_);
|
||||
break;
|
||||
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_.value().action);
|
||||
this->action_request_.reset();
|
||||
return false;
|
||||
}
|
||||
this->action_request_ = ActionRequest::NO_ACTION;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
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");
|
||||
return; // cancel the control, we cant do it without a poll answer.
|
||||
}
|
||||
if (this->hvac_settings_.valid) {
|
||||
ESP_LOGW(TAG, "Overriding old valid settings before they were applied!");
|
||||
if (this->current_hvac_settings_.valid) {
|
||||
ESP_LOGW(TAG, "New settings come faster then processed!");
|
||||
}
|
||||
{
|
||||
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())
|
||||
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())
|
||||
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())
|
||||
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())
|
||||
this->hvac_settings_.preset = call.get_preset();
|
||||
this->hvac_settings_.valid = true;
|
||||
this->next_hvac_settings_.preset = call.get_preset();
|
||||
this->next_hvac_settings_.valid = true;
|
||||
}
|
||||
this->first_control_attempt_ = true;
|
||||
}
|
||||
|
||||
void HaierClimateBase::HvacSettings::reset() {
|
||||
|
@ -330,19 +356,9 @@ void HaierClimateBase::HvacSettings::reset() {
|
|||
this->preset.reset();
|
||||
}
|
||||
|
||||
void HaierClimateBase::set_force_send_control_(bool status) {
|
||||
this->force_send_control_ = status;
|
||||
if (status) {
|
||||
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);
|
||||
}
|
||||
void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats,
|
||||
std::chrono::milliseconds interval) {
|
||||
this->haier_protocol_.send_message(command, use_crc, num_repeats, interval);
|
||||
this->last_request_timestamp_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace esphome {
|
|||
namespace haier {
|
||||
|
||||
enum class ActionRequest : uint8_t {
|
||||
NO_ACTION = 0,
|
||||
SEND_CUSTOM_COMMAND = 0,
|
||||
TURN_POWER_ON = 1,
|
||||
TURN_POWER_OFF = 2,
|
||||
TOGGLE_POWER = 3,
|
||||
|
@ -33,7 +33,6 @@ class HaierClimateBase : public esphome::Component,
|
|||
void control(const esphome::climate::ClimateCall &call) override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; }
|
||||
void set_fahrenheit(bool fahrenheit);
|
||||
void set_display_state(bool state);
|
||||
bool get_display_state() const;
|
||||
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_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes);
|
||||
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 read_array(uint8_t *data, size_t len) noexcept override {
|
||||
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; };
|
||||
void set_answer_timeout(uint32_t timeout);
|
||||
void set_send_wifi(bool send_wifi);
|
||||
void send_custom_command(const haier_protocol::HaierMessage &message);
|
||||
|
||||
protected:
|
||||
enum class ProtocolPhases {
|
||||
UNKNOWN = -1,
|
||||
// INITIALIZATION
|
||||
SENDING_INIT_1 = 0,
|
||||
WAITING_INIT_1_ANSWER = 1,
|
||||
SENDING_INIT_2 = 2,
|
||||
WAITING_INIT_2_ANSWER = 3,
|
||||
SENDING_FIRST_STATUS_REQUEST = 4,
|
||||
WAITING_FIRST_STATUS_ANSWER = 5,
|
||||
SENDING_ALARM_STATUS_REQUEST = 6,
|
||||
WAITING_ALARM_STATUS_ANSWER = 7,
|
||||
SENDING_INIT_2,
|
||||
SENDING_FIRST_STATUS_REQUEST,
|
||||
SENDING_ALARM_STATUS_REQUEST,
|
||||
// FUNCTIONAL STATE
|
||||
IDLE = 8,
|
||||
SENDING_STATUS_REQUEST = 10,
|
||||
WAITING_STATUS_ANSWER = 11,
|
||||
SENDING_UPDATE_SIGNAL_REQUEST = 12,
|
||||
WAITING_UPDATE_SIGNAL_ANSWER = 13,
|
||||
SENDING_SIGNAL_LEVEL = 14,
|
||||
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,
|
||||
IDLE,
|
||||
SENDING_STATUS_REQUEST,
|
||||
SENDING_UPDATE_SIGNAL_REQUEST,
|
||||
SENDING_SIGNAL_LEVEL,
|
||||
SENDING_CONTROL,
|
||||
SENDING_ACTION_COMMAND,
|
||||
NUM_PROTOCOL_PHASES
|
||||
};
|
||||
#if (HAIER_LOG_LEVEL > 4)
|
||||
const char *phase_to_string_(ProtocolPhases phase);
|
||||
#endif
|
||||
virtual void set_handlers() = 0;
|
||||
virtual void process_phase(std::chrono::steady_clock::time_point now) = 0;
|
||||
virtual haier_protocol::HaierMessage get_control_message() = 0;
|
||||
virtual bool is_message_invalid(uint8_t message_type) = 0;
|
||||
virtual void process_pending_action();
|
||||
virtual haier_protocol::HaierMessage get_power_message(bool state) = 0;
|
||||
virtual bool prepare_pending_action();
|
||||
virtual void process_protocol_reset();
|
||||
esphome::climate::ClimateTraits traits() override;
|
||||
// Answers handlers
|
||||
haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type,
|
||||
uint8_t answer_message_type, uint8_t expected_answer_message_type,
|
||||
// Answer handlers
|
||||
haier_protocol::HandlerError 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);
|
||||
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
|
||||
haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type);
|
||||
haier_protocol::HandlerError timeout_default_handler_(haier_protocol::FrameType request_type);
|
||||
// Helper functions
|
||||
void set_force_send_control_(bool status);
|
||||
void send_message_(const haier_protocol::HaierMessage &command, bool use_crc);
|
||||
void send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats = 0,
|
||||
std::chrono::milliseconds interval = std::chrono::milliseconds::zero());
|
||||
virtual void set_phase(ProtocolPhases phase);
|
||||
bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint,
|
||||
size_t timeout);
|
||||
void reset_phase_();
|
||||
void reset_to_idle_();
|
||||
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_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_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now);
|
||||
#ifdef USE_WIFI
|
||||
haier_protocol::HaierMessage get_wifi_signal_message_(uint8_t message_type);
|
||||
haier_protocol::HaierMessage get_wifi_signal_message_();
|
||||
#endif
|
||||
|
||||
struct HvacSettings {
|
||||
|
@ -122,28 +115,33 @@ class HaierClimateBase : public esphome::Component,
|
|||
esphome::optional<esphome::climate::ClimatePreset> preset;
|
||||
bool valid;
|
||||
HvacSettings() : valid(false){};
|
||||
HvacSettings(const HvacSettings &) = default;
|
||||
HvacSettings &operator=(const HvacSettings &) = default;
|
||||
void reset();
|
||||
};
|
||||
struct PendingAction {
|
||||
ActionRequest action;
|
||||
esphome::optional<haier_protocol::HaierMessage> message;
|
||||
};
|
||||
haier_protocol::ProtocolHandler haier_protocol_;
|
||||
ProtocolPhases protocol_phase_;
|
||||
ActionRequest action_request_;
|
||||
esphome::optional<PendingAction> action_request_;
|
||||
uint8_t fan_mode_speed_;
|
||||
uint8_t other_modes_fan_speed_;
|
||||
bool display_status_;
|
||||
bool health_mode_;
|
||||
bool force_send_control_;
|
||||
bool forced_publish_;
|
||||
bool forced_request_status_;
|
||||
bool first_control_attempt_;
|
||||
bool reset_protocol_request_;
|
||||
bool send_wifi_signal_;
|
||||
bool use_crc_;
|
||||
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_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 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
|
||||
};
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ namespace haier {
|
|||
static const char *const TAG = "haier.climate";
|
||||
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
|
||||
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) {
|
||||
switch (direction) {
|
||||
|
@ -48,14 +50,11 @@ hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDir
|
|||
}
|
||||
|
||||
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),
|
||||
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},
|
||||
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->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) {
|
||||
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_; }
|
||||
|
||||
void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) {
|
||||
this->horizontal_direction_ = direction;
|
||||
this->set_force_send_control_(true);
|
||||
this->force_send_control_ = true;
|
||||
}
|
||||
|
||||
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() {
|
||||
if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
|
||||
ESP_LOGI(TAG, "Sending self cleaning start request");
|
||||
this->action_request_ = ActionRequest::START_SELF_CLEAN;
|
||||
this->set_force_send_control_(true);
|
||||
this->action_request_ =
|
||||
PendingAction({ActionRequest::START_SELF_CLEAN, esphome::optional<haier_protocol::HaierMessage>()});
|
||||
}
|
||||
}
|
||||
|
||||
void HonClimate::start_steri_cleaning() {
|
||||
if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
|
||||
ESP_LOGI(TAG, "Sending steri cleaning start request");
|
||||
this->action_request_ = ActionRequest::START_STERI_CLEAN;
|
||||
this->set_force_send_control_(true);
|
||||
this->action_request_ =
|
||||
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) {
|
||||
// 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 "
|
||||
"protocol instead of hOn");
|
||||
this->set_phase(ProtocolPhases::SENDING_INIT_1);
|
||||
return haier_protocol::HandlerError::INVALID_ANSWER;
|
||||
}
|
||||
haier_protocol::HandlerError result = this->answer_preprocess_(
|
||||
request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type,
|
||||
(uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_INIT_1_ANSWER);
|
||||
haier_protocol::HandlerError result =
|
||||
this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_VERSION, message_type,
|
||||
haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::SENDING_INIT_1);
|
||||
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||
if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) {
|
||||
// Wrong structure
|
||||
this->set_phase(ProtocolPhases::SENDING_INIT_1);
|
||||
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
|
||||
}
|
||||
// All OK
|
||||
|
@ -134,54 +133,57 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint
|
|||
char tmp[9];
|
||||
tmp[8] = 0;
|
||||
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);
|
||||
this->hvac_software_version_ = std::string(tmp);
|
||||
this->hvac_hardware_info_.value().software_version_ = std::string(tmp);
|
||||
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);
|
||||
this->hvac_device_name_ = std::string(tmp);
|
||||
this->hvac_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_functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support
|
||||
this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support
|
||||
this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support
|
||||
this->hvac_hardware_info_available_ = true;
|
||||
this->hvac_hardware_info_.value().device_name_ = std::string(tmp);
|
||||
this->hvac_hardware_info_.value().functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support
|
||||
this->hvac_hardware_info_.value().functions_[1] =
|
||||
(answr->functions[1] & 0x02) != 0; // controller-device mode support
|
||||
this->hvac_hardware_info_.value().functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support
|
||||
this->hvac_hardware_info_.value().functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support
|
||||
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);
|
||||
return result;
|
||||
} else {
|
||||
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||
: ProtocolPhases::SENDING_INIT_1);
|
||||
this->reset_phase_();
|
||||
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) {
|
||||
haier_protocol::HandlerError result = this->answer_preprocess_(
|
||||
request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type,
|
||||
(uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_INIT_2_ANSWER);
|
||||
haier_protocol::HandlerError result =
|
||||
this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_ID, message_type,
|
||||
haier_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::SENDING_INIT_2);
|
||||
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||
this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||
return result;
|
||||
} else {
|
||||
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||
: ProtocolPhases::SENDING_INIT_1);
|
||||
this->reset_phase_();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type,
|
||||
const uint8_t *data, size_t data_size) {
|
||||
haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameType request_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) hon_protocol::FrameType::CONTROL, message_type,
|
||||
(uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
|
||||
this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type,
|
||||
haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
|
||||
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||
result = this->process_status_message_(data, data_size);
|
||||
if (result != haier_protocol::HandlerError::HANDLER_OK) {
|
||||
ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
|
||||
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||
: ProtocolPhases::SENDING_INIT_1);
|
||||
this->reset_phase_();
|
||||
this->action_request_.reset();
|
||||
this->force_send_control_ = false;
|
||||
} else {
|
||||
if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) {
|
||||
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,
|
||||
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");
|
||||
this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
|
||||
} else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) ||
|
||||
(this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) ||
|
||||
(this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_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);
|
||||
} 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_force_send_control_(false);
|
||||
if (this->hvac_settings_.valid)
|
||||
this->hvac_settings_.reset();
|
||||
this->force_send_control_ = false;
|
||||
if (this->current_hvac_settings_.valid)
|
||||
this->current_hvac_settings_.reset();
|
||||
} else {
|
||||
this->set_phase(ProtocolPhases::SENDING_CONTROL);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||
: ProtocolPhases::SENDING_INIT_1);
|
||||
this->action_request_.reset();
|
||||
this->force_send_control_ = false;
|
||||
this->reset_phase_();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type,
|
||||
uint8_t message_type,
|
||||
const uint8_t *data,
|
||||
haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(
|
||||
haier_protocol::FrameType request_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) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION,
|
||||
message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE,
|
||||
ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
|
||||
haier_protocol::HandlerError result = this->answer_preprocess_(
|
||||
request_type, haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, message_type,
|
||||
haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
|
||||
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||
this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL);
|
||||
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,
|
||||
uint8_t message_type,
|
||||
haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_protocol::FrameType request_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) hon_protocol::FrameType::REPORT_NETWORK_STATUS, message_type,
|
||||
(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) {
|
||||
if (request_type == haier_protocol::FrameType::GET_ALARM_STATUS) {
|
||||
if (message_type != haier_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) {
|
||||
// Unexpected answer to request
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
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
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
|
||||
|
@ -263,27 +268,27 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_
|
|||
void HonClimate::set_handlers() {
|
||||
// Set handlers
|
||||
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::placeholders::_3, std::placeholders::_4));
|
||||
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::placeholders::_3, std::placeholders::_4));
|
||||
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::placeholders::_4));
|
||||
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::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
|
||||
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::placeholders::_3, std::placeholders::_4));
|
||||
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::placeholders::_3, std::placeholders::_4));
|
||||
}
|
||||
|
@ -291,14 +296,18 @@ void HonClimate::set_handlers() {
|
|||
void HonClimate::dump_config() {
|
||||
HaierClimateBase::dump_config();
|
||||
ESP_LOGCONFIG(TAG, " Protocol version: hOn");
|
||||
if (this->hvac_hardware_info_available_) {
|
||||
ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_protocol_version_.c_str());
|
||||
ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_software_version_.c_str());
|
||||
ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_version_.c_str());
|
||||
ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_device_name_.c_str());
|
||||
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""),
|
||||
(this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""),
|
||||
(this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : ""));
|
||||
ESP_LOGCONFIG(TAG, " Control method: %d", (uint8_t) this->control_method_);
|
||||
if (this->hvac_hardware_info_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_hardware_info_.value().protocol_version_.c_str());
|
||||
ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_hardware_info_.value().software_version_.c_str());
|
||||
ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_info_.value().hardware_version_.c_str());
|
||||
ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_hardware_info_.value().device_name_.c_str());
|
||||
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s",
|
||||
(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());
|
||||
}
|
||||
}
|
||||
|
@ -307,7 +316,6 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
|
|||
switch (this->protocol_phase_) {
|
||||
case ProtocolPhases::SENDING_INIT_1:
|
||||
if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) {
|
||||
this->hvac_hardware_info_available_ = false;
|
||||
// Indicate device capabilities:
|
||||
// bit 0 - if 1 module support interactive 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
|
||||
uint8_t module_capabilities[2] = {0b00000000, 0b00000111};
|
||||
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->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER);
|
||||
}
|
||||
break;
|
||||
case ProtocolPhases::SENDING_INIT_2:
|
||||
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->set_phase(ProtocolPhases::WAITING_INIT_2_ANSWER);
|
||||
}
|
||||
break;
|
||||
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
|
||||
case ProtocolPhases::SENDING_STATUS_REQUEST:
|
||||
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||
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->last_status_request_ = now;
|
||||
this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1));
|
||||
}
|
||||
break;
|
||||
#ifdef USE_WIFI
|
||||
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
|
||||
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||
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->last_signal_request_ = now;
|
||||
this->set_phase(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
|
||||
}
|
||||
break;
|
||||
case ProtocolPhases::SENDING_SIGNAL_LEVEL:
|
||||
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->use_crc_);
|
||||
this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
|
||||
this->send_message_(this->get_wifi_signal_message_(), this->use_crc_);
|
||||
}
|
||||
break;
|
||||
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
|
||||
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
|
||||
break;
|
||||
#else
|
||||
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
|
||||
case ProtocolPhases::SENDING_SIGNAL_LEVEL:
|
||||
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
|
||||
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
break;
|
||||
#endif
|
||||
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
|
||||
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||
static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(
|
||||
(uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS);
|
||||
static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS);
|
||||
this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_);
|
||||
this->set_phase(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER);
|
||||
}
|
||||
break;
|
||||
case ProtocolPhases::SENDING_CONTROL:
|
||||
if (this->first_control_attempt_) {
|
||||
this->control_request_timestamp_ = now;
|
||||
this->first_control_attempt_ = false;
|
||||
if (this->control_messages_queue_.empty()) {
|
||||
switch (this->control_method_) {
|
||||
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!");
|
||||
this->set_force_send_control_(false);
|
||||
if (this->hvac_settings_.valid)
|
||||
this->hvac_settings_.reset();
|
||||
this->forced_request_status_ = true;
|
||||
this->forced_publish_ = true;
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
}
|
||||
if (this->control_messages_queue_.empty()) {
|
||||
ESP_LOGW(TAG, "Control message queue is empty!");
|
||||
this->reset_to_idle_();
|
||||
} else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
|
||||
haier_protocol::HaierMessage control_message = get_control_message();
|
||||
this->send_message_(control_message, this->use_crc_);
|
||||
ESP_LOGI(TAG, "Control packet sent");
|
||||
this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER);
|
||||
ESP_LOGI(TAG, "Sending control packet, queue size %d", this->control_messages_queue_.size());
|
||||
this->send_message_(this->control_messages_queue_.front(), this->use_crc_, CONTROL_MESSAGE_RETRIES,
|
||||
CONTROL_MESSAGE_RETRIES_INTERVAL);
|
||||
}
|
||||
break;
|
||||
case ProtocolPhases::SENDING_POWER_ON_COMMAND:
|
||||
case ProtocolPhases::SENDING_POWER_OFF_COMMAND:
|
||||
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||
uint8_t pwr_cmd_buf[2] = {0x00, 0x00};
|
||||
if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND)
|
||||
pwr_cmd_buf[1] = 0x01;
|
||||
haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL,
|
||||
((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1,
|
||||
pwr_cmd_buf, sizeof(pwr_cmd_buf));
|
||||
this->send_message_(power_cmd, this->use_crc_);
|
||||
this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND
|
||||
? ProtocolPhases::WAITING_POWER_ON_ANSWER
|
||||
: ProtocolPhases::WAITING_POWER_OFF_ANSWER);
|
||||
case ProtocolPhases::SENDING_ACTION_COMMAND:
|
||||
if (this->action_request_.has_value()) {
|
||||
if (this->action_request_.value().message.has_value()) {
|
||||
this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
|
||||
this->action_request_.value().message.reset();
|
||||
} else {
|
||||
// Message already sent, reseting request and return to idle
|
||||
this->action_request_.reset();
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!");
|
||||
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;
|
||||
case ProtocolPhases::IDLE: {
|
||||
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;
|
||||
default:
|
||||
// Shouldn't get here
|
||||
#if (HAIER_LOG_LEVEL > 4)
|
||||
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
|
||||
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);
|
||||
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() {
|
||||
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;
|
||||
bool has_hvac_settings = false;
|
||||
if (this->hvac_settings_.valid) {
|
||||
if (this->current_hvac_settings_.valid) {
|
||||
has_hvac_settings = true;
|
||||
HvacSettings climate_control;
|
||||
climate_control = this->hvac_settings_;
|
||||
HvacSettings &climate_control = this->current_hvac_settings_;
|
||||
if (climate_control.mode.has_value()) {
|
||||
switch (climate_control.mode.value()) {
|
||||
case CLIMATE_MODE_OFF:
|
||||
|
@ -535,7 +538,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
|
|||
}
|
||||
if (climate_control.target_temperature.has_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;
|
||||
}
|
||||
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
|
||||
out_data->display_status = this->display_status_ ? 1 : 0;
|
||||
out_data->health_mode = this->health_mode_ ? 1 : 0;
|
||||
switch (this->action_request_) {
|
||||
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,
|
||||
return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
|
||||
(uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS,
|
||||
control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
|
||||
}
|
||||
|
||||
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;
|
||||
hon_protocol::HaierStatus packet;
|
||||
if (size < sizeof(hon_protocol::HaierStatus))
|
||||
size = sizeof(hon_protocol::HaierStatus);
|
||||
memcpy(&packet, packet_buffer, size);
|
||||
struct {
|
||||
hon_protocol::HaierPacketControl control;
|
||||
hon_protocol::HaierPacketSensors sensors;
|
||||
} 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) {
|
||||
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))) {
|
||||
got_valid_outdoor_temp_ = true;
|
||||
if ((this->outdoor_sensor_ != nullptr) &&
|
||||
(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);
|
||||
if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_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
|
||||
if (this->mode == CLIMATE_MODE_OFF) {
|
||||
// AC just turned on from remote need to turn off display
|
||||
this->set_force_send_control_(true);
|
||||
this->force_send_control_ = true;
|
||||
} else {
|
||||
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);
|
||||
if (new_cleaning == CleaningState::NO_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;
|
||||
}
|
||||
|
@ -783,51 +765,257 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
|||
should_publish = should_publish || (old_swing_mode != this->swing_mode);
|
||||
}
|
||||
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
|
||||
if (this->forced_publish_ || should_publish) {
|
||||
#if (HAIER_LOG_LEVEL > 4)
|
||||
std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now();
|
||||
#endif
|
||||
if (should_publish) {
|
||||
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) {
|
||||
ESP_LOGI(TAG, "HVAC values changed");
|
||||
}
|
||||
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), 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__,
|
||||
"Fan speed Status = 0x%X", packet.control.fan_mode);
|
||||
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), 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__,
|
||||
"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);
|
||||
int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG;
|
||||
esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode);
|
||||
esp_log_printf_(log_level, TAG, __LINE__, "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_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode);
|
||||
esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point);
|
||||
return haier_protocol::HandlerError::HANDLER_OK;
|
||||
}
|
||||
|
||||
bool HonClimate::is_message_invalid(uint8_t message_type) {
|
||||
return message_type == (uint8_t) hon_protocol::FrameType::INVALID;
|
||||
void HonClimate::fill_control_messages_queue_() {
|
||||
static uint8_t one_buf[] = {0x00, 0x01};
|
||||
static uint8_t zero_buf[] = {0x00, 0x00};
|
||||
if (!this->current_hvac_settings_.valid && !this->force_send_control_)
|
||||
return;
|
||||
this->clear_control_messages_queue_();
|
||||
HvacSettings climate_control;
|
||||
climate_control = this->current_hvac_settings_;
|
||||
// Beeper command
|
||||
{
|
||||
this->control_messages_queue_.push(
|
||||
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
|
||||
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
|
||||
(uint8_t) hon_protocol::DataParameters::BEEPER_STATUS,
|
||||
this->beeper_status_ ? zero_buf : one_buf, 2));
|
||||
}
|
||||
|
||||
void HonClimate::process_pending_action() {
|
||||
switch (this->action_request_) {
|
||||
case ActionRequest::START_SELF_CLEAN:
|
||||
case ActionRequest::START_STERI_CLEAN:
|
||||
// Will reset action with control message sending
|
||||
this->set_phase(ProtocolPhases::SENDING_CONTROL);
|
||||
// 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;
|
||||
default:
|
||||
HaierClimateBase::process_pending_action();
|
||||
ESP_LOGE("Control", "Unsupported climate mode");
|
||||
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 esphome
|
||||
|
|
|
@ -30,6 +30,8 @@ enum class CleaningState : uint8_t {
|
|||
STERI_CLEAN = 2,
|
||||
};
|
||||
|
||||
enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER };
|
||||
|
||||
class HonClimate : public HaierClimateBase {
|
||||
public:
|
||||
HonClimate();
|
||||
|
@ -48,44 +50,57 @@ class HonClimate : public HaierClimateBase {
|
|||
CleaningState get_cleaning_status() const;
|
||||
void start_self_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:
|
||||
void set_handlers() override;
|
||||
void process_phase(std::chrono::steady_clock::time_point now) override;
|
||||
haier_protocol::HaierMessage get_control_message() override;
|
||||
bool is_message_invalid(uint8_t message_type) override;
|
||||
void process_pending_action() override;
|
||||
haier_protocol::HaierMessage get_power_message(bool state) override;
|
||||
bool prepare_pending_action() override;
|
||||
void process_protocol_reset() override;
|
||||
|
||||
// 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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||
const uint8_t *data, size_t data_size);
|
||||
haier_protocol::HandlerError get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||
haier_protocol::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type,
|
||||
haier_protocol::FrameType message_type,
|
||||
const uint8_t *data, size_t data_size);
|
||||
// Helper functions
|
||||
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_;
|
||||
CleaningState cleaning_status_;
|
||||
bool got_valid_outdoor_temp_;
|
||||
AirflowVerticalDirection vertical_direction_;
|
||||
AirflowHorizontalDirection horizontal_direction_;
|
||||
bool hvac_hardware_info_available_;
|
||||
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_;
|
||||
esphome::optional<HardwareInfo> hvac_hardware_info_;
|
||||
uint8_t active_alarms_[8];
|
||||
int extra_control_packet_bytes_;
|
||||
HonControlMethod control_method_;
|
||||
esphome::sensor::Sensor *outdoor_sensor_;
|
||||
std::queue<haier_protocol::HaierMessage> control_messages_queue_;
|
||||
};
|
||||
|
||||
} // namespace haier
|
||||
|
|
|
@ -35,6 +35,20 @@ enum class ConditioningMode : uint8_t {
|
|||
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 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)
|
||||
};
|
||||
|
||||
struct HaierStatus {
|
||||
uint16_t subcommand;
|
||||
HaierPacketControl control;
|
||||
HaierPacketSensors sensors;
|
||||
};
|
||||
constexpr size_t HAIER_STATUS_FRAME_SIZE = 2 + sizeof(HaierPacketControl) + sizeof(HaierPacketSensors);
|
||||
|
||||
struct DeviceVersionAnswer {
|
||||
char protocol_version[8];
|
||||
|
@ -140,76 +150,6 @@ struct DeviceVersionAnswer {
|
|||
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 {
|
||||
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)
|
||||
|
|
|
@ -12,21 +12,28 @@ namespace haier {
|
|||
|
||||
static const char *const TAG = "haier.climate";
|
||||
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()
|
||||
: last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]), timeouts_counter_(0) {}
|
||||
Smartair2Climate::Smartair2Climate() {
|
||||
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) {
|
||||
haier_protocol::HandlerError result =
|
||||
this->answer_preprocess_(request_type, (uint8_t) smartair2_protocol::FrameType::CONTROL, message_type,
|
||||
(uint8_t) smartair2_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
|
||||
this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type,
|
||||
haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
|
||||
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||
result = this->process_status_message_(data, data_size);
|
||||
if (result != haier_protocol::HandlerError::HANDLER_OK) {
|
||||
ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
|
||||
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||
: ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||
this->reset_phase_();
|
||||
this->action_request_.reset();
|
||||
this->force_send_control_ = false;
|
||||
} else {
|
||||
if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) {
|
||||
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,
|
||||
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");
|
||||
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);
|
||||
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) {
|
||||
break;
|
||||
case ProtocolPhases::SENDING_CONTROL:
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
this->set_force_send_control_(false);
|
||||
if (this->hvac_settings_.valid)
|
||||
this->hvac_settings_.reset();
|
||||
this->force_send_control_ = false;
|
||||
if (this->current_hvac_settings_.valid)
|
||||
this->current_hvac_settings_.reset();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||
: ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||
this->action_request_.reset();
|
||||
this->force_send_control_ = false;
|
||||
this->reset_phase_();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_(uint8_t request_type,
|
||||
uint8_t message_type,
|
||||
const uint8_t *data,
|
||||
haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_(
|
||||
haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data,
|
||||
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;
|
||||
if (ProtocolPhases::WAITING_INIT_1_ANSWER != this->protocol_phase_)
|
||||
if (ProtocolPhases::SENDING_INIT_1 != this->protocol_phase_)
|
||||
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
|
||||
// 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)) {
|
||||
ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the hOn protocol "
|
||||
"instead of smartAir2");
|
||||
|
@ -72,58 +88,35 @@ haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler
|
|||
return haier_protocol::HandlerError::HANDLER_OK;
|
||||
}
|
||||
|
||||
haier_protocol::HandlerError Smartair2Climate::report_network_status_answer_handler_(uint8_t request_type,
|
||||
uint8_t 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) {
|
||||
haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cycle_for_init_(
|
||||
haier_protocol::FrameType message_type) {
|
||||
if (this->protocol_phase_ >= ProtocolPhases::IDLE)
|
||||
return HaierClimateBase::timeout_default_handler_(message_type);
|
||||
this->timeouts_counter_++;
|
||||
ESP_LOGI(TAG, "Answer timeout for command %02X, phase %d, timeout counter %d", message_type,
|
||||
(int) this->protocol_phase_, this->timeouts_counter_);
|
||||
if (this->timeouts_counter_ >= 3) {
|
||||
ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type,
|
||||
phase_to_string_(this->protocol_phase_));
|
||||
ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1);
|
||||
if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST)
|
||||
new_phase = ProtocolPhases::SENDING_INIT_1;
|
||||
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;
|
||||
}
|
||||
|
||||
void Smartair2Climate::set_handlers() {
|
||||
// Set handlers
|
||||
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::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
|
||||
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::placeholders::_3, std::placeholders::_4));
|
||||
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::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
|
||||
this->haier_protocol_.set_timeout_handler(
|
||||
(uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_ID),
|
||||
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));
|
||||
this->haier_protocol_.set_default_timeout_handler(
|
||||
std::bind(&Smartair2Climate::messages_timeout_handler_with_cycle_for_init_, this, std::placeholders::_1));
|
||||
}
|
||||
|
||||
void Smartair2Climate::dump_config() {
|
||||
|
@ -134,9 +127,7 @@ void Smartair2Climate::dump_config() {
|
|||
void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) {
|
||||
switch (this->protocol_phase_) {
|
||||
case ProtocolPhases::SENDING_INIT_1:
|
||||
if (this->can_send_message() &&
|
||||
(((this->timeouts_counter_ == 0) && (this->is_protocol_initialisation_interval_exceeded_(now))) ||
|
||||
((this->timeouts_counter_ > 0) && (this->is_message_interval_exceeded_(now))))) {
|
||||
if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) {
|
||||
// Indicate device capabilities:
|
||||
// bit 0 - if 1 module support interactive 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
|
||||
uint8_t module_capabilities[2] = {0b00000000, 0b00000111};
|
||||
static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST(
|
||||
(uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities,
|
||||
sizeof(module_capabilities));
|
||||
this->send_message_(DEVICE_VERSION_REQUEST, false);
|
||||
this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER);
|
||||
haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities));
|
||||
this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL);
|
||||
}
|
||||
break;
|
||||
case ProtocolPhases::SENDING_INIT_2:
|
||||
case ProtocolPhases::WAITING_INIT_2_ANSWER:
|
||||
this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||
break;
|
||||
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
|
||||
case ProtocolPhases::SENDING_STATUS_REQUEST:
|
||||
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||
static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL,
|
||||
0x4D01);
|
||||
this->send_message_(STATUS_REQUEST, false);
|
||||
static const haier_protocol::HaierMessage STATUS_REQUEST(haier_protocol::FrameType::CONTROL, 0x4D01);
|
||||
if (this->protocol_phase_ == ProtocolPhases::SENDING_FIRST_STATUS_REQUEST) {
|
||||
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->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1));
|
||||
}
|
||||
break;
|
||||
#ifdef USE_WIFI
|
||||
case ProtocolPhases::SENDING_SIGNAL_LEVEL:
|
||||
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||
this->send_message_(
|
||||
this->get_wifi_signal_message_((uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), false);
|
||||
this->send_message_(this->get_wifi_signal_message_(), this->use_crc_);
|
||||
this->last_signal_request_ = now;
|
||||
this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
|
||||
}
|
||||
break;
|
||||
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
|
||||
break;
|
||||
#else
|
||||
case ProtocolPhases::SENDING_SIGNAL_LEVEL:
|
||||
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
break;
|
||||
#endif
|
||||
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
|
||||
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
|
||||
this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL);
|
||||
break;
|
||||
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
|
||||
case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER:
|
||||
this->set_phase(ProtocolPhases::SENDING_INIT_1);
|
||||
break;
|
||||
case ProtocolPhases::SENDING_CONTROL:
|
||||
if (this->first_control_attempt_) {
|
||||
this->control_request_timestamp_ = now;
|
||||
this->first_control_attempt_ = false;
|
||||
if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
|
||||
ESP_LOGI(TAG, "Sending control packet");
|
||||
this->send_message_(get_control_message(), this->use_crc_, CONTROL_MESSAGE_RETRIES,
|
||||
CONTROL_MESSAGE_RETRIES_INTERVAL);
|
||||
}
|
||||
if (this->is_control_message_timeout_exceeded_(now)) {
|
||||
ESP_LOGW(TAG, "Sending control packet timeout!");
|
||||
this->set_force_send_control_(false);
|
||||
if (this->hvac_settings_.valid)
|
||||
this->hvac_settings_.reset();
|
||||
this->forced_request_status_ = true;
|
||||
this->forced_publish_ = true;
|
||||
break;
|
||||
case ProtocolPhases::SENDING_ACTION_COMMAND:
|
||||
if (this->action_request_.has_value()) {
|
||||
if (this->action_request_.value().message.has_value()) {
|
||||
this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
|
||||
this->action_request_.value().message.reset();
|
||||
} else {
|
||||
// Message already sent, reseting request and return to idle
|
||||
this->action_request_.reset();
|
||||
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;
|
||||
case ProtocolPhases::SENDING_POWER_ON_COMMAND:
|
||||
case ProtocolPhases::SENDING_POWER_OFF_COMMAND:
|
||||
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);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!");
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
}
|
||||
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: {
|
||||
if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
|
||||
this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST);
|
||||
|
@ -245,55 +209,55 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
|
|||
} break;
|
||||
default:
|
||||
// Shouldn't get here
|
||||
#if (HAIER_LOG_LEVEL > 4)
|
||||
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
|
||||
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);
|
||||
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() {
|
||||
uint8_t control_out_buffer[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;
|
||||
out_data->cntrl = 0;
|
||||
if (this->hvac_settings_.valid) {
|
||||
HvacSettings climate_control;
|
||||
climate_control = this->hvac_settings_;
|
||||
if (this->current_hvac_settings_.valid) {
|
||||
HvacSettings &climate_control = this->current_hvac_settings_;
|
||||
if (climate_control.mode.has_value()) {
|
||||
switch (climate_control.mode.value()) {
|
||||
case CLIMATE_MODE_OFF:
|
||||
out_data->ac_power = 0;
|
||||
break;
|
||||
|
||||
case CLIMATE_MODE_HEAT_COOL:
|
||||
out_data->ac_power = 1;
|
||||
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO;
|
||||
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||
break;
|
||||
|
||||
case CLIMATE_MODE_HEAT:
|
||||
out_data->ac_power = 1;
|
||||
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT;
|
||||
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||
break;
|
||||
|
||||
case CLIMATE_MODE_DRY:
|
||||
out_data->ac_power = 1;
|
||||
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY;
|
||||
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||
break;
|
||||
|
||||
case CLIMATE_MODE_FAN_ONLY:
|
||||
out_data->ac_power = 1;
|
||||
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
|
||||
break;
|
||||
|
||||
case CLIMATE_MODE_COOL:
|
||||
out_data->ac_power = 1;
|
||||
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL;
|
||||
|
@ -327,32 +291,49 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
|
|||
}
|
||||
// Set swing mode
|
||||
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()) {
|
||||
case CLIMATE_SWING_OFF:
|
||||
out_data->use_swing_bits = 0;
|
||||
out_data->swing_both = 0;
|
||||
out_data->swing_mode = 0;
|
||||
break;
|
||||
case CLIMATE_SWING_VERTICAL:
|
||||
out_data->swing_both = 0;
|
||||
out_data->swing_mode = 0;
|
||||
out_data->vertical_swing = 1;
|
||||
out_data->horizontal_swing = 0;
|
||||
break;
|
||||
case CLIMATE_SWING_HORIZONTAL:
|
||||
out_data->swing_both = 0;
|
||||
out_data->swing_mode = 0;
|
||||
out_data->vertical_swing = 0;
|
||||
out_data->horizontal_swing = 1;
|
||||
break;
|
||||
case CLIMATE_SWING_BOTH:
|
||||
out_data->swing_both = 1;
|
||||
out_data->swing_mode = 1;
|
||||
out_data->use_swing_bits = 0;
|
||||
out_data->vertical_swing = 0;
|
||||
out_data->horizontal_swing = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (climate_control.target_temperature.has_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;
|
||||
}
|
||||
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->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));
|
||||
}
|
||||
|
||||
|
@ -459,13 +440,19 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
|
|||
// Do something only if display status changed
|
||||
if (this->mode == CLIMATE_MODE_OFF) {
|
||||
// AC just turned on from remote need to turn off display
|
||||
this->set_force_send_control_(true);
|
||||
this->force_send_control_ = true;
|
||||
} else {
|
||||
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
|
||||
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);
|
||||
}
|
||||
{
|
||||
// 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
|
||||
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) {
|
||||
this->swing_mode = CLIMATE_SWING_VERTICAL;
|
||||
} else if (packet.control.horizontal_swing != 0) {
|
||||
|
@ -513,50 +510,27 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
|
|||
} else {
|
||||
swing_mode = CLIMATE_SWING_BOTH;
|
||||
}
|
||||
}
|
||||
should_publish = should_publish || (old_swing_mode != this->swing_mode);
|
||||
}
|
||||
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
|
||||
if (this->forced_publish_ || should_publish) {
|
||||
#if (HAIER_LOG_LEVEL > 4)
|
||||
std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now();
|
||||
#endif
|
||||
if (should_publish) {
|
||||
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) {
|
||||
ESP_LOGI(TAG, "HVAC values changed");
|
||||
}
|
||||
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), 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__,
|
||||
"Fan speed Status = 0x%X", packet.control.fan_mode);
|
||||
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), 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__,
|
||||
"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);
|
||||
int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG;
|
||||
esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode);
|
||||
esp_log_printf_(log_level, TAG, __LINE__, "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_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing);
|
||||
esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point);
|
||||
return haier_protocol::HandlerError::HANDLER_OK;
|
||||
}
|
||||
|
||||
bool Smartair2Climate::is_message_invalid(uint8_t message_type) {
|
||||
return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID;
|
||||
}
|
||||
|
||||
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);
|
||||
void Smartair2Climate::set_alternative_swing_control(bool swing_control) {
|
||||
this->use_alternative_swing_control_ = swing_control;
|
||||
}
|
||||
|
||||
} // namespace haier
|
||||
|
|
|
@ -13,27 +13,27 @@ class Smartair2Climate : public HaierClimateBase {
|
|||
Smartair2Climate &operator=(const Smartair2Climate &) = delete;
|
||||
~Smartair2Climate();
|
||||
void dump_config() override;
|
||||
void set_alternative_swing_control(bool swing_control);
|
||||
|
||||
protected:
|
||||
void set_handlers() 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;
|
||||
bool is_message_invalid(uint8_t message_type) override;
|
||||
void set_phase(HaierClimateBase::ProtocolPhases phase) override;
|
||||
// Answer and timeout handlers
|
||||
haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data,
|
||||
// Answer handlers
|
||||
haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type,
|
||||
haier_protocol::FrameType message_type, const uint8_t *data,
|
||||
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);
|
||||
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);
|
||||
haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||
const uint8_t *data, size_t data_size);
|
||||
haier_protocol::HandlerError initial_messages_timeout_handler_(uint8_t message_type);
|
||||
haier_protocol::HandlerError messages_timeout_handler_with_cycle_for_init_(haier_protocol::FrameType message_type);
|
||||
// Helper functions
|
||||
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
|
||||
std::unique_ptr<uint8_t[]> last_status_message_;
|
||||
unsigned int timeouts_counter_;
|
||||
bool use_alternative_swing_control_;
|
||||
};
|
||||
|
||||
} // namespace haier
|
||||
|
|
|
@ -41,8 +41,9 @@ struct HaierPacketControl {
|
|||
// 24
|
||||
uint8_t : 8;
|
||||
// 25
|
||||
uint8_t swing_both; // If 1 - swing both direction, if 0 - horizontal_swing and vertical_swing define
|
||||
// vertical/horizontal/off
|
||||
uint8_t swing_mode; // In normal mode: If 1 - swing both direction, if 0 - horizontal_swing and
|
||||
// vertical_swing define vertical/horizontal/off
|
||||
// In alternative mode: 0 - off, 01 - vertical, 02 - horizontal, 03 - both
|
||||
// 26
|
||||
uint8_t : 3;
|
||||
uint8_t use_fahrenheit : 1;
|
||||
|
@ -82,19 +83,6 @@ struct HaierStatus {
|
|||
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 haier
|
||||
} // namespace esphome
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include "esphome/components/remote_transmitter/remote_transmitter.h"
|
||||
#include <IRSender.h> // arduino-heatpump library
|
||||
|
||||
namespace esphome {
|
||||
|
@ -11,14 +10,13 @@ namespace heatpumpir {
|
|||
|
||||
class IRSenderESPHome : public IRSender {
|
||||
public:
|
||||
IRSenderESPHome(remote_transmitter::RemoteTransmitterComponent *transmitter)
|
||||
: IRSender(0), transmit_(transmitter->transmit()){};
|
||||
IRSenderESPHome(remote_base::RemoteTransmitterBase *transmitter) : IRSender(0), transmit_(transmitter->transmit()){};
|
||||
void setFrequency(int frequency) override; // NOLINT(readability-identifier-naming)
|
||||
void space(int space_length) override;
|
||||
void mark(int mark_length) override;
|
||||
|
||||
protected:
|
||||
remote_transmitter::RemoteTransmitterComponent::TransmitCall transmit_;
|
||||
remote_base::RemoteTransmitterBase::TransmitCall transmit_;
|
||||
};
|
||||
|
||||
} // namespace heatpumpir
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
from pathlib import Path
|
||||
import re
|
||||
import requests
|
||||
from magic import Magic
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from esphome import core
|
||||
from esphome.components import font
|
||||
from esphome import external_files
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
from esphome.const import (
|
||||
__version__,
|
||||
CONF_DITHER,
|
||||
CONF_FILE,
|
||||
CONF_ICON,
|
||||
|
@ -19,6 +27,7 @@ from esphome.const import (
|
|||
CONF_RESIZE,
|
||||
CONF_SOURCE,
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
|
||||
|
@ -43,34 +52,74 @@ IMAGE_TYPE = {
|
|||
CONF_USE_TRANSPARENCY = "use_transparency"
|
||||
|
||||
# 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_MDI = "mdi"
|
||||
SOURCE_WEB = "web"
|
||||
|
||||
|
||||
Image_ = image_ns.class_("Image")
|
||||
|
||||
|
||||
def _compute_local_icon_path(value) -> Path:
|
||||
base_dir = Path(CORE.data_dir) / DOMAIN / "mdi"
|
||||
def _compute_local_icon_path(value: dict) -> Path:
|
||||
base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi"
|
||||
return base_dir / f"{value[CONF_ICON]}.svg"
|
||||
|
||||
|
||||
def download_mdi(value):
|
||||
mdi_id = value[CONF_ICON]
|
||||
path = _compute_local_icon_path(value)
|
||||
if path.is_file():
|
||||
return value
|
||||
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg"
|
||||
_LOGGER.debug("Downloading %s MDI image from %s", mdi_id, url)
|
||||
def _compute_local_image_path(value: dict) -> Path:
|
||||
url = value[CONF_URL]
|
||||
h = hashlib.new("sha256")
|
||||
h.update(url.encode())
|
||||
key = h.hexdigest()[:8]
|
||||
base_dir = external_files.compute_local_file_dir(DOMAIN)
|
||||
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:
|
||||
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()
|
||||
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.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
|
||||
|
||||
|
||||
|
@ -139,6 +188,13 @@ def validate_file_shorthand(value):
|
|||
CONF_ICON: icon,
|
||||
}
|
||||
)
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_WEB,
|
||||
CONF_URL: value,
|
||||
}
|
||||
)
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_LOCAL,
|
||||
|
@ -160,10 +216,18 @@ MDI_SCHEMA = cv.All(
|
|||
download_mdi,
|
||||
)
|
||||
|
||||
WEB_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_URL): cv.string,
|
||||
},
|
||||
download_image,
|
||||
)
|
||||
|
||||
TYPED_FILE_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
SOURCE_LOCAL: LOCAL_SCHEMA,
|
||||
SOURCE_MDI: MDI_SCHEMA,
|
||||
SOURCE_WEB: WEB_SCHEMA,
|
||||
},
|
||||
key=CONF_SOURCE,
|
||||
)
|
||||
|
@ -201,9 +265,7 @@ IMAGE_SCHEMA = cv.Schema(
|
|||
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA)
|
||||
|
||||
|
||||
def load_svg_image(file: str, resize: tuple[int, int]):
|
||||
from PIL import Image
|
||||
|
||||
def load_svg_image(file: bytes, resize: tuple[int, int]):
|
||||
# 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
|
||||
# installed for no reason.
|
||||
|
@ -212,19 +274,17 @@ def load_svg_image(file: str, resize: tuple[int, int]):
|
|||
if resize:
|
||||
req_width, req_height = resize
|
||||
svg_image = svg2png(
|
||||
url=file,
|
||||
file,
|
||||
output_width=req_width,
|
||||
output_height=req_height,
|
||||
)
|
||||
else:
|
||||
svg_image = svg2png(url=file)
|
||||
svg_image = svg2png(file)
|
||||
|
||||
return Image.open(io.BytesIO(svg_image))
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
from PIL import Image
|
||||
|
||||
conf_file = config[CONF_FILE]
|
||||
|
||||
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
|
||||
|
@ -233,17 +293,26 @@ async def to_code(config):
|
|||
elif conf_file[CONF_SOURCE] == SOURCE_MDI:
|
||||
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:
|
||||
resize = config.get(CONF_RESIZE)
|
||||
if path.lower().endswith(".svg"):
|
||||
image = load_svg_image(path, resize)
|
||||
else:
|
||||
image = Image.open(path)
|
||||
if resize:
|
||||
image.thumbnail(resize)
|
||||
with open(path, "rb") as f:
|
||||
file_contents = f.read()
|
||||
except Exception as 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
|
||||
|
||||
if CONF_RESIZE not in config and (width > 500 or height > 500):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
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"]
|
||||
interval_ns = cg.esphome_ns.namespace("interval")
|
||||
|
@ -13,6 +13,9 @@ CONFIG_SCHEMA = automation.validate_automation(
|
|||
cv.Schema(
|
||||
{
|
||||
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,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
@ -26,3 +29,4 @@ async def to_code(config):
|
|||
await automation.build_automation(var, [], conf)
|
||||
|
||||
cg.add(var.set_update_interval(conf[CONF_INTERVAL]))
|
||||
cg.add(var.set_startup_delay(conf[CONF_STARTUP_DELAY]))
|
||||
|
|
|
@ -8,8 +8,26 @@ namespace interval {
|
|||
|
||||
class IntervalTrigger : public Trigger<>, public PollingComponent {
|
||||
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; }
|
||||
|
||||
protected:
|
||||
uint32_t startup_delay_{0};
|
||||
bool started_{false};
|
||||
};
|
||||
|
||||
} // namespace interval
|
||||
|
|
|
@ -147,7 +147,7 @@ void MQTTClientComponent::dump_config() {
|
|||
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_() {
|
||||
for (auto &subscription : this->subscriptions_) {
|
||||
|
|
|
@ -40,6 +40,8 @@ const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
|
|||
void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
if (this->lock_->traits.get_assumed_state())
|
||||
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(); }
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include "my9231.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace my9231 {
|
||||
|
@ -51,7 +52,11 @@ void MY9231OutputComponent::setup() {
|
|||
MY9231_CMD_SCATTER_APDM | MY9231_CMD_FREQUENCY_DIVIDE_1 | MY9231_CMD_REACTION_FAST | MY9231_CMD_ONE_SHOT_DISABLE;
|
||||
ESP_LOGV(TAG, " Command: 0x%02X", command);
|
||||
|
||||
{
|
||||
InterruptLock lock;
|
||||
this->send_dcki_pulses_(32 * this->num_chips_);
|
||||
this->init_chips_(command);
|
||||
}
|
||||
ESP_LOGV(TAG, " Chips initialized.");
|
||||
}
|
||||
void MY9231OutputComponent::dump_config() {
|
||||
|
@ -66,11 +71,14 @@ void MY9231OutputComponent::loop() {
|
|||
if (!this->update_)
|
||||
return;
|
||||
|
||||
{
|
||||
InterruptLock lock;
|
||||
for (auto pwm_amount : this->pwm_amounts_) {
|
||||
this->write_word_(pwm_amount, this->bit_depth_);
|
||||
}
|
||||
// Send 8 DI pulses. After 8 falling edges, the duty data are store.
|
||||
this->send_di_pulses_(8);
|
||||
}
|
||||
this->update_ = false;
|
||||
}
|
||||
void MY9231OutputComponent::set_channel_value_(uint8_t channel, uint16_t value) {
|
||||
|
@ -92,6 +100,7 @@ void MY9231OutputComponent::init_chips_(uint8_t command) {
|
|||
// Send 16 DI pulse. After 14 falling edges, the command data are
|
||||
// stored and after 16 falling edges the duty mode is activated.
|
||||
this->send_di_pulses_(16);
|
||||
delayMicroseconds(12);
|
||||
}
|
||||
void MY9231OutputComponent::write_word_(uint16_t value, uint8_t bits) {
|
||||
for (uint8_t i = bits; i > 0; i--) {
|
||||
|
@ -106,6 +115,13 @@ void MY9231OutputComponent::send_di_pulses_(uint8_t count) {
|
|||
this->pin_di_->digital_write(false);
|
||||
}
|
||||
}
|
||||
void MY9231OutputComponent::send_dcki_pulses_(uint8_t count) {
|
||||
delayMicroseconds(12);
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
this->pin_dcki_->digital_write(true);
|
||||
this->pin_dcki_->digital_write(false);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace my9231
|
||||
} // namespace esphome
|
||||
|
|
|
@ -49,6 +49,7 @@ class MY9231OutputComponent : public Component {
|
|||
void init_chips_(uint8_t command);
|
||||
void write_word_(uint16_t value, uint8_t bits);
|
||||
void send_di_pulses_(uint8_t count);
|
||||
void send_dcki_pulses_(uint8_t count);
|
||||
|
||||
GPIOPin *pin_di_;
|
||||
GPIOPin *pin_dcki_;
|
||||
|
|
|
@ -29,6 +29,14 @@ bool is_connected() {
|
|||
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() {
|
||||
#ifdef USE_ETHERNET
|
||||
if (ethernet::global_eth_component != nullptr)
|
||||
|
|
|
@ -8,6 +8,8 @@ namespace network {
|
|||
|
||||
/// Return whether the node is connected to the network (through wifi, eth, ...)
|
||||
bool is_connected();
|
||||
/// Return whether the network is disabled (only wifi for now)
|
||||
bool is_disabled();
|
||||
/// Get the active network hostname
|
||||
std::string get_use_address();
|
||||
IPAddress get_ip_address();
|
||||
|
|
|
@ -52,8 +52,9 @@ RemoteReceiverTrigger = ns.class_(
|
|||
"RemoteReceiverTrigger", automation.Trigger, RemoteReceiverListener
|
||||
)
|
||||
RemoteTransmitterDumper = ns.class_("RemoteTransmitterDumper")
|
||||
RemoteTransmittable = ns.class_("RemoteTransmittable")
|
||||
RemoteTransmitterActionBase = ns.class_(
|
||||
"RemoteTransmitterActionBase", automation.Action
|
||||
"RemoteTransmitterActionBase", RemoteTransmittable, automation.Action
|
||||
)
|
||||
RemoteReceiverBase = ns.class_("RemoteReceiverBase")
|
||||
RemoteTransmitterBase = ns.class_("RemoteTransmitterBase")
|
||||
|
@ -68,11 +69,30 @@ def templatize(value):
|
|||
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):
|
||||
receiver = await cg.get_variable(config[CONF_RECEIVER_ID])
|
||||
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):
|
||||
return BINARY_SENSOR_REGISTRY.register(name, type, schema)
|
||||
|
||||
|
@ -129,10 +149,9 @@ def validate_repeat(value):
|
|||
|
||||
BASE_REMOTE_TRANSMITTER_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase),
|
||||
cv.Optional(CONF_REPEAT): validate_repeat,
|
||||
}
|
||||
)
|
||||
).extend(REMOTE_TRANSMITTABLE_SCHEMA)
|
||||
|
||||
|
||||
def register_action(name, type_, schema):
|
||||
|
@ -143,9 +162,8 @@ def register_action(name, type_, schema):
|
|||
|
||||
def decorator(func):
|
||||
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)
|
||||
cg.add(var.set_parent(transmitter))
|
||||
await register_transmittable(var, config)
|
||||
if CONF_REPEAT in config:
|
||||
conf = config[CONF_REPEAT]
|
||||
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)
|
||||
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)
|
||||
|
|
|
@ -67,20 +67,7 @@ class MideaProtocol : public RemoteProtocol<MideaData> {
|
|||
void dump(const MideaData &data) override;
|
||||
};
|
||||
|
||||
class MideaBinarySensor : public RemoteReceiverBinarySensorBase {
|
||||
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>;
|
||||
DECLARE_REMOTE_PROTOCOL(Midea)
|
||||
|
||||
template<typename... Ts> class MideaAction : public RemoteTransmitterActionBase<Ts...> {
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, code)
|
||||
|
|
|
@ -15,6 +15,8 @@ struct RCSwitchData {
|
|||
|
||||
class RCSwitchBase {
|
||||
public:
|
||||
using ProtocolData = RCSwitchData;
|
||||
|
||||
RCSwitchBase() = default;
|
||||
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);
|
||||
|
@ -213,7 +215,7 @@ class RCSwitchDumper : public RemoteReceiverDumperBase {
|
|||
bool dump(RemoteReceiveData src) override;
|
||||
};
|
||||
|
||||
using RCSwitchTrigger = RemoteReceiverTrigger<RCSwitchBase, RCSwitchData>;
|
||||
using RCSwitchTrigger = RemoteReceiverTrigger<RCSwitchBase>;
|
||||
|
||||
} // namespace remote_base
|
||||
} // namespace esphome
|
||||
|
|
|
@ -127,6 +127,14 @@ class RemoteTransmitterBase : public RemoteComponentBase {
|
|||
this->temp_.reset();
|
||||
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:
|
||||
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 {
|
||||
public:
|
||||
virtual void encode(RemoteTransmitData *dst, const T &data) = 0;
|
||||
virtual optional<T> decode(RemoteReceiveData src) = 0;
|
||||
virtual void dump(const T &data) = 0;
|
||||
using ProtocolData = T;
|
||||
virtual void encode(RemoteTransmitData *dst, const ProtocolData &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:
|
||||
RemoteReceiverBinarySensor() : RemoteReceiverBinarySensorBase() {}
|
||||
|
||||
|
@ -201,13 +210,14 @@ template<typename T, typename D> class RemoteReceiverBinarySensor : public Remot
|
|||
}
|
||||
|
||||
public:
|
||||
void set_data(D data) { data_ = data; }
|
||||
void set_data(typename T::ProtocolData data) { data_ = data; }
|
||||
|
||||
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:
|
||||
bool on_receive(RemoteReceiveData src) override {
|
||||
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:
|
||||
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);
|
||||
TEMPLATABLE_VALUE(uint32_t, send_wait);
|
||||
protected:
|
||||
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 {
|
||||
auto call = this->parent_->transmit();
|
||||
auto call = this->transmitter_->transmit();
|
||||
this->encode(call.get_data(), x...);
|
||||
call.set_send_times(this->send_times_.value_or(x..., 1));
|
||||
call.set_send_wait(this->send_wait_.value_or(x..., 0));
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
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:
|
||||
bool dump(RemoteReceiveData src) override {
|
||||
auto proto = T();
|
||||
|
@ -254,9 +272,9 @@ template<typename T, typename D> class RemoteReceiverDumper : public RemoteRecei
|
|||
};
|
||||
|
||||
#define DECLARE_REMOTE_PROTOCOL_(prefix) \
|
||||
using prefix##BinarySensor = RemoteReceiverBinarySensor<prefix##Protocol, prefix##Data>; \
|
||||
using prefix##Trigger = RemoteReceiverTrigger<prefix##Protocol, prefix##Data>; \
|
||||
using prefix##Dumper = RemoteReceiverDumper<prefix##Protocol, prefix##Data>;
|
||||
using prefix##BinarySensor = RemoteReceiverBinarySensor<prefix##Protocol>; \
|
||||
using prefix##Trigger = RemoteReceiverTrigger<prefix##Protocol>; \
|
||||
using prefix##Dumper = RemoteReceiverDumper<prefix##Protocol>;
|
||||
#define DECLARE_REMOTE_PROTOCOL(prefix) DECLARE_REMOTE_PROTOCOL_(prefix)
|
||||
|
||||
} // namespace remote_base
|
||||
|
|
|
@ -74,12 +74,12 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
|||
# The default/recommended arduino framework version
|
||||
# - https://github.com/earlephilhower/arduino-pico/releases
|
||||
# - 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
|
||||
# - https://github.com/platformio/platform-raspberrypi/releases
|
||||
# - 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):
|
||||
|
|
|
@ -201,13 +201,19 @@ void SEN5XComponent::setup() {
|
|||
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());
|
||||
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());
|
||||
delay(20);
|
||||
}
|
||||
|
||||
if (this->temperature_compensation_.has_value())
|
||||
if (this->temperature_compensation_.has_value()) {
|
||||
this->write_temperature_compensation_(this->temperature_compensation_.value());
|
||||
delay(20);
|
||||
}
|
||||
|
||||
// Finally start sensor measurements
|
||||
auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY;
|
||||
|
|
|
@ -41,8 +41,8 @@ struct GasTuning {
|
|||
};
|
||||
|
||||
struct TemperatureCompensation {
|
||||
uint16_t offset;
|
||||
uint16_t normalized_offset_slope;
|
||||
int16_t offset;
|
||||
int16_t normalized_offset_slope;
|
||||
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,
|
||||
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
|
||||
uint16_t std_initial, uint16_t gain_factor) {
|
||||
voc_tuning_params_.value().index_offset = index_offset;
|
||||
voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
|
||||
voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
|
||||
voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
|
||||
voc_tuning_params_.value().std_initial = std_initial;
|
||||
voc_tuning_params_.value().gain_factor = gain_factor;
|
||||
GasTuning tuning_params;
|
||||
tuning_params.index_offset = index_offset;
|
||||
tuning_params.learning_time_offset_hours = learning_time_offset_hours;
|
||||
tuning_params.learning_time_gain_hours = learning_time_gain_hours;
|
||||
tuning_params.gating_max_duration_minutes = gating_max_duration_minutes;
|
||||
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,
|
||||
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
|
||||
uint16_t gain_factor) {
|
||||
nox_tuning_params_.value().index_offset = index_offset;
|
||||
nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
|
||||
nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
|
||||
nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
|
||||
nox_tuning_params_.value().std_initial = 50;
|
||||
nox_tuning_params_.value().gain_factor = gain_factor;
|
||||
GasTuning tuning_params;
|
||||
tuning_params.index_offset = index_offset;
|
||||
tuning_params.learning_time_offset_hours = learning_time_offset_hours;
|
||||
tuning_params.learning_time_gain_hours = learning_time_gain_hours;
|
||||
tuning_params.gating_max_duration_minutes = gating_max_duration_minutes;
|
||||
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) {
|
||||
temperature_compensation_.value().offset = offset * 200;
|
||||
temperature_compensation_.value().normalized_offset_slope = normalized_offset_slope * 100;
|
||||
temperature_compensation_.value().time_constant = time_constant;
|
||||
TemperatureCompensation temp_comp;
|
||||
temp_comp.offset = offset * 200;
|
||||
temp_comp.normalized_offset_slope = normalized_offset_slope * 10000;
|
||||
temp_comp.time_constant = time_constant;
|
||||
temperature_compensation_ = temp_comp;
|
||||
}
|
||||
bool start_fan_cleaning();
|
||||
|
||||
|
|
|
@ -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 = (
|
||||
cv.Schema(
|
||||
{
|
||||
|
@ -151,7 +160,9 @@ CONFIG_SCHEMA = (
|
|||
cv.Optional(CONF_TEMPERATURE_COMPENSATION): cv.Schema(
|
||||
{
|
||||
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_,
|
||||
}
|
||||
),
|
||||
|
|
|
@ -33,6 +33,7 @@ MODELS = {
|
|||
"SH1106_96X16": SSD1306Model.SH1106_MODEL_96_16,
|
||||
"SH1106_64X48": SSD1306Model.SH1106_MODEL_64_48,
|
||||
"SH1107_128X64": SSD1306Model.SH1107_MODEL_128_64,
|
||||
"SH1107_128X128": SSD1306Model.SH1107_MODEL_128_128,
|
||||
"SSD1305_128X32": SSD1306Model.SSD1305_MODEL_128_32,
|
||||
"SSD1305_128X64": SSD1306Model.SSD1305_MODEL_128_64,
|
||||
}
|
||||
|
@ -63,8 +64,10 @@ SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
|
|||
cv.Optional(CONF_EXTERNAL_VCC): cv.boolean,
|
||||
cv.Optional(CONF_FLIP_X, default=True): cv.boolean,
|
||||
cv.Optional(CONF_FLIP_Y, default=True): cv.boolean,
|
||||
cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=-32, max=32),
|
||||
cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=-32, max=32),
|
||||
# Offsets determine shifts of memory location to LCD rows/columns,
|
||||
# and this family of controllers supports up to 128x128 screens
|
||||
cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=0, max=128),
|
||||
cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=0, max=128),
|
||||
cv.Optional(CONF_INVERT, default=False): cv.boolean,
|
||||
}
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
|
|
@ -35,16 +35,31 @@ static const uint8_t SSD1306_COMMAND_INVERSE_DISPLAY = 0xA7;
|
|||
static const uint8_t SSD1305_COMMAND_SET_BRIGHTNESS = 0x82;
|
||||
static const uint8_t SSD1305_COMMAND_SET_AREA_COLOR = 0xD8;
|
||||
|
||||
static const uint8_t SH1107_COMMAND_SET_START_LINE = 0xDC;
|
||||
static const uint8_t SH1107_COMMAND_CHARGE_PUMP = 0xAD;
|
||||
|
||||
void SSD1306::setup() {
|
||||
this->init_internal_(this->get_buffer_length_());
|
||||
|
||||
// SH1107 resources
|
||||
//
|
||||
// Datasheet v2.3:
|
||||
// www.displayfuture.com/Display/datasheet/controller/SH1107.pdf
|
||||
// Adafruit C++ driver:
|
||||
// github.com/adafruit/Adafruit_SH110x
|
||||
// Adafruit CircuitPython driver:
|
||||
// github.com/adafruit/Adafruit_CircuitPython_DisplayIO_SH1107
|
||||
|
||||
// Turn off display during initialization (0xAE)
|
||||
this->command(SSD1306_COMMAND_DISPLAY_OFF);
|
||||
|
||||
// If SH1107, use POR defaults (0x50) = divider 1, frequency +0%
|
||||
if (!this->is_sh1107_()) {
|
||||
// Set oscillator frequency to 4'b1000 with no clock division (0xD5)
|
||||
this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV);
|
||||
// Oscillator frequency <= 4'b1000, no clock division
|
||||
this->command(0x80);
|
||||
}
|
||||
|
||||
// Enable low power display mode for SSD1305 (0xD8)
|
||||
if (this->is_ssd1305_()) {
|
||||
|
@ -60,11 +75,26 @@ void SSD1306::setup() {
|
|||
this->command(SSD1306_COMMAND_SET_DISPLAY_OFFSET_Y);
|
||||
this->command(0x00 + this->offset_y_);
|
||||
|
||||
if (this->is_sh1107_()) {
|
||||
// Set start line at line 0 (0xDC)
|
||||
this->command(SH1107_COMMAND_SET_START_LINE);
|
||||
this->command(0x00);
|
||||
} else {
|
||||
// Set start line at line 0 (0x40)
|
||||
this->command(SSD1306_COMMAND_SET_START_LINE | 0x00);
|
||||
}
|
||||
|
||||
if (this->is_ssd1305_()) {
|
||||
// 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)
|
||||
this->command(SSD1306_COMMAND_CHARGE_PUMP);
|
||||
if (this->external_vcc_) {
|
||||
|
@ -76,14 +106,17 @@ void SSD1306::setup() {
|
|||
|
||||
// Set addressing mode to horizontal (0x20)
|
||||
this->command(SSD1306_COMMAND_MEMORY_MODE);
|
||||
if (!this->is_sh1107_()) {
|
||||
// SH1107 memory mode is a 1 byte command
|
||||
this->command(0x00);
|
||||
|
||||
}
|
||||
// X flip mode (0xA0, 0xA1)
|
||||
this->command(SSD1306_COMMAND_SEGRE_MAP | this->flip_x_);
|
||||
|
||||
// Y flip mode (0xC0, 0xC8)
|
||||
this->command(SSD1306_COMMAND_COM_SCAN_INC | (this->flip_y_ << 3));
|
||||
|
||||
if (!this->is_sh1107_()) {
|
||||
// Set pin configuration (0xDA)
|
||||
this->command(SSD1306_COMMAND_SET_COM_PINS);
|
||||
switch (this->model_) {
|
||||
|
@ -98,12 +131,16 @@ void SSD1306::setup() {
|
|||
case SSD1306_MODEL_64_48:
|
||||
case SSD1306_MODEL_64_32:
|
||||
case SH1106_MODEL_64_48:
|
||||
case SH1107_MODEL_128_64:
|
||||
case SSD1305_MODEL_128_32:
|
||||
case SSD1305_MODEL_128_64:
|
||||
case SSD1306_MODEL_72_40:
|
||||
this->command(0x12);
|
||||
break;
|
||||
case SH1107_MODEL_128_64:
|
||||
case SH1107_MODEL_128_128:
|
||||
// Not used, but prevents build warning
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-charge period (0xD9)
|
||||
|
@ -117,7 +154,9 @@ void SSD1306::setup() {
|
|||
// Set V_COM (0xDB)
|
||||
this->command(SSD1306_COMMAND_SET_VCOM_DETECT);
|
||||
switch (this->model_) {
|
||||
case SH1106_MODEL_128_64:
|
||||
case SH1107_MODEL_128_64:
|
||||
case SH1107_MODEL_128_128:
|
||||
this->command(0x35);
|
||||
break;
|
||||
case SSD1306_MODEL_72_40:
|
||||
|
@ -149,7 +188,7 @@ void SSD1306::setup() {
|
|||
this->turn_on();
|
||||
}
|
||||
void SSD1306::display() {
|
||||
if (this->is_sh1106_()) {
|
||||
if (this->is_sh1106_() || this->is_sh1107_()) {
|
||||
this->write_display_data();
|
||||
return;
|
||||
}
|
||||
|
@ -183,6 +222,7 @@ bool SSD1306::is_sh1106_() const {
|
|||
return this->model_ == SH1106_MODEL_96_16 || this->model_ == SH1106_MODEL_128_32 ||
|
||||
this->model_ == SH1106_MODEL_128_64;
|
||||
}
|
||||
bool SSD1306::is_sh1107_() const { return this->model_ == SH1107_MODEL_128_64 || this->model_ == SH1107_MODEL_128_128; }
|
||||
bool SSD1306::is_ssd1305_() const {
|
||||
return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64;
|
||||
}
|
||||
|
@ -224,6 +264,7 @@ void SSD1306::turn_off() {
|
|||
int SSD1306::get_height_internal() {
|
||||
switch (this->model_) {
|
||||
case SH1107_MODEL_128_64:
|
||||
case SH1107_MODEL_128_128:
|
||||
return 128;
|
||||
case SSD1306_MODEL_128_32:
|
||||
case SSD1306_MODEL_64_32:
|
||||
|
@ -254,6 +295,7 @@ int SSD1306::get_width_internal() {
|
|||
case SH1106_MODEL_128_64:
|
||||
case SSD1305_MODEL_128_32:
|
||||
case SSD1305_MODEL_128_64:
|
||||
case SH1107_MODEL_128_128:
|
||||
return 128;
|
||||
case SSD1306_MODEL_96_16:
|
||||
case SH1106_MODEL_96_16:
|
||||
|
|
|
@ -19,6 +19,7 @@ enum SSD1306Model {
|
|||
SH1106_MODEL_96_16,
|
||||
SH1106_MODEL_64_48,
|
||||
SH1107_MODEL_128_64,
|
||||
SH1107_MODEL_128_128,
|
||||
SSD1305_MODEL_128_32,
|
||||
SSD1305_MODEL_128_64,
|
||||
};
|
||||
|
@ -58,6 +59,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer {
|
|||
void init_reset_();
|
||||
|
||||
bool is_sh1106_() const;
|
||||
bool is_sh1107_() const;
|
||||
bool is_ssd1305_() const;
|
||||
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
|
|
|
@ -38,13 +38,19 @@ void I2CSSD1306::dump_config() {
|
|||
}
|
||||
void I2CSSD1306::command(uint8_t value) { this->write_byte(0x00, value); }
|
||||
void HOT I2CSSD1306::write_display_data() {
|
||||
if (this->is_sh1106_()) {
|
||||
if (this->is_sh1106_() || this->is_sh1107_()) {
|
||||
uint32_t i = 0;
|
||||
for (uint8_t page = 0; page < (uint8_t) this->get_height_internal() / 8; page++) {
|
||||
this->command(0xB0 + page); // row
|
||||
this->command(0x02); // lower column
|
||||
if (this->is_sh1106_()) {
|
||||
this->command(0x02); // lower column - 0x02 is historical SH1106 value
|
||||
} else {
|
||||
// Other SH1107 drivers use 0x00
|
||||
// Column values dont change and it seems they can be set only once,
|
||||
// but we follow SH1106 implementation and resend them
|
||||
this->command(0x00);
|
||||
}
|
||||
this->command(0x10); // higher column
|
||||
|
||||
for (uint8_t x = 0; x < (uint8_t) this->get_width_internal() / 16; x++) {
|
||||
uint8_t data[16];
|
||||
for (uint8_t &j : data)
|
||||
|
|
|
@ -36,10 +36,14 @@ void SPISSD1306::command(uint8_t value) {
|
|||
this->disable();
|
||||
}
|
||||
void HOT SPISSD1306::write_display_data() {
|
||||
if (this->is_sh1106_()) {
|
||||
if (this->is_sh1106_() || this->is_sh1107_()) {
|
||||
for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) {
|
||||
this->command(0xB0 + y);
|
||||
if (this->is_sh1106_()) {
|
||||
this->command(0x02);
|
||||
} else {
|
||||
this->command(0x00);
|
||||
}
|
||||
this->command(0x10);
|
||||
this->dc_pin_->digital_write(true);
|
||||
for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x++) {
|
||||
|
|
|
@ -18,20 +18,27 @@ DEPENDENCIES = ["api", "microphone"]
|
|||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
|
||||
CONF_SILENCE_DETECTION = "silence_detection"
|
||||
CONF_ON_LISTENING = "on_listening"
|
||||
CONF_ON_START = "on_start"
|
||||
CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected"
|
||||
CONF_ON_STT_END = "on_stt_end"
|
||||
CONF_ON_TTS_START = "on_tts_start"
|
||||
CONF_ON_TTS_END = "on_tts_end"
|
||||
CONF_ON_END = "on_end"
|
||||
CONF_ON_ERROR = "on_error"
|
||||
CONF_ON_INTENT_END = "on_intent_end"
|
||||
CONF_ON_INTENT_START = "on_intent_start"
|
||||
CONF_ON_LISTENING = "on_listening"
|
||||
CONF_ON_START = "on_start"
|
||||
CONF_ON_STT_END = "on_stt_end"
|
||||
CONF_ON_STT_VAD_END = "on_stt_vad_end"
|
||||
CONF_ON_STT_VAD_START = "on_stt_vad_start"
|
||||
CONF_ON_TTS_END = "on_tts_end"
|
||||
CONF_ON_TTS_START = "on_tts_start"
|
||||
CONF_ON_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_VAD_THRESHOLD = "vad_threshold"
|
||||
|
||||
CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level"
|
||||
CONF_AUTO_GAIN = "auto_gain"
|
||||
CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level"
|
||||
CONF_VOLUME_MULTIPLIER = "volume_multiplier"
|
||||
|
||||
|
||||
|
@ -51,6 +58,17 @@ IsRunningCondition = voice_assistant_ns.class_(
|
|||
"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(
|
||||
cv.Schema(
|
||||
{
|
||||
|
@ -88,8 +106,27 @@ CONFIG_SCHEMA = cv.All(
|
|||
cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
cv.Optional(CONF_ON_INTENT_START): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
cv.Optional(CONF_ON_INTENT_END): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
cv.Optional(CONF_ON_STT_VAD_START): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
cv.Optional(CONF_ON_STT_VAD_END): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
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),
|
||||
tts_stream_validate,
|
||||
)
|
||||
|
||||
|
||||
|
@ -177,6 +214,48 @@ async def to_code(config):
|
|||
config[CONF_ON_CLIENT_DISCONNECTED],
|
||||
)
|
||||
|
||||
if CONF_ON_INTENT_START in config:
|
||||
await automation.build_automation(
|
||||
var.get_intent_start_trigger(),
|
||||
[],
|
||||
config[CONF_ON_INTENT_START],
|
||||
)
|
||||
|
||||
if CONF_ON_INTENT_END in config:
|
||||
await automation.build_automation(
|
||||
var.get_intent_end_trigger(),
|
||||
[],
|
||||
config[CONF_ON_INTENT_END],
|
||||
)
|
||||
|
||||
if CONF_ON_STT_VAD_START in config:
|
||||
await automation.build_automation(
|
||||
var.get_stt_vad_start_trigger(),
|
||||
[],
|
||||
config[CONF_ON_STT_VAD_START],
|
||||
)
|
||||
|
||||
if CONF_ON_STT_VAD_END in config:
|
||||
await automation.build_automation(
|
||||
var.get_stt_vad_end_trigger(),
|
||||
[],
|
||||
config[CONF_ON_STT_VAD_END],
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ void VoiceAssistant::setup() {
|
|||
|
||||
this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
||||
if (socket_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not create socket.");
|
||||
ESP_LOGW(TAG, "Could not create socket");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ void VoiceAssistant::setup() {
|
|||
ExternalRAMAllocator<uint8_t> speaker_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE);
|
||||
if (this->speaker_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate speaker buffer.");
|
||||
ESP_LOGW(TAG, "Could not allocate speaker buffer");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ void VoiceAssistant::setup() {
|
|||
ExternalRAMAllocator<int16_t> allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
|
||||
this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE);
|
||||
if (this->input_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate input buffer.");
|
||||
ESP_LOGW(TAG, "Could not allocate input buffer");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ void VoiceAssistant::setup() {
|
|||
|
||||
this->ring_buffer_ = rb_create(BUFFER_SIZE, sizeof(int16_t));
|
||||
if (this->ring_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate ring buffer.");
|
||||
ESP_LOGW(TAG, "Could not allocate ring buffer");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ void VoiceAssistant::setup() {
|
|||
ExternalRAMAllocator<uint8_t> send_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE);
|
||||
if (send_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate send buffer.");
|
||||
ESP_LOGW(TAG, "Could not allocate send buffer");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
@ -221,8 +221,8 @@ void VoiceAssistant::loop() {
|
|||
msg.audio_settings = audio_settings;
|
||||
|
||||
if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) {
|
||||
ESP_LOGW(TAG, "Could not request start.");
|
||||
this->error_trigger_->trigger("not-connected", "Could not request start.");
|
||||
ESP_LOGW(TAG, "Could not request start");
|
||||
this->error_trigger_->trigger("not-connected", "Could not request start");
|
||||
this->continuous_ = false;
|
||||
this->set_state_(State::IDLE, State::IDLE);
|
||||
break;
|
||||
|
@ -280,7 +280,7 @@ void VoiceAssistant::loop() {
|
|||
this->speaker_buffer_size_ += len;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Receive buffer full.");
|
||||
ESP_LOGW(TAG, "Receive buffer full");
|
||||
}
|
||||
if (this->speaker_buffer_size_ > 0) {
|
||||
size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_);
|
||||
|
@ -290,7 +290,7 @@ void VoiceAssistant::loop() {
|
|||
this->speaker_buffer_index_ -= written;
|
||||
this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); });
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Speaker buffer full.");
|
||||
ESP_LOGW(TAG, "Speaker buffer full");
|
||||
}
|
||||
}
|
||||
if (this->wait_for_stream_end_) {
|
||||
|
@ -513,7 +513,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
|||
break;
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_STT_START:
|
||||
ESP_LOGD(TAG, "STT Started");
|
||||
ESP_LOGD(TAG, "STT started");
|
||||
this->listening_trigger_->trigger();
|
||||
break;
|
||||
case api::enums::VOICE_ASSISTANT_STT_END: {
|
||||
|
@ -525,19 +525,24 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
|||
}
|
||||
}
|
||||
if (text.empty()) {
|
||||
ESP_LOGW(TAG, "No text in STT_END event.");
|
||||
ESP_LOGW(TAG, "No text in STT_END event");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str());
|
||||
this->stt_end_trigger_->trigger(text);
|
||||
break;
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_INTENT_START:
|
||||
ESP_LOGD(TAG, "Intent started");
|
||||
this->intent_start_trigger_->trigger();
|
||||
break;
|
||||
case api::enums::VOICE_ASSISTANT_INTENT_END: {
|
||||
for (auto arg : msg.data) {
|
||||
if (arg.name == "conversation_id") {
|
||||
this->conversation_id_ = std::move(arg.value);
|
||||
}
|
||||
}
|
||||
this->intent_end_trigger_->trigger();
|
||||
break;
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_TTS_START: {
|
||||
|
@ -548,7 +553,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
|||
}
|
||||
}
|
||||
if (text.empty()) {
|
||||
ESP_LOGW(TAG, "No text in TTS_START event.");
|
||||
ESP_LOGW(TAG, "No text in TTS_START event");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Response: \"%s\"", text.c_str());
|
||||
|
@ -566,7 +571,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
|||
}
|
||||
}
|
||||
if (url.empty()) {
|
||||
ESP_LOGW(TAG, "No url in TTS_END event.");
|
||||
ESP_LOGW(TAG, "No url in TTS_END event");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str());
|
||||
|
@ -610,6 +615,11 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
|||
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.
|
||||
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());
|
||||
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: {
|
||||
#ifdef USE_SPEAKER
|
||||
this->wait_for_stream_end_ = true;
|
||||
ESP_LOGD(TAG, "TTS stream start");
|
||||
this->tts_stream_start_trigger_->trigger();
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: {
|
||||
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;
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_STT_VAD_START:
|
||||
ESP_LOGD(TAG, "Starting STT by VAD");
|
||||
this->stt_vad_start_trigger_->trigger();
|
||||
break;
|
||||
case api::enums::VOICE_ASSISTANT_STT_VAD_END:
|
||||
ESP_LOGD(TAG, "STT by VAD end");
|
||||
this->stt_vad_end_trigger_->trigger();
|
||||
break;
|
||||
default:
|
||||
ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type);
|
||||
break;
|
||||
|
|
|
@ -100,13 +100,21 @@ class VoiceAssistant : public Component {
|
|||
void set_auto_gain(uint8_t auto_gain) { this->auto_gain_ = auto_gain; }
|
||||
void set_volume_multiplier(float volume_multiplier) { this->volume_multiplier_ = volume_multiplier; }
|
||||
|
||||
Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; }
|
||||
Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; }
|
||||
Trigger<> *get_listening_trigger() const { return this->listening_trigger_; }
|
||||
Trigger<> *get_end_trigger() const { return this->end_trigger_; }
|
||||
Trigger<> *get_start_trigger() const { return this->start_trigger_; }
|
||||
Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; }
|
||||
Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; }
|
||||
#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<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<> *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<> *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;
|
||||
struct sockaddr_storage dest_addr_;
|
||||
|
||||
Trigger<> *intent_end_trigger_ = new Trigger<>();
|
||||
Trigger<> *intent_start_trigger_ = new Trigger<>();
|
||||
Trigger<> *listening_trigger_ = new Trigger<>();
|
||||
Trigger<> *end_trigger_ = new Trigger<>();
|
||||
Trigger<> *start_trigger_ = new Trigger<>();
|
||||
Trigger<> *stt_vad_start_trigger_ = new Trigger<>();
|
||||
Trigger<> *stt_vad_end_trigger_ = new Trigger<>();
|
||||
#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<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<> *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<> *client_connected_trigger_ = new Trigger<>();
|
||||
|
|
|
@ -403,6 +403,10 @@ async def to_code(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_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_power_save_mode(config[CONF_POWER_SAVE_MODE]))
|
||||
|
|
|
@ -82,6 +82,7 @@ void WiFiComponent::start() {
|
|||
} else {
|
||||
this->start_scanning();
|
||||
}
|
||||
#ifdef USE_WIFI_AP
|
||||
} else if (this->has_ap()) {
|
||||
this->setup_ap_config_();
|
||||
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();
|
||||
}
|
||||
#endif
|
||||
#endif // USE_WIFI_AP
|
||||
}
|
||||
#ifdef USE_IMPROV
|
||||
if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) {
|
||||
|
@ -160,6 +162,7 @@ void WiFiComponent::loop() {
|
|||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
if (this->has_ap() && !this->ap_setup_) {
|
||||
if (now - this->last_connected_ > this->ap_timeout_) {
|
||||
ESP_LOGI(TAG, "Starting fallback AP!");
|
||||
|
@ -170,6 +173,7 @@ void WiFiComponent::loop() {
|
|||
#endif
|
||||
}
|
||||
}
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
#ifdef USE_IMPROV
|
||||
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_rrm(bool rrm) { this->rrm_ = rrm; }
|
||||
#endif
|
||||
|
||||
network::IPAddress WiFiComponent::get_ip_address() {
|
||||
if (this->has_sta())
|
||||
return this->wifi_sta_ip();
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
if (this->has_ap())
|
||||
return this->wifi_soft_ap_ip();
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
return {};
|
||||
}
|
||||
network::IPAddress WiFiComponent::get_dns_address(int num) {
|
||||
|
@ -218,6 +227,8 @@ std::string WiFiComponent::get_use_address() const {
|
|||
return this->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_() {
|
||||
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) {
|
||||
this->ap_ = ap;
|
||||
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::set_sta(const WiFiAP &ap) {
|
||||
this->clear_sta();
|
||||
|
@ -389,6 +403,10 @@ void WiFiComponent::print_connect_params_() {
|
|||
bssid_t bssid = wifi_bssid();
|
||||
|
||||
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, " 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],
|
||||
|
|
|
@ -194,6 +194,7 @@ class WiFiComponent : public Component {
|
|||
void add_sta(const WiFiAP &ap);
|
||||
void clear_sta();
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
/** 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.
|
||||
|
@ -203,6 +204,7 @@ class WiFiComponent : public Component {
|
|||
*/
|
||||
void set_ap(const WiFiAP &ap);
|
||||
WiFiAP get_ap() { return this->ap_; }
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
|
@ -299,7 +301,11 @@ class WiFiComponent : public Component {
|
|||
|
||||
protected:
|
||||
static std::string format_mac_addr(const uint8_t mac[6]);
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
void setup_ap_config_();
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
void print_connect_params_();
|
||||
|
||||
void wifi_loop_();
|
||||
|
@ -313,8 +319,12 @@ class WiFiComponent : public Component {
|
|||
void wifi_pre_setup_();
|
||||
WiFiSTAConnectStatus wifi_sta_connect_status_();
|
||||
bool wifi_scan_start_(bool passive);
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
bool wifi_ap_ip_config_(optional<ManualIP> manual_ip);
|
||||
bool wifi_start_ap_(const WiFiAP &ap);
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
bool wifi_disconnect_();
|
||||
int32_t wifi_channel_();
|
||||
network::IPAddress wifi_subnet_mask_();
|
||||
|
|
|
@ -597,6 +597,8 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
|||
WiFi.scanDelete();
|
||||
this->scan_done_ = true;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
||||
esp_err_t err;
|
||||
|
||||
|
@ -654,6 +656,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
// enable AP
|
||||
if (!this->wifi_mode_({}, true))
|
||||
|
@ -692,11 +695,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
|
||||
tcpip_adapter_ip_info_t ip;
|
||||
tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip);
|
||||
return network::IPAddress(&ip.ip);
|
||||
}
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
|
||||
|
||||
bssid_t WiFiComponent::wifi_bssid() {
|
||||
|
|
|
@ -688,6 +688,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
|||
}
|
||||
this->scan_done_ = true;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
||||
// enable AP
|
||||
if (!this->wifi_mode_({}, true))
|
||||
|
@ -753,6 +755,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
// enable AP
|
||||
if (!this->wifi_mode_({}, true))
|
||||
|
@ -790,11 +793,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
|
||||
struct ip_info ip {};
|
||||
wifi_get_ip_info(SOFTAP_IF, &ip);
|
||||
return network::IPAddress(&ip.ip);
|
||||
}
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
bssid_t WiFiComponent::wifi_bssid() {
|
||||
bssid_t bssid{};
|
||||
uint8_t *raw_bssid = WiFi.BSSID();
|
||||
|
|
|
@ -17,7 +17,11 @@
|
|||
#ifdef USE_WIFI_WPA2_EAP
|
||||
#include <esp_wpa2.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
#include "dhcpserver/dhcpserver.h"
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
#include "lwip/err.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 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)
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
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_connected = 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();
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
s_ap_netif = esp_netif_create_default_wifi_ap();
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
// cfg.nvs_enable = false;
|
||||
err = esp_wifi_init(&cfg);
|
||||
|
@ -674,6 +686,11 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (it.number == 0) {
|
||||
// no results
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t number = it.number;
|
||||
std::vector<wifi_ap_record_t> records(number);
|
||||
err = esp_wifi_scan_get_ap_records(&number, records.data());
|
||||
|
@ -761,6 +778,8 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
|||
scan_done_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
||||
esp_err_t err;
|
||||
|
||||
|
@ -816,6 +835,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
// enable AP
|
||||
if (!this->wifi_mode_({}, true))
|
||||
|
@ -853,6 +873,8 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
|||
|
||||
return true;
|
||||
}
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_netif_get_ip_info(s_sta_netif, &ip);
|
||||
|
|
|
@ -412,6 +412,8 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
|||
WiFi.scanDelete();
|
||||
this->scan_done_ = true;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
||||
// enable AP
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
// enable AP
|
||||
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(),
|
||||
ap.get_channel().value_or(1), ap.get_hidden());
|
||||
}
|
||||
|
||||
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; }
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
bool WiFiComponent::wifi_disconnect_() { return WiFi.disconnect(); }
|
||||
|
||||
bssid_t WiFiComponent::wifi_bssid() {
|
||||
|
|
|
@ -138,6 +138,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
|||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
||||
// TODO:
|
||||
return false;
|
||||
|
@ -151,7 +152,9 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.localIP()}; }
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
bool WiFiComponent::wifi_disconnect_() {
|
||||
int err = cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA);
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
#define USE_TOUCHSCREEN
|
||||
#define USE_UART_DEBUGGER
|
||||
#define USE_WIFI
|
||||
#define USE_WIFI_AP
|
||||
|
||||
// Arduino-specific feature flags
|
||||
#ifdef USE_ARDUINO
|
||||
|
|
8
esphome/dashboard/const.py
Normal file
8
esphome/dashboard/const.py
Normal 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
135
esphome/dashboard/core.py
Normal 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
387
esphome/dashboard/entries.py
Normal file
387
esphome/dashboard/entries.py
Normal 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
19
esphome/dashboard/enum.py
Normal 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)
|
78
esphome/dashboard/settings.py
Normal file
78
esphome/dashboard/settings.py
Normal 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
|
0
esphome/dashboard/status/__init__.py
Normal file
0
esphome/dashboard/status/__init__.py
Normal file
98
esphome/dashboard/status/mdns.py
Normal file
98
esphome/dashboard/status/mdns.py
Normal 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
|
67
esphome/dashboard/status/mqtt.py
Normal file
67
esphome/dashboard/status/mqtt.py
Normal 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()
|
49
esphome/dashboard/status/ping.py
Normal file
49
esphome/dashboard/status/ping.py
Normal 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))
|
0
esphome/dashboard/util/__init__.py
Normal file
0
esphome/dashboard/util/__init__.py
Normal file
55
esphome/dashboard/util/file.py
Normal file
55
esphome/dashboard/util/file.py
Normal 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,
|
||||
)
|
22
esphome/dashboard/util/itertools.py
Normal file
22
esphome/dashboard/util/itertools.py
Normal 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)), [])
|
11
esphome/dashboard/util/password.py
Normal file
11
esphome/dashboard/util/password.py
Normal 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()
|
31
esphome/dashboard/util/subprocess.py
Normal file
31
esphome/dashboard/util/subprocess.py
Normal 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
|
|
@ -1,17 +1,10 @@
|
|||
import hashlib
|
||||
from __future__ import annotations
|
||||
|
||||
import unicodedata
|
||||
|
||||
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):
|
||||
return "".join(
|
||||
c
|
1091
esphome/dashboard/web_server.py
Normal file
1091
esphome/dashboard/web_server.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,10 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import logging
|
||||
import random
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import gzip
|
||||
|
||||
from esphome.core import EsphomeError
|
||||
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
|
||||
|
||||
|
||||
UPLOAD_BLOCK_SIZE = 8192
|
||||
UPLOAD_BUFFER_SIZE = UPLOAD_BLOCK_SIZE * 8
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -184,7 +191,9 @@ def send_check(sock, data, msg):
|
|||
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_size = len(file_contents)
|
||||
_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)
|
||||
# Limit send buffer (usually around 100kB) in order to have progress bar
|
||||
# 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
|
||||
sock.settimeout(20.0)
|
||||
sock.settimeout(30.0)
|
||||
start_time = time.perf_counter()
|
||||
|
||||
offset = 0
|
||||
progress = ProgressBar()
|
||||
while True:
|
||||
chunk = upload_contents[offset : offset + 1024]
|
||||
chunk = upload_contents[offset : offset + UPLOAD_BLOCK_SIZE]
|
||||
if not chunk:
|
||||
break
|
||||
offset += len(chunk)
|
||||
|
@ -277,8 +288,9 @@ def perform_ota(sock, password, file_handle, filename):
|
|||
|
||||
# Enable nodelay for last checks
|
||||
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, "Update end", RESPONSE_UPDATE_END_OK)
|
||||
|
|
75
esphome/external_files.py
Normal file
75
esphome/external_files.py
Normal 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
|
|
@ -1,21 +1,15 @@
|
|||
from __future__ import annotations
|
||||
import binascii
|
||||
import codecs
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
from esphome import const
|
||||
from esphome.const import CONF_DISABLED, CONF_MDNS
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import write_file_if_changed
|
||||
|
||||
|
||||
from esphome.const import (
|
||||
CONF_MDNS,
|
||||
CONF_DISABLED,
|
||||
)
|
||||
|
||||
from esphome.types import CoreType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -40,48 +34,47 @@ def trash_storage_path() -> str:
|
|||
class StorageJSON:
|
||||
def __init__(
|
||||
self,
|
||||
storage_version,
|
||||
name,
|
||||
friendly_name,
|
||||
comment,
|
||||
esphome_version,
|
||||
src_version,
|
||||
address,
|
||||
web_port,
|
||||
target_platform,
|
||||
build_path,
|
||||
firmware_bin_path,
|
||||
loaded_integrations,
|
||||
no_mdns,
|
||||
):
|
||||
storage_version: int,
|
||||
name: str,
|
||||
friendly_name: str,
|
||||
comment: str,
|
||||
esphome_version: str,
|
||||
src_version: int | None,
|
||||
address: str,
|
||||
web_port: int | None,
|
||||
target_platform: str,
|
||||
build_path: str,
|
||||
firmware_bin_path: str,
|
||||
loaded_integrations: set[str],
|
||||
no_mdns: bool,
|
||||
) -> None:
|
||||
# Version of the storage JSON schema
|
||||
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
|
||||
self.name: str = name
|
||||
self.name = name
|
||||
# The friendly name of the node
|
||||
self.friendly_name: str = friendly_name
|
||||
self.friendly_name = friendly_name
|
||||
# The comment of the node
|
||||
self.comment: str = comment
|
||||
self.comment = comment
|
||||
# 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
|
||||
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
|
||||
self.address: str = address
|
||||
self.address = address
|
||||
# Web server port of the ESP, for example 80
|
||||
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.
|
||||
self.target_platform: str = target_platform
|
||||
self.target_platform = target_platform
|
||||
# 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
|
||||
self.firmware_bin_path: str = firmware_bin_path
|
||||
# A list of strings of names of loaded integrations
|
||||
self.loaded_integrations: list[str] = loaded_integrations
|
||||
self.loaded_integrations.sort()
|
||||
self.firmware_bin_path = firmware_bin_path
|
||||
# A set of strings of names of loaded integrations
|
||||
self.loaded_integrations = loaded_integrations
|
||||
# Is mDNS disabled
|
||||
self.no_mdns = no_mdns
|
||||
|
||||
|
@ -98,7 +91,7 @@ class StorageJSON:
|
|||
"esp_platform": self.target_platform,
|
||||
"build_path": self.build_path,
|
||||
"firmware_bin_path": self.firmware_bin_path,
|
||||
"loaded_integrations": self.loaded_integrations,
|
||||
"loaded_integrations": sorted(self.loaded_integrations),
|
||||
"no_mdns": self.no_mdns,
|
||||
}
|
||||
|
||||
|
@ -109,9 +102,7 @@ class StorageJSON:
|
|||
write_file_if_changed(path, self.to_json())
|
||||
|
||||
@staticmethod
|
||||
def from_esphome_core(
|
||||
esph: CoreType, old: Optional["StorageJSON"]
|
||||
) -> "StorageJSON":
|
||||
def from_esphome_core(esph: CoreType, old: StorageJSON | None) -> StorageJSON:
|
||||
hardware = esph.target_platform.upper()
|
||||
if esph.is_esp32:
|
||||
from esphome.components import esp32
|
||||
|
@ -129,7 +120,7 @@ class StorageJSON:
|
|||
target_platform=hardware,
|
||||
build_path=esph.build_path,
|
||||
firmware_bin_path=esph.firmware_bin,
|
||||
loaded_integrations=list(esph.loaded_integrations),
|
||||
loaded_integrations=esph.loaded_integrations,
|
||||
no_mdns=(
|
||||
CONF_MDNS in esph.config
|
||||
and CONF_DISABLED in esph.config[CONF_MDNS]
|
||||
|
@ -140,7 +131,7 @@ class StorageJSON:
|
|||
@staticmethod
|
||||
def from_wizard(
|
||||
name: str, friendly_name: str, address: str, platform: str
|
||||
) -> "StorageJSON":
|
||||
) -> StorageJSON:
|
||||
return StorageJSON(
|
||||
storage_version=1,
|
||||
name=name,
|
||||
|
@ -153,12 +144,12 @@ class StorageJSON:
|
|||
target_platform=platform,
|
||||
build_path=None,
|
||||
firmware_bin_path=None,
|
||||
loaded_integrations=[],
|
||||
loaded_integrations=set(),
|
||||
no_mdns=False,
|
||||
)
|
||||
|
||||
@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:
|
||||
storage = json.load(f_handle)
|
||||
storage_version = storage["storage_version"]
|
||||
|
@ -174,7 +165,7 @@ class StorageJSON:
|
|||
esp_platform = storage.get("esp_platform")
|
||||
build_path = storage.get("build_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)
|
||||
return StorageJSON(
|
||||
storage_version,
|
||||
|
@ -193,7 +184,7 @@ class StorageJSON:
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def load(path: str) -> Optional["StorageJSON"]:
|
||||
def load(path: str) -> StorageJSON | None:
|
||||
try:
|
||||
return StorageJSON._load_impl(path)
|
||||
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
|
||||
self.last_update_check_str: str = last_update_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:
|
||||
return {
|
||||
|
@ -226,7 +217,7 @@ class EsphomeStorageJSON:
|
|||
}
|
||||
|
||||
@property
|
||||
def last_update_check(self) -> Optional[datetime]:
|
||||
def last_update_check(self) -> datetime | None:
|
||||
try:
|
||||
return datetime.strptime(self.last_update_check_str, "%Y-%m-%dT%H:%M:%S")
|
||||
except Exception: # pylint: disable=broad-except
|
||||
|
@ -243,7 +234,7 @@ class EsphomeStorageJSON:
|
|||
write_file_if_changed(path, self.to_json())
|
||||
|
||||
@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:
|
||||
storage = json.load(f_handle)
|
||||
storage_version = storage["storage_version"]
|
||||
|
@ -255,14 +246,14 @@ class EsphomeStorageJSON:
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def load(path: str) -> Optional["EsphomeStorageJSON"]:
|
||||
def load(path: str) -> EsphomeStorageJSON | None:
|
||||
try:
|
||||
return EsphomeStorageJSON._load_impl(path)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_default() -> "EsphomeStorageJSON":
|
||||
def get_default() -> EsphomeStorageJSON:
|
||||
return EsphomeStorageJSON(
|
||||
storage_version=1,
|
||||
cookie_secret=binascii.hexlify(os.urandom(64)).decode(),
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
from zeroconf import (
|
||||
IPVersion,
|
||||
ServiceBrowser,
|
||||
ServiceInfo,
|
||||
ServiceStateChange,
|
||||
Zeroconf,
|
||||
)
|
||||
from zeroconf import IPVersion, ServiceInfo, ServiceStateChange, Zeroconf
|
||||
from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf
|
||||
|
||||
from esphome.storage_json import StorageJSON, ext_storage_path
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_BACKGROUND_TASKS: set[asyncio.Task] = set()
|
||||
|
||||
|
||||
class HostResolver(ServiceInfo):
|
||||
"""Resolve a host name to an IP address."""
|
||||
|
||||
|
@ -65,7 +64,7 @@ class DiscoveredImport:
|
|||
network: str
|
||||
|
||||
|
||||
class DashboardBrowser(ServiceBrowser):
|
||||
class DashboardBrowser(AsyncServiceBrowser):
|
||||
"""A class to browse for ESPHome nodes."""
|
||||
|
||||
|
||||
|
@ -94,7 +93,28 @@ class DashboardImportDiscovery:
|
|||
# Ignore updates for devices that are not in the import state
|
||||
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)
|
||||
if info is None:
|
||||
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):
|
||||
def resolve_host(self, host: str, timeout: float = 3.0) -> str | None:
|
||||
"""Resolve a host name to an IP address."""
|
||||
name = host.partition(".")[0]
|
||||
info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}")
|
||||
info = _make_host_resolver(host)
|
||||
if (
|
||||
info.load_from_cache(self)
|
||||
or (timeout and info.request(self, timeout * 1000))
|
||||
) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)):
|
||||
return str(addresses[0])
|
||||
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
|
||||
|
|
|
@ -39,7 +39,7 @@ lib_deps =
|
|||
bblanchon/ArduinoJson@6.18.5 ; json
|
||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||
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
|
||||
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
|
||||
build_flags =
|
||||
|
@ -160,7 +160,7 @@ board_build.filesystem_size = 0.5m
|
|||
platform = https://github.com/maxgerhardt/platform-raspberrypi.git
|
||||
platform_packages =
|
||||
; 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
|
||||
lib_deps =
|
||||
|
|
|
@ -10,8 +10,9 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile
|
|||
esptool==4.6.2
|
||||
click==8.1.7
|
||||
esphome-dashboard==20231107.0
|
||||
aioesphomeapi==18.4.0
|
||||
zeroconf==0.122.3
|
||||
aioesphomeapi==18.5.5
|
||||
zeroconf==0.127.0
|
||||
python-magic==0.4.27
|
||||
|
||||
# esp-idf requires this, but doesn't bundle it by default
|
||||
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
pillow==10.0.1
|
||||
pillow==10.1.0
|
||||
cairosvg==2.7.1
|
||||
cryptography==41.0.4
|
||||
|
|
|
@ -45,7 +45,7 @@ def sub(path, pattern, repl, expected_count=1):
|
|||
content, count = re.subn(pattern, repl, content, flags=re.MULTILINE)
|
||||
if expected_count is not None:
|
||||
assert count == expected_count, f"Pattern {pattern} replacement failed!"
|
||||
with open(path, "wt") as fh:
|
||||
with open(path, "w") as fh:
|
||||
fh.write(content)
|
||||
|
||||
|
||||
|
|
0
tests/dashboard/__init__.py
Normal file
0
tests/dashboard/__init__.py
Normal file
0
tests/dashboard/util/__init__.py
Normal file
0
tests/dashboard/util/__init__.py
Normal file
53
tests/dashboard/util/test_file.py
Normal file
53
tests/dashboard/util/test_file.py
Normal 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
|
|
@ -3072,6 +3072,9 @@ remote_receiver:
|
|||
on_coolix:
|
||||
then:
|
||||
delay: !lambda "return x.first + x.second;"
|
||||
on_rc_switch:
|
||||
then:
|
||||
delay: !lambda "return uint32_t(x.code) + x.protocol;"
|
||||
|
||||
status_led:
|
||||
pin: GPIO2
|
||||
|
|
|
@ -718,6 +718,7 @@ stepper:
|
|||
|
||||
interval:
|
||||
interval: 5s
|
||||
startup_delay: 10s
|
||||
then:
|
||||
- logger.log: Interval Run
|
||||
|
||||
|
@ -752,6 +753,18 @@ image:
|
|||
file: pnglogo.png
|
||||
type: RGB565
|
||||
use_transparency: no
|
||||
- id: web_svg_image
|
||||
file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
|
||||
resize: 256x48
|
||||
type: TRANSPARENT_BINARY
|
||||
- id: web_tiff_image
|
||||
file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
|
||||
type: RGB24
|
||||
resize: 48x48
|
||||
- id: web_redirect_image
|
||||
file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
|
||||
type: RGB24
|
||||
resize: 48x48
|
||||
|
||||
- id: mdi_alert
|
||||
file: mdi:alert-circle-outline
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Iterator
|
||||
from collections.abc import Iterator
|
||||
|
||||
import math
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import pytest
|
||||
from mock import Mock
|
||||
from unittest.mock import Mock
|
||||
|
||||
from esphome import cpp_helpers as ch
|
||||
from esphome import const
|
||||
|
|
Loading…
Reference in a new issue