Merge branch 'dev' into optolink

This commit is contained in:
j0ta29 2023-06-23 23:09:26 +02:00 committed by GitHub
commit 4973dd4a5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
88 changed files with 5472 additions and 2194 deletions

View file

@ -0,0 +1,38 @@
name: Restore Python
inputs:
python-version:
description: Python version to restore
required: true
type: string
cache-key:
description: Cache key to use
required: true
type: string
outputs:
python-version:
description: Python version restored
value: ${{ steps.python.outputs.python-version }}
runs:
using: "composite"
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@v4.6.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@v3.3.1
with:
path: venv
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
shell: bash
run: |
python -m venv venv
. venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
pip install -e .

View file

@ -26,10 +26,16 @@ jobs:
common: common:
name: Create common environment name: Create common environment
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.2 uses: actions/checkout@v3.5.2
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.6.0 uses: actions/setup-python@v4.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -39,7 +45,7 @@ jobs:
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ steps.cache-key.outputs.key }}
- name: Create Python virtual environment - name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -66,12 +72,11 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.2 uses: actions/checkout@v3.5.2
- name: Restore Python virtual environment - name: Restore Python
uses: actions/cache/restore@v3.3.1 uses: ./.github/actions/restore-python
with: with:
path: venv python-version: ${{ env.DEFAULT_PYTHON }}
# yamllint disable-line rule:line-length cache-key: ${{ needs.common.outputs.cache-key }}
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
- name: Run black - name: Run black
run: | run: |
. venv/bin/activate . venv/bin/activate
@ -88,12 +93,11 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.2 uses: actions/checkout@v3.5.2
- name: Restore Python virtual environment - name: Restore Python
uses: actions/cache/restore@v3.3.1 uses: ./.github/actions/restore-python
with: with:
path: venv python-version: ${{ env.DEFAULT_PYTHON }}
# yamllint disable-line rule:line-length cache-key: ${{ needs.common.outputs.cache-key }}
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
- name: Run flake8 - name: Run flake8
run: | run: |
. venv/bin/activate . venv/bin/activate
@ -110,12 +114,11 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.2 uses: actions/checkout@v3.5.2
- name: Restore Python virtual environment - name: Restore Python
uses: actions/cache/restore@v3.3.1 uses: ./.github/actions/restore-python
with: with:
path: venv python-version: ${{ env.DEFAULT_PYTHON }}
# yamllint disable-line rule:line-length cache-key: ${{ needs.common.outputs.cache-key }}
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
- name: Run pylint - name: Run pylint
run: | run: |
. venv/bin/activate . venv/bin/activate
@ -132,12 +135,11 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.2 uses: actions/checkout@v3.5.2
- name: Restore Python virtual environment - name: Restore Python
uses: actions/cache/restore@v3.3.1 uses: ./.github/actions/restore-python
with: with:
path: venv python-version: ${{ env.DEFAULT_PYTHON }}
# yamllint disable-line rule:line-length cache-key: ${{ needs.common.outputs.cache-key }}
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
- name: Run pyupgrade - name: Run pyupgrade
run: | run: |
. venv/bin/activate . venv/bin/activate
@ -154,12 +156,11 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.2 uses: actions/checkout@v3.5.2
- name: Restore Python virtual environment - name: Restore Python
uses: actions/cache/restore@v3.3.1 uses: ./.github/actions/restore-python
with: with:
path: venv python-version: ${{ env.DEFAULT_PYTHON }}
# yamllint disable-line rule:line-length cache-key: ${{ needs.common.outputs.cache-key }}
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
- name: Register matcher - name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json" run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json"
- name: Run script/ci-custom - name: Run script/ci-custom
@ -176,12 +177,11 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.2 uses: actions/checkout@v3.5.2
- name: Restore Python virtual environment - name: Restore Python
uses: actions/cache/restore@v3.3.1 uses: ./.github/actions/restore-python
with: with:
path: venv python-version: ${{ env.DEFAULT_PYTHON }}
# yamllint disable-line rule:line-length cache-key: ${{ needs.common.outputs.cache-key }}
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
- name: Register matcher - name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/pytest.json" run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run pytest - name: Run pytest
@ -197,12 +197,11 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.2 uses: actions/checkout@v3.5.2
- name: Restore Python virtual environment - name: Restore Python
uses: actions/cache/restore@v3.3.1 uses: ./.github/actions/restore-python
with: with:
path: venv python-version: ${{ env.DEFAULT_PYTHON }}
# yamllint disable-line rule:line-length cache-key: ${{ needs.common.outputs.cache-key }}
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
- name: Install clang-format - name: Install clang-format
run: | run: |
. venv/bin/activate . venv/bin/activate
@ -237,12 +236,11 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.2 uses: actions/checkout@v3.5.2
- name: Restore Python virtual environment - name: Restore Python
uses: actions/cache/restore@v3.3.1 uses: ./.github/actions/restore-python
with: with:
path: venv python-version: ${{ env.DEFAULT_PYTHON }}
# yamllint disable-line rule:line-length cache-key: ${{ needs.common.outputs.cache-key }}
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
- name: Cache platformio - name: Cache platformio
uses: actions/cache@v3.3.1 uses: actions/cache@v3.3.1
with: with:
@ -300,13 +298,11 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.2 uses: actions/checkout@v3.5.2
- name: Restore Python virtual environment - name: Restore Python
uses: actions/cache/restore@v3.3.1 uses: ./.github/actions/restore-python
with: with:
path: venv python-version: ${{ env.DEFAULT_PYTHON }}
# yamllint disable-line rule:line-length cache-key: ${{ needs.common.outputs.cache-key }}
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
# Use per check platformio cache because checks use different parts
- name: Cache platformio - name: Cache platformio
uses: actions/cache@v3.3.1 uses: actions/cache@v3.3.1
with: with:

View file

@ -49,9 +49,11 @@ jobs:
with: with:
python-version: "3.x" python-version: "3.x"
- name: Set up python environment - name: Set up python environment
env:
ESPHOME_NO_VENV: 1
run: | run: |
script/setup script/setup
pip install setuptools wheel twine pip install twine
- name: Build - name: Build
run: python setup.py sdist bdist_wheel run: python setup.py sdist bdist_wheel
- name: Upload - name: Upload

View file

@ -17,6 +17,7 @@ esphome/components/adc/* @esphome/core
esphome/components/adc128s102/* @DeerMaximum esphome/components/adc128s102/* @DeerMaximum
esphome/components/addressable_light/* @justfalter esphome/components/addressable_light/* @justfalter
esphome/components/airthings_ble/* @jeromelaban esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_base/* @jeromelaban @ncareau
esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_mini/* @ncareau
esphome/components/airthings_wave_plus/* @jeromelaban esphome/components/airthings_wave_plus/* @jeromelaban
esphome/components/alarm_control_panel/* @grahambrown11 esphome/components/alarm_control_panel/* @grahambrown11
@ -102,7 +103,7 @@ esphome/components/gpio/* @esphome/core
esphome/components/gps/* @coogle esphome/components/gps/* @coogle
esphome/components/graph/* @synco esphome/components/graph/* @synco
esphome/components/growatt_solar/* @leeuwte esphome/components/growatt_solar/* @leeuwte
esphome/components/haier/* @Yarikx esphome/components/haier/* @paveldn
esphome/components/havells_solar/* @sourabhjaiswal esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/light/* @DotNetDann
@ -319,4 +320,5 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xiaomi_rtcgq02lm/* @jesserockz esphome/components/xiaomi_rtcgq02lm/* @jesserockz
esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/* @nielsnl68 @numo68 esphome/components/xpt2046/* @nielsnl68 @numo68

View file

@ -0,0 +1,83 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, ble_client
from esphome.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
STATE_CLASS_MEASUREMENT,
UNIT_PERCENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
CONF_HUMIDITY,
CONF_TVOC,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
UNIT_PARTS_PER_BILLION,
ICON_RADIATOR,
)
CODEOWNERS = ["@ncareau", "@jeromelaban"]
DEPENDENCIES = ["ble_client"]
airthings_wave_base_ns = cg.esphome_ns.namespace("airthings_wave_base")
AirthingsWaveBase = airthings_wave_base_ns.class_(
"AirthingsWaveBase", cg.PollingComponent, ble_client.BLEClientNode
)
BASE_SCHEMA = (
sensor.SENSOR_SCHEMA.extend(
{
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=0,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("5min"))
.extend(ble_client.BLE_CLIENT_SCHEMA)
)
async def wave_base_to_code(var, config):
await cg.register_component(var, config)
await ble_client.register_ble_node(var, config)
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity(sens))
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature(sens))
if CONF_PRESSURE in config:
sens = await sensor.new_sensor(config[CONF_PRESSURE])
cg.add(var.set_pressure(sens))
if CONF_TVOC in config:
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))

View file

@ -0,0 +1,83 @@
#include "airthings_wave_base.h"
#ifdef USE_ESP32
namespace esphome {
namespace airthings_wave_base {
static const char *const TAG = "airthings_wave_base";
void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_OPEN_EVT: {
if (param->open.status == ESP_GATT_OK) {
ESP_LOGI(TAG, "Connected successfully!");
}
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
ESP_LOGW(TAG, "Disconnected!");
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
this->handle_ = 0;
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_);
if (chr == nullptr) {
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(),
this->sensors_data_characteristic_uuid_.to_string().c_str());
break;
}
this->handle_ = chr->handle;
this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;
this->request_read_values_();
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->parent()->get_conn_id())
break;
if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
break;
}
if (param->read.handle == this->handle_) {
this->read_sensors(param->read.value, param->read.value_len);
}
break;
}
default:
break;
}
}
bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; }
void AirthingsWaveBase::update() {
if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) {
if (!this->parent()->enabled) {
ESP_LOGW(TAG, "Reconnecting to device");
this->parent()->set_enabled(true);
this->parent()->connect();
} else {
ESP_LOGW(TAG, "Connection in progress");
}
}
}
void AirthingsWaveBase::request_read_values_() {
auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_,
ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
}
}
} // namespace airthings_wave_base
} // namespace esphome
#endif // USE_ESP32

View file

@ -0,0 +1,50 @@
#pragma once
#ifdef USE_ESP32
#include <esp_gattc_api.h>
#include <algorithm>
#include <iterator>
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"
namespace esphome {
namespace airthings_wave_base {
class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientNode {
public:
AirthingsWaveBase() = default;
void update() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
protected:
bool is_valid_voc_value_(uint16_t voc);
virtual void read_sensors(uint8_t *value, uint16_t value_len) = 0;
void request_read_values_();
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
sensor::Sensor *tvoc_sensor_{nullptr};
uint16_t handle_;
esp32_ble_tracker::ESPBTUUID service_uuid_;
esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_;
};
} // namespace airthings_wave_base
} // namespace esphome
#endif // USE_ESP32

View file

@ -7,105 +7,47 @@ namespace airthings_wave_mini {
static const char *const TAG = "airthings_wave_mini"; static const char *const TAG = "airthings_wave_mini";
void AirthingsWaveMini::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) {
esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_OPEN_EVT: {
if (param->open.status == ESP_GATT_OK) {
ESP_LOGI(TAG, "Connected successfully!");
}
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
ESP_LOGW(TAG, "Disconnected!");
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
this->handle_ = 0;
auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_);
if (chr == nullptr) {
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(),
sensors_data_characteristic_uuid_.to_string().c_str());
break;
}
this->handle_ = chr->handle;
this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;
request_read_values_();
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->parent()->get_conn_id())
break;
if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
break;
}
if (param->read.handle == this->handle_) {
read_sensors_(param->read.value, param->read.value_len);
}
break;
}
default:
break;
}
}
void AirthingsWaveMini::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
auto *value = (WaveMiniReadings *) raw_value; auto *value = (WaveMiniReadings *) raw_value;
if (sizeof(WaveMiniReadings) <= value_len) { if (sizeof(WaveMiniReadings) <= value_len) {
this->humidity_sensor_->publish_state(value->humidity / 100.0f); if (this->humidity_sensor_ != nullptr) {
this->pressure_sensor_->publish_state(value->pressure / 50.0f); this->humidity_sensor_->publish_state(value->humidity / 100.0f);
this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f); }
if (is_valid_voc_value_(value->voc)) {
if (this->pressure_sensor_ != nullptr) {
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
}
if (this->temperature_sensor_ != nullptr) {
this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f);
}
if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
this->tvoc_sensor_->publish_state(value->voc); this->tvoc_sensor_->publish_state(value->voc);
} }
// This instance must not stay connected // This instance must not stay connected
// so other clients can connect to it (e.g. the // so other clients can connect to it (e.g. the
// mobile app). // mobile app).
parent()->set_enabled(false); this->parent()->set_enabled(false);
}
}
bool AirthingsWaveMini::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; }
void AirthingsWaveMini::update() {
if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) {
if (!parent()->enabled) {
ESP_LOGW(TAG, "Reconnecting to device");
parent()->set_enabled(true);
parent()->connect();
} else {
ESP_LOGW(TAG, "Connection in progress");
}
}
}
void AirthingsWaveMini::request_read_values_() {
auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_,
ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
} }
} }
void AirthingsWaveMini::dump_config() { void AirthingsWaveMini::dump_config() {
// these really don't belong here, but there doesn't seem to be a
// practical way to have the base class use LOG_SENSOR and include
// the TAG from this component
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
} }
AirthingsWaveMini::AirthingsWaveMini() AirthingsWaveMini::AirthingsWaveMini() {
: PollingComponent(10000), this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID);
service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} }
} // namespace airthings_wave_mini } // namespace airthings_wave_mini
} // namespace esphome } // namespace esphome

View file

@ -2,14 +2,7 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_gattc_api.h> #include "esphome/components/airthings_wave_base/airthings_wave_base.h"
#include <algorithm>
#include <iterator>
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"
namespace esphome { namespace esphome {
namespace airthings_wave_mini { namespace airthings_wave_mini {
@ -17,35 +10,14 @@ namespace airthings_wave_mini {
static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba"; static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba";
static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba"; static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba";
class AirthingsWaveMini : public PollingComponent, public ble_client::BLEClientNode { class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase {
public: public:
AirthingsWaveMini(); AirthingsWaveMini();
void dump_config() override; void dump_config() override;
void update() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
protected: protected:
bool is_valid_voc_value_(uint16_t voc); void read_sensors(uint8_t *value, uint16_t value_len) override;
void read_sensors_(uint8_t *value, uint16_t value_len);
void request_read_values_();
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
sensor::Sensor *tvoc_sensor_{nullptr};
uint16_t handle_;
esp32_ble_tracker::ESPBTUUID service_uuid_;
esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_;
struct WaveMiniReadings { struct WaveMiniReadings {
uint16_t unused01; uint16_t unused01;

View file

@ -1,82 +1,28 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import sensor, ble_client from esphome.components import airthings_wave_base
from esphome.const import ( from esphome.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
STATE_CLASS_MEASUREMENT,
UNIT_PERCENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
CONF_ID, CONF_ID,
CONF_HUMIDITY,
CONF_TVOC,
CONF_PRESSURE,
CONF_TEMPERATURE,
UNIT_PARTS_PER_BILLION,
ICON_RADIATOR,
) )
DEPENDENCIES = ["ble_client"] DEPENDENCIES = airthings_wave_base.DEPENDENCIES
AUTO_LOAD = ["airthings_wave_base"]
airthings_wave_mini_ns = cg.esphome_ns.namespace("airthings_wave_mini") airthings_wave_mini_ns = cg.esphome_ns.namespace("airthings_wave_mini")
AirthingsWaveMini = airthings_wave_mini_ns.class_( AirthingsWaveMini = airthings_wave_mini_ns.class_(
"AirthingsWaveMini", cg.PollingComponent, ble_client.BLEClientNode "AirthingsWaveMini", airthings_wave_base.AirthingsWaveBase
) )
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
cv.Schema( {
{ cv.GenerateID(): cv.declare_id(AirthingsWaveMini),
cv.GenerateID(): cv.declare_id(AirthingsWaveMini), }
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=2,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("5min"))
.extend(ble_client.BLE_CLIENT_SCHEMA),
) )
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await airthings_wave_base.wave_base_to_code(var, config)
await ble_client.register_ble_node(var, config)
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity(sens))
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature(sens))
if CONF_PRESSURE in config:
sens = await sensor.new_sensor(config[CONF_PRESSURE])
cg.add(var.set_pressure(sens))
if CONF_TVOC in config:
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))

View file

@ -7,55 +7,7 @@ namespace airthings_wave_plus {
static const char *const TAG = "airthings_wave_plus"; static const char *const TAG = "airthings_wave_plus";
void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) {
esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_OPEN_EVT: {
if (param->open.status == ESP_GATT_OK) {
ESP_LOGI(TAG, "Connected successfully!");
}
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
ESP_LOGW(TAG, "Disconnected!");
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
this->handle_ = 0;
auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_);
if (chr == nullptr) {
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(),
sensors_data_characteristic_uuid_.to_string().c_str());
break;
}
this->handle_ = chr->handle;
this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;
request_read_values_();
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->parent()->get_conn_id())
break;
if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
break;
}
if (param->read.handle == this->handle_) {
read_sensors_(param->read.value, param->read.value_len);
}
break;
}
default:
break;
}
}
void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
auto *value = (WavePlusReadings *) raw_value; auto *value = (WavePlusReadings *) raw_value;
if (sizeof(WavePlusReadings) <= value_len) { if (sizeof(WavePlusReadings) <= value_len) {
@ -64,26 +16,38 @@ void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
if (value->version == 1) { if (value->version == 1) {
ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); ESP_LOGD(TAG, "ambient light = %d", value->ambientLight);
this->humidity_sensor_->publish_state(value->humidity / 2.0f); if (this->humidity_sensor_ != nullptr) {
if (is_valid_radon_value_(value->radon)) { this->humidity_sensor_->publish_state(value->humidity / 2.0f);
}
if ((this->radon_sensor_ != nullptr) && this->is_valid_radon_value_(value->radon)) {
this->radon_sensor_->publish_state(value->radon); this->radon_sensor_->publish_state(value->radon);
} }
if (is_valid_radon_value_(value->radon_lt)) {
if ((this->radon_long_term_sensor_ != nullptr) && this->is_valid_radon_value_(value->radon_lt)) {
this->radon_long_term_sensor_->publish_state(value->radon_lt); this->radon_long_term_sensor_->publish_state(value->radon_lt);
} }
this->temperature_sensor_->publish_state(value->temperature / 100.0f);
this->pressure_sensor_->publish_state(value->pressure / 50.0f); if (this->temperature_sensor_ != nullptr) {
if (is_valid_co2_value_(value->co2)) { this->temperature_sensor_->publish_state(value->temperature / 100.0f);
}
if (this->pressure_sensor_ != nullptr) {
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
}
if ((this->co2_sensor_ != nullptr) && this->is_valid_co2_value_(value->co2)) {
this->co2_sensor_->publish_state(value->co2); this->co2_sensor_->publish_state(value->co2);
} }
if (is_valid_voc_value_(value->voc)) {
if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
this->tvoc_sensor_->publish_state(value->voc); this->tvoc_sensor_->publish_state(value->voc);
} }
// This instance must not stay connected // This instance must not stay connected
// so other clients can connect to it (e.g. the // so other clients can connect to it (e.g. the
// mobile app). // mobile app).
parent()->set_enabled(false); this->parent()->set_enabled(false);
} else { } else {
ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version);
} }
@ -92,44 +56,26 @@ void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; } bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; }
bool AirthingsWavePlus::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; }
bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; } bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; }
void AirthingsWavePlus::update() {
if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) {
if (!parent()->enabled) {
ESP_LOGW(TAG, "Reconnecting to device");
parent()->set_enabled(true);
parent()->connect();
} else {
ESP_LOGW(TAG, "Connection in progress");
}
}
}
void AirthingsWavePlus::request_read_values_() {
auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_,
ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
}
}
void AirthingsWavePlus::dump_config() { void AirthingsWavePlus::dump_config() {
// these really don't belong here, but there doesn't seem to be a
// practical way to have the base class use LOG_SENSOR and include
// the TAG from this component
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
LOG_SENSOR(" ", "Radon", this->radon_sensor_);
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
LOG_SENSOR(" ", "Radon", this->radon_sensor_);
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
} }
AirthingsWavePlus::AirthingsWavePlus() AirthingsWavePlus::AirthingsWavePlus() {
: PollingComponent(10000), this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID);
service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} }
} // namespace airthings_wave_plus } // namespace airthings_wave_plus
} // namespace esphome } // namespace esphome

View file

@ -2,14 +2,7 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_gattc_api.h> #include "esphome/components/airthings_wave_base/airthings_wave_base.h"
#include <algorithm>
#include <iterator>
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"
namespace esphome { namespace esphome {
namespace airthings_wave_plus { namespace airthings_wave_plus {
@ -17,43 +10,25 @@ namespace airthings_wave_plus {
static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba"; static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba";
static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba"; static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba";
class AirthingsWavePlus : public PollingComponent, public ble_client::BLEClientNode { class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
public: public:
AirthingsWavePlus(); AirthingsWavePlus();
void dump_config() override; void dump_config() override;
void update() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; }
void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
protected: protected:
bool is_valid_radon_value_(uint16_t radon); bool is_valid_radon_value_(uint16_t radon);
bool is_valid_voc_value_(uint16_t voc);
bool is_valid_co2_value_(uint16_t co2); bool is_valid_co2_value_(uint16_t co2);
void read_sensors_(uint8_t *value, uint16_t value_len); void read_sensors(uint8_t *value, uint16_t value_len) override;
void request_read_values_();
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *radon_sensor_{nullptr}; sensor::Sensor *radon_sensor_{nullptr};
sensor::Sensor *radon_long_term_sensor_{nullptr}; sensor::Sensor *radon_long_term_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *co2_sensor_{nullptr};
sensor::Sensor *tvoc_sensor_{nullptr};
uint16_t handle_;
esp32_ble_tracker::ESPBTUUID service_uuid_;
esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_;
struct WavePlusReadings { struct WavePlusReadings {
uint8_t version; uint8_t version;

View file

@ -1,116 +1,64 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import sensor, ble_client from esphome.components import sensor, airthings_wave_base
from esphome.const import ( from esphome.const import (
DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_PERCENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
ICON_RADIOACTIVE, ICON_RADIOACTIVE,
CONF_ID, CONF_ID,
CONF_RADON, CONF_RADON,
CONF_RADON_LONG_TERM, CONF_RADON_LONG_TERM,
CONF_HUMIDITY,
CONF_TVOC,
CONF_CO2, CONF_CO2,
CONF_PRESSURE,
CONF_TEMPERATURE,
UNIT_BECQUEREL_PER_CUBIC_METER, UNIT_BECQUEREL_PER_CUBIC_METER,
UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_MILLION,
UNIT_PARTS_PER_BILLION,
ICON_RADIATOR,
) )
DEPENDENCIES = ["ble_client"] DEPENDENCIES = airthings_wave_base.DEPENDENCIES
AUTO_LOAD = ["airthings_wave_base"]
airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus") airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus")
AirthingsWavePlus = airthings_wave_plus_ns.class_( AirthingsWavePlus = airthings_wave_plus_ns.class_(
"AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode "AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase
) )
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
cv.Schema( {
{ cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
cv.GenerateID(): cv.declare_id(AirthingsWavePlus), cv.Optional(CONF_RADON): sensor.sensor_schema(
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
unit_of_measurement=UNIT_PERCENT, icon=ICON_RADIOACTIVE,
device_class=DEVICE_CLASS_HUMIDITY, accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=0, ),
), cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
cv.Optional(CONF_RADON): sensor.sensor_schema( unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, icon=ICON_RADIOACTIVE,
icon=ICON_RADIOACTIVE, accuracy_decimals=0,
accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT,
state_class=STATE_CLASS_MEASUREMENT, ),
), cv.Optional(CONF_CO2): sensor.sensor_schema(
cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION,
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, accuracy_decimals=0,
icon=ICON_RADIOACTIVE, device_class=DEVICE_CLASS_CARBON_DIOXIDE,
accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT,
state_class=STATE_CLASS_MEASUREMENT, ),
), }
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("5min"))
.extend(ble_client.BLE_CLIENT_SCHEMA),
) )
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await airthings_wave_base.wave_base_to_code(var, config)
await ble_client.register_ble_node(var, config)
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity(sens))
if CONF_RADON in config: if CONF_RADON in config:
sens = await sensor.new_sensor(config[CONF_RADON]) sens = await sensor.new_sensor(config[CONF_RADON])
cg.add(var.set_radon(sens)) cg.add(var.set_radon(sens))
if CONF_RADON_LONG_TERM in config: if CONF_RADON_LONG_TERM in config:
sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM])
cg.add(var.set_radon_long_term(sens)) cg.add(var.set_radon_long_term(sens))
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature(sens))
if CONF_PRESSURE in config:
sens = await sensor.new_sensor(config[CONF_PRESSURE])
cg.add(var.set_pressure(sens))
if CONF_CO2 in config: if CONF_CO2 in config:
sens = await sensor.new_sensor(config[CONF_CO2]) sens = await sensor.new_sensor(config[CONF_CO2])
cg.add(var.set_co2(sens)) cg.add(var.set_co2(sens))
if CONF_TVOC in config:
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))

View file

@ -1,6 +1,6 @@
import logging import logging
from esphome import core from esphome import automation, core
from esphome.components import display, font from esphome.components import display, font
import esphome.components.image as espImage import esphome.components.image as espImage
from esphome.components.image import CONF_USE_TRANSPARENCY from esphome.components.image import CONF_USE_TRANSPARENCY
@ -18,15 +18,28 @@ from esphome.core import CORE, HexInt
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@syndlex"]
DEPENDENCIES = ["display"] DEPENDENCIES = ["display"]
MULTI_CONF = True MULTI_CONF = True
CONF_LOOP = "loop" CONF_LOOP = "loop"
CONF_START_FRAME = "start_frame" CONF_START_FRAME = "start_frame"
CONF_END_FRAME = "end_frame" CONF_END_FRAME = "end_frame"
CONF_FRAME = "frame"
Animation_ = display.display_ns.class_("Animation", espImage.Image_) Animation_ = display.display_ns.class_("Animation", espImage.Image_)
# Actions
NextFrameAction = display.display_ns.class_(
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
)
PrevFrameAction = display.display_ns.class_(
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
)
SetFrameAction = display.display_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
)
def validate_cross_dependencies(config): def validate_cross_dependencies(config):
""" """
@ -74,7 +87,35 @@ ANIMATION_SCHEMA = cv.Schema(
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA) CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA)
CODEOWNERS = ["@syndlex"] NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
PREV_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
SET_FRAME_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(Animation_),
cv.Required(CONF_FRAME): cv.uint16_t,
}
)
@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA)
@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA)
@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA)
async def animation_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_FRAME in config:
template_ = await cg.templatable(config[CONF_FRAME], args, cg.uint16)
cg.add(var.set_frame(template_))
return var
async def to_code(config): async def to_code(config):

View file

@ -94,3 +94,5 @@ async def to_code(config):
sens = await sensor.new_sensor(conf) sens = await sensor.new_sensor(conf)
cg.add(var.set_pressure_sensor(sens)) cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING])) cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING]))
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))

View file

@ -6,102 +6,100 @@ namespace esphome {
namespace captive_portal { namespace captive_portal {
const uint8_t INDEX_GZ[] PROGMEM = { const uint8_t INDEX_GZ[] PROGMEM = {
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xdd, 0x58, 0x09, 0x6f, 0xdc, 0x36, 0x16, 0xfe, 0x2b, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xdd, 0x58, 0x5b, 0x8f, 0xdb, 0x36, 0x16, 0x7e, 0xef,
0xac, 0x92, 0x74, 0x34, 0x8d, 0xc5, 0xd1, 0x31, 0x97, 0x35, 0xd2, 0x14, 0x89, 0x37, 0x45, 0x0b, 0x24, 0x69, 0x00, 0xaf, 0xe0, 0x2a, 0x49, 0x2d, 0x37, 0x23, 0xea, 0x66, 0xf9, 0x2a, 0xa9, 0x48, 0xb2, 0x29, 0x5a, 0x20, 0x69, 0x03,
0xbb, 0x5d, 0x14, 0x69, 0x00, 0x73, 0x24, 0x6a, 0xc4, 0x58, 0xa2, 0x54, 0x91, 0x9a, 0x23, 0x83, 0xd9, 0xdf, 0xde, 0xcc, 0xb4, 0xfb, 0x10, 0x04, 0x18, 0x5a, 0xa2, 0x2c, 0x66, 0x24, 0x4a, 0x15, 0xe9, 0x5b, 0x0c, 0xef, 0x6f, 0xdf,
0x47, 0x52, 0x73, 0x38, 0x6b, 0x2f, 0x90, 0x62, 0x8b, 0xa2, 0x4d, 0x6c, 0x9a, 0xc7, 0x3b, 0x3f, 0xf2, 0xf1, 0x3d, 0x43, 0x52, 0xf6, 0x38, 0xb3, 0x99, 0x05, 0x52, 0xec, 0x62, 0xd1, 0x4e, 0x26, 0x1c, 0x92, 0x3a, 0xd7, 0x4f, 0x3c,
0x2a, 0xfa, 0x2a, 0xad, 0x12, 0xb9, 0xad, 0x29, 0xca, 0x65, 0x59, 0xcc, 0x23, 0xd5, 0xa2, 0x82, 0xf0, 0x65, 0x4c, 0x17, 0x2a, 0xfe, 0x5b, 0xde, 0x64, 0x72, 0xdf, 0x52, 0x54, 0xca, 0xba, 0x4a, 0x63, 0x35, 0xa2, 0x8a, 0xf0, 0x55,
0x39, 0x8c, 0x28, 0x49, 0xe7, 0x51, 0x49, 0x25, 0x41, 0x49, 0x4e, 0x1a, 0x41, 0x65, 0xfc, 0xd3, 0xcd, 0x77, 0xce, 0x42, 0x39, 0xac, 0x28, 0xc9, 0xd3, 0xb8, 0xa6, 0x92, 0xa0, 0xac, 0x24, 0x9d, 0xa0, 0x32, 0xf9, 0xf5, 0xe6, 0x07,
0x14, 0x0d, 0xe6, 0x51, 0xc1, 0xf8, 0x1d, 0x6a, 0x68, 0x11, 0xb3, 0xa4, 0xe2, 0x28, 0x6f, 0x68, 0x16, 0xa7, 0x44, 0x67, 0x8a, 0xdc, 0x34, 0xae, 0x18, 0xbf, 0x43, 0x1d, 0xad, 0x12, 0x96, 0x35, 0x1c, 0x95, 0x1d, 0x2d, 0x92, 0x9c,
0x92, 0x90, 0x95, 0x64, 0x49, 0x15, 0x81, 0x66, 0xe3, 0xa4, 0xa4, 0xf1, 0x8a, 0xd1, 0x75, 0x5d, 0x35, 0x12, 0x01, 0x48, 0x32, 0x67, 0x35, 0x59, 0x51, 0x45, 0xa0, 0xd9, 0x38, 0xa9, 0x69, 0xb2, 0x61, 0x74, 0xdb, 0x36, 0x9d, 0x44,
0xa5, 0xa4, 0x5c, 0xc6, 0xd6, 0x9a, 0xa5, 0x32, 0x8f, 0x53, 0xba, 0x62, 0x09, 0x75, 0xf4, 0xe0, 0x82, 0x71, 0x26, 0x40, 0x29, 0x29, 0x97, 0x89, 0xb5, 0x65, 0xb9, 0x2c, 0x93, 0x9c, 0x6e, 0x58, 0x46, 0x1d, 0xbd, 0xb8, 0x62, 0x9c,
0x19, 0x29, 0x1c, 0x91, 0x90, 0x82, 0xc6, 0xde, 0x45, 0x2b, 0x68, 0xa3, 0x07, 0x64, 0x01, 0x63, 0x5e, 0x59, 0x20, 0x49, 0x46, 0x2a, 0x47, 0x64, 0xa4, 0xa2, 0x89, 0x7f, 0xb5, 0x16, 0xb4, 0xd3, 0x0b, 0xb2, 0x84, 0x35, 0x6f, 0x2c,
0x52, 0x24, 0x0d, 0xab, 0x25, 0x52, 0xf6, 0xc6, 0x65, 0x95, 0xb6, 0x05, 0x9d, 0x67, 0x2d, 0x4f, 0x24, 0x03, 0x0b, 0x10, 0x29, 0xb2, 0x8e, 0xb5, 0x12, 0x29, 0x7b, 0x93, 0xba, 0xc9, 0xd7, 0x15, 0x4d, 0x5d, 0x97, 0x08, 0xb0, 0x4b,
0x84, 0xcd, 0xfb, 0xbb, 0x82, 0x4a, 0x44, 0xe3, 0x37, 0x44, 0xe6, 0xb8, 0x24, 0x1b, 0xdb, 0x74, 0x18, 0xb7, 0xfd, 0xb8, 0x8c, 0xe7, 0x74, 0x87, 0xa7, 0xb3, 0x68, 0x32, 0x9e, 0xe6, 0x13, 0xfc, 0x51, 0x7c, 0x03, 0x9e, 0xad, 0x6b,
0x6f, 0x6c, 0xfe, 0xdc, 0x73, 0xdd, 0xfe, 0x85, 0x6e, 0xdc, 0xfe, 0x00, 0xfe, 0xce, 0x1a, 0x2a, 0xdb, 0x86, 0x23, 0x50, 0x87, 0xab, 0x26, 0x23, 0x92, 0x35, 0x1c, 0x0b, 0x4a, 0xba, 0xac, 0x4c, 0x92, 0xc4, 0xfa, 0x5e, 0x90, 0x0d,
0x62, 0xdf, 0x46, 0x35, 0x50, 0xa2, 0x34, 0xb6, 0x4a, 0xcf, 0xc7, 0xae, 0x3b, 0x45, 0xde, 0x25, 0xf6, 0x47, 0x8e, 0xb5, 0xbe, 0xfd, 0xd6, 0x3e, 0x13, 0xad, 0xa8, 0x7c, 0x5d, 0x51, 0x35, 0x15, 0x2f, 0xf7, 0x37, 0x64, 0xf5, 0x33,
0xe7, 0xe1, 0xc0, 0xf1, 0x46, 0xc9, 0xc4, 0x19, 0x21, 0x6f, 0x08, 0x8d, 0xef, 0xe3, 0x11, 0x72, 0x3f, 0x59, 0x28, 0x58, 0x6e, 0x5b, 0x44, 0xb0, 0x9c, 0x5a, 0xc3, 0xf7, 0xde, 0x07, 0x2c, 0xe4, 0xbe, 0xa2, 0x38, 0x67, 0xa2, 0xad,
0x63, 0x45, 0x11, 0x5b, 0xbc, 0xe2, 0xd4, 0x42, 0x42, 0x36, 0xd5, 0x1d, 0x8d, 0xad, 0xa4, 0x6d, 0x1a, 0xf0, 0xee, 0xc8, 0x3e, 0xb1, 0x96, 0x20, 0xf5, 0xce, 0x1a, 0x2e, 0x8a, 0x35, 0xcf, 0x94, 0x70, 0x24, 0x6c, 0x3a, 0x3c, 0x54,
0xaa, 0x2a, 0xaa, 0x06, 0xac, 0xfd, 0x95, 0xa3, 0x7b, 0xff, 0xbe, 0x58, 0x87, 0x6c, 0x08, 0x17, 0x59, 0xd5, 0x94, 0x14, 0xcc, 0x4b, 0xde, 0x12, 0x59, 0xe2, 0x9a, 0xec, 0x6c, 0x33, 0x61, 0xdc, 0x0e, 0xbe, 0xb3, 0xe9, 0x73, 0xdf,
0xb1, 0xa5, 0x41, 0xb1, 0x9f, 0xee, 0xe8, 0x1e, 0xa9, 0xa6, 0x7f, 0xb6, 0xe8, 0x54, 0x0d, 0x5b, 0x32, 0x1e, 0x5b, 0xf3, 0x86, 0x57, 0x7a, 0xf0, 0x86, 0x2e, 0xfc, 0x5d, 0x74, 0x54, 0xae, 0x3b, 0x8e, 0x88, 0x7d, 0x1b, 0xb7, 0x40,
0x9e, 0x8f, 0xbc, 0x29, 0xe8, 0xbd, 0xed, 0xef, 0x8f, 0xa0, 0x10, 0x05, 0x4a, 0xe7, 0x66, 0x65, 0xbf, 0xbf, 0x8d, 0x89, 0xf2, 0xc4, 0xaa, 0xfd, 0x00, 0x7b, 0xde, 0x14, 0xf9, 0x33, 0x1c, 0x44, 0x8e, 0xef, 0xe3, 0xd0, 0xf1, 0xa3,
0xc4, 0x6a, 0x89, 0x36, 0x65, 0xc1, 0x45, 0x6c, 0xe5, 0x52, 0xd6, 0xe1, 0x60, 0xb0, 0x5e, 0xaf, 0xf1, 0x3a, 0xc0, 0x6c, 0xe2, 0x44, 0xc8, 0x1f, 0xc1, 0x10, 0x04, 0x38, 0x42, 0xde, 0x27, 0x0b, 0x15, 0xac, 0xaa, 0x12, 0x8b, 0x37,
0x55, 0xb3, 0x1c, 0xf8, 0xae, 0xeb, 0x0e, 0x80, 0xc2, 0x42, 0x66, 0x7f, 0x2c, 0x7f, 0x68, 0xa1, 0x9c, 0xb2, 0x65, 0x9c, 0x5a, 0x48, 0xc8, 0xae, 0xb9, 0xa3, 0x89, 0x95, 0xad, 0xbb, 0x0e, 0xec, 0x7f, 0xd5, 0x54, 0x4d, 0x07, 0x70,
0x2e, 0x75, 0x7f, 0xfe, 0x74, 0xc7, 0xf7, 0x91, 0xa2, 0x98, 0xdf, 0x7e, 0x38, 0xd3, 0xd2, 0x9c, 0x69, 0xe1, 0xdf, 0x7d, 0x83, 0x3e, 0xfb, 0xf9, 0x6a, 0x15, 0xb2, 0x23, 0x5c, 0x14, 0x4d, 0x57, 0x27, 0x96, 0x7e, 0x29, 0xf6, 0xd3,
0x12, 0xdb, 0x3a, 0xb8, 0xda, 0x7b, 0xa3, 0x8c, 0x9a, 0x10, 0x1f, 0xf9, 0xc8, 0xd5, 0xff, 0x7d, 0x47, 0xf5, 0xbb, 0x83, 0x3c, 0x22, 0x35, 0x0c, 0x2f, 0x1e, 0x3a, 0x4d, 0xc7, 0x56, 0x8c, 0x27, 0x96, 0x1f, 0x20, 0x7f, 0x0a, 0x6a,
0x91, 0xf3, 0xd9, 0x08, 0x9d, 0x8d, 0xe0, 0xaf, 0x02, 0xd0, 0x2f, 0xc7, 0xce, 0xe5, 0x91, 0xdf, 0x53, 0xeb, 0x2b, 0x6f, 0x87, 0xc7, 0x33, 0x26, 0x44, 0x61, 0xd2, 0x7b, 0xd9, 0xd8, 0xef, 0x6f, 0x63, 0xb1, 0x59, 0xa1, 0x5d, 0x5d,
0xcf, 0x3d, 0x4d, 0x28, 0xa6, 0xef, 0xc7, 0xe7, 0x63, 0xc7, 0xff, 0x59, 0x11, 0x68, 0xf4, 0x8f, 0x5c, 0x8e, 0x9f, 0x71, 0x91, 0x58, 0xa5, 0x94, 0xed, 0xdc, 0x75, 0xb7, 0xdb, 0x2d, 0xde, 0x86, 0xb8, 0xe9, 0x56, 0x6e, 0xe0, 0x79,
0x7b, 0x3f, 0x8f, 0xc9, 0x08, 0x8d, 0xba, 0x99, 0x91, 0xa3, 0xfa, 0xc7, 0x91, 0xd6, 0x85, 0x46, 0x2b, 0x20, 0x2b, 0x9e, 0x0b, 0x14, 0x16, 0x32, 0xe7, 0xc3, 0x0a, 0x46, 0x16, 0x2a, 0x29, 0x5b, 0x95, 0x52, 0xcf, 0xd3, 0xa7, 0x07,
0x9d, 0xb1, 0x33, 0x22, 0x01, 0x0a, 0x3a, 0xab, 0xa0, 0x07, 0xd3, 0x63, 0xe0, 0x3e, 0x9b, 0x73, 0x82, 0x4f, 0xbd, 0x7a, 0x8c, 0x15, 0x45, 0x7a, 0xfb, 0xe1, 0x42, 0x4b, 0x77, 0xa1, 0x85, 0x7e, 0x7f, 0x81, 0xe6, 0xe0, 0xad, 0x32,
0xc1, 0xdc, 0xea, 0x87, 0x96, 0x75, 0x82, 0xa1, 0x3a, 0x87, 0x01, 0x7f, 0xac, 0xe0, 0xdc, 0x59, 0x56, 0x7f, 0x6f, 0x6a, 0x42, 0x02, 0x14, 0x20, 0x4f, 0xff, 0x0b, 0x1c, 0x35, 0xef, 0x57, 0xce, 0x83, 0x15, 0xba, 0x58, 0xc1, 0x5f,
0x7d, 0x2b, 0xc8, 0x8a, 0x5a, 0x71, 0x1c, 0x43, 0xa8, 0xb5, 0x25, 0x9c, 0x10, 0x5c, 0x54, 0x09, 0x51, 0x2c, 0x58, 0xc0, 0x2f, 0xa8, 0xc7, 0xce, 0xec, 0xcc, 0xee, 0xab, 0xc7, 0x1b, 0xdf, 0xbb, 0xdf, 0x50, 0x3c, 0x3f, 0x8e, 0x2f,
0x50, 0xd2, 0x24, 0xf9, 0xd7, 0x5f, 0xdb, 0xc7, 0xa5, 0x25, 0x95, 0xaf, 0x0a, 0xaa, 0xba, 0xe2, 0xe5, 0xf6, 0x86, 0xd7, 0x4e, 0xf0, 0x9b, 0x22, 0x50, 0xd8, 0x9f, 0x99, 0x9c, 0xa0, 0xf4, 0x7f, 0x1b, 0x93, 0x08, 0x45, 0xfd, 0x4e,
0x2c, 0xdf, 0x42, 0x00, 0xd9, 0x16, 0x11, 0x2c, 0xa5, 0x56, 0xff, 0xbd, 0xfb, 0x01, 0x0b, 0xb9, 0x2d, 0x28, 0x4e, 0xe4, 0xa8, 0xf9, 0x79, 0xa5, 0x34, 0xa1, 0x68, 0x03, 0x54, 0xb5, 0x33, 0x76, 0x22, 0x12, 0xa2, 0xb0, 0x37, 0x09,
0x99, 0xa8, 0x0b, 0xb2, 0x8d, 0xad, 0x05, 0xc8, 0xba, 0xb3, 0xfa, 0x17, 0x19, 0x95, 0x49, 0x6e, 0x5b, 0x03, 0x08, 0x66, 0xb0, 0x3d, 0x06, 0xe6, 0x8b, 0x3d, 0x27, 0xfc, 0x34, 0x50, 0x30, 0xcf, 0x2d, 0xeb, 0x1e, 0x83, 0xe6, 0x12,
0xb1, 0x8c, 0x2d, 0xf1, 0x47, 0x51, 0x71, 0xab, 0x8f, 0x65, 0x4e, 0xb9, 0x6d, 0x1f, 0x2c, 0x54, 0xf6, 0x71, 0xbd, 0x03, 0xfc, 0xb1, 0x81, 0x33, 0x67, 0x59, 0x80, 0x11, 0x95, 0x59, 0x69, 0x5b, 0x2e, 0x44, 0x5e, 0xc1, 0x56, 0x10,
0x64, 0x3f, 0xb4, 0x74, 0xb4, 0x41, 0x32, 0xa9, 0x42, 0x0e, 0xab, 0xe0, 0xbd, 0x38, 0xce, 0x2e, 0xaa, 0x74, 0xfb, 0x15, 0x0d, 0xb7, 0x86, 0x58, 0x96, 0x94, 0xdb, 0x27, 0x56, 0xc5, 0x48, 0xf5, 0x13, 0xfb, 0xe1, 0x13, 0x39, 0x3c,
0x88, 0x79, 0xb9, 0x67, 0x6c, 0x63, 0x9c, 0xd3, 0xe6, 0x86, 0x6e, 0xe0, 0xb8, 0xfc, 0x9b, 0x7d, 0xc7, 0xd0, 0x5b, 0x9c, 0xe3, 0x43, 0x32, 0x09, 0x71, 0x28, 0xb1, 0x8a, 0xe8, 0xab, 0xf3, 0xee, 0xb2, 0xc9, 0xf7, 0x8f, 0x84, 0x4e,
0x2a, 0xd7, 0x55, 0x73, 0x27, 0x42, 0x64, 0x3d, 0x37, 0xe2, 0x66, 0x26, 0x42, 0x39, 0x26, 0xb5, 0xc0, 0xa2, 0x80, 0xe9, 0x9b, 0xb8, 0x61, 0x9c, 0xd3, 0xee, 0x86, 0xee, 0xe0, 0x1d, 0xfe, 0x83, 0xfd, 0xc0, 0xd0, 0xcf, 0x54, 0x6e,
0xf0, 0xb7, 0xbd, 0x3e, 0xc4, 0x6a, 0x7d, 0xdf, 0x14, 0x83, 0xe2, 0x6d, 0x94, 0xb2, 0x15, 0x4a, 0x0a, 0x22, 0xe0, 0x9b, 0xee, 0x4e, 0xcc, 0x91, 0xf5, 0xdc, 0x88, 0x5b, 0xa8, 0xa8, 0x61, 0x20, 0x9b, 0xb4, 0x02, 0x8b, 0x0a, 0x72,
0xb8, 0x72, 0x23, 0xcb, 0x42, 0x87, 0xb8, 0xaa, 0x78, 0x02, 0xfc, 0x77, 0xb1, 0xf5, 0x00, 0x76, 0x2f, 0xb7, 0x3f, 0x82, 0xed, 0x0f, 0x21, 0x7e, 0xda, 0x7b, 0x4b, 0xf8, 0xc9, 0xb9, 0xdb, 0x38, 0x67, 0x1b, 0x94, 0x55, 0x10, 0xf5,
0xa4, 0x76, 0x4f, 0x00, 0x6a, 0xbd, 0x3e, 0x5e, 0x91, 0xa2, 0xa5, 0x28, 0x46, 0x32, 0x67, 0xe2, 0x64, 0xe2, 0xec, 0x70, 0xfc, 0x8d, 0x28, 0x0b, 0xf5, 0x47, 0xbd, 0xe1, 0x19, 0x70, 0xdf, 0x25, 0xd6, 0x17, 0xa2, 0xfa, 0xe5, 0xfe,
0x51, 0xb6, 0x5a, 0xdc, 0x01, 0x57, 0x06, 0xcb, 0xc2, 0xee, 0x5b, 0xc7, 0x38, 0x8e, 0x88, 0xb9, 0xe5, 0xac, 0x27, 0xa7, 0xdc, 0x1e, 0x08, 0x88, 0xe7, 0xc1, 0x10, 0x6f, 0x48, 0xb5, 0xa6, 0x28, 0x41, 0xb2, 0x64, 0xe2, 0xde, 0xc0,
0xd6, 0x67, 0x36, 0x39, 0x05, 0xcd, 0xa4, 0x75, 0x16, 0xf0, 0x4f, 0x77, 0x70, 0x1b, 0xe1, 0x06, 0xf4, 0xf7, 0xf7, 0xc5, 0xa3, 0x6c, 0xad, 0xb8, 0x03, 0xae, 0x02, 0x1e, 0x0b, 0x7b, 0x68, 0x9d, 0x22, 0x2b, 0x26, 0x26, 0xef, 0x59,
0xa7, 0xd9, 0x48, 0xd4, 0x84, 0x7f, 0xce, 0xaa, 0x6c, 0xd4, 0x81, 0x85, 0x55, 0x4f, 0x45, 0x17, 0x10, 0x9d, 0x74, 0x4f, 0xac, 0x07, 0x16, 0x39, 0x15, 0x2d, 0xa4, 0x75, 0x1f, 0x81, 0x4f, 0x0f, 0xc2, 0xe6, 0xb8, 0x03, 0xed, 0xc3,
0x0e, 0xc8, 0xb1, 0xff, 0x74, 0x07, 0x71, 0xa6, 0x8e, 0xce, 0xdd, 0x49, 0x68, 0x34, 0x00, 0x84, 0xe6, 0xb7, 0xfb, 0xe3, 0x79, 0x33, 0x16, 0x2d, 0xe1, 0x0f, 0x19, 0x95, 0x81, 0xea, 0xa0, 0x43, 0xb2, 0x82, 0x99, 0x3a, 0xed, 0x40,
0x7e, 0xff, 0xe4, 0xce, 0x6f, 0x2d, 0x6d, 0xb6, 0xd7, 0xb4, 0xa0, 0x89, 0xac, 0x1a, 0xdb, 0x7a, 0x02, 0x9a, 0xe0, 0x74, 0x56, 0xe8, 0x92, 0xd3, 0xf4, 0xe9, 0xa1, 0x03, 0x89, 0x2a, 0x07, 0x9d, 0x25, 0xc6, 0x2e, 0x40, 0x93, 0xde,
0x24, 0x68, 0xbf, 0xbf, 0xbf, 0x79, 0xf3, 0x3a, 0xae, 0x6c, 0xda, 0xbf, 0x78, 0x8c, 0x5a, 0xdd, 0xea, 0xef, 0xe1, 0x1e, 0x87, 0xf7, 0x7e, 0xfc, 0xbe, 0xa6, 0xdd, 0xfe, 0x9a, 0x56, 0x34, 0x93, 0x4d, 0x67, 0x5b, 0x4f, 0x40, 0x0b,
0x56, 0xff, 0x4f, 0xdc, 0x53, 0xf7, 0x7a, 0xef, 0x03, 0xb0, 0x1a, 0xaf, 0x4f, 0x97, 0xbb, 0xba, 0x00, 0x9e, 0xc3, 0xbc, 0x7e, 0xed, 0xf0, 0x8f, 0x37, 0x6f, 0xdf, 0x24, 0x8d, 0xcd, 0x86, 0x57, 0x8f, 0x51, 0xab, 0x0c, 0xff, 0x1e,
0x25, 0x72, 0x61, 0x3d, 0x17, 0xb6, 0x33, 0x1e, 0xf5, 0x41, 0x3d, 0xfc, 0x80, 0xe9, 0xfa, 0x7a, 0x86, 0x6b, 0x5a, 0x32, 0xfc, 0x3f, 0x93, 0x81, 0xca, 0xf1, 0x83, 0x0f, 0xc0, 0xaa, 0xfd, 0xbd, 0xbd, 0x4f, 0xf4, 0x2a, 0x18, 0x9f,
0x1d, 0xd1, 0xf9, 0x37, 0xbb, 0x45, 0xb5, 0x71, 0x04, 0xfb, 0xc4, 0xf8, 0x32, 0x64, 0x3c, 0xa7, 0x0d, 0x93, 0x7b, 0x43, 0x40, 0x5f, 0x29, 0x0f, 0x9d, 0x71, 0x34, 0x3c, 0x82, 0x7e, 0xb0, 0x00, 0xec, 0xd6, 0xb9, 0x1a, 0x72, 0xb6,
0x30, 0x17, 0x6e, 0xfa, 0xba, 0x95, 0xbb, 0x9a, 0xa4, 0xa9, 0x5a, 0x19, 0xd5, 0x9b, 0x59, 0x06, 0x79, 0x41, 0x51, 0x4a, 0x9b, 0xe9, 0x77, 0x87, 0x65, 0xb3, 0x73, 0x04, 0xfb, 0xc4, 0xf8, 0x6a, 0xce, 0x78, 0x49, 0x3b, 0x26, 0x8f,
0xd2, 0xd0, 0xa3, 0xe5, 0xde, 0xac, 0xeb, 0x2b, 0x28, 0xbc, 0x1c, 0x3d, 0xdb, 0xab, 0x83, 0xb7, 0x93, 0xb0, 0x65, 0x60, 0x2e, 0xa4, 0xfd, 0x76, 0x2d, 0x0f, 0x2d, 0xc9, 0x73, 0xf5, 0x24, 0x6a, 0x77, 0x8b, 0x02, 0x8a, 0x84, 0xa2,
0x0e, 0x29, 0xd8, 0x92, 0x87, 0x09, 0xd8, 0x4d, 0x1b, 0xc3, 0x94, 0x91, 0x92, 0x15, 0xdb, 0x50, 0xc0, 0x65, 0xe8, 0xa4, 0x73, 0x9f, 0xd6, 0x47, 0xf3, 0x5c, 0xe7, 0x83, 0xf9, 0x2c, 0x7a, 0x76, 0x54, 0x07, 0xee, 0x20, 0xe1, 0x65,
0x40, 0xc2, 0x60, 0xd9, 0x7e, 0xd1, 0x4a, 0x59, 0x71, 0xd0, 0xdd, 0xa4, 0xb4, 0x09, 0xdd, 0x99, 0xe9, 0x38, 0x0d, 0x39, 0xa4, 0x62, 0x2b, 0x3e, 0xcf, 0xc0, 0x70, 0xda, 0x19, 0xa6, 0x82, 0xd4, 0xac, 0xda, 0xcf, 0x05, 0x64, 0x26,
0x49, 0x59, 0x2b, 0x42, 0x1c, 0x34, 0xb4, 0x9c, 0x2d, 0x48, 0x72, 0xb7, 0x6c, 0xaa, 0x96, 0xa7, 0x4e, 0xa2, 0x6e, 0x07, 0xaa, 0x07, 0x2b, 0x8e, 0xcb, 0xb5, 0x94, 0x0d, 0x07, 0xdd, 0x5d, 0x4e, 0xbb, 0xb9, 0xb7, 0x30, 0x13, 0xa7,
0xeb, 0xf0, 0x89, 0x97, 0x91, 0x80, 0x26, 0xb3, 0x6e, 0x94, 0x65, 0xd9, 0x0c, 0x90, 0xa0, 0x8e, 0xb9, 0xfc, 0x42, 0x23, 0x39, 0x5b, 0x8b, 0x39, 0x0e, 0x3b, 0x5a, 0x2f, 0x96, 0x24, 0xbb, 0x5b, 0x75, 0xcd, 0x9a, 0xe7, 0x4e, 0xa6,
0x1f, 0x0f, 0x15, 0xdb, 0x99, 0x99, 0xd8, 0x57, 0x13, 0xc6, 0x46, 0x48, 0x25, 0xcf, 0x66, 0x07, 0x77, 0xdc, 0x19, 0x32, 0xe7, 0xfc, 0x89, 0x5f, 0x90, 0x90, 0x66, 0x8b, 0x7e, 0x55, 0x14, 0xc5, 0x02, 0xa0, 0xa0, 0x8e, 0xc9, 0x44,
0xa4, 0x01, 0x01, 0x42, 0x6a, 0x88, 0x7f, 0x30, 0x73, 0x5f, 0x12, 0xc6, 0xcf, 0xad, 0x57, 0x67, 0x65, 0xd6, 0x85, 0xf3, 0x00, 0x8f, 0x14, 0xdb, 0x85, 0x99, 0x38, 0x50, 0x1b, 0xc6, 0x46, 0x48, 0xeb, 0xcf, 0x16, 0x27, 0x77, 0xbc,
0x2f, 0xc0, 0xa2, 0xd5, 0xe8, 0x20, 0x9e, 0x41, 0xa2, 0x32, 0xb9, 0x30, 0xf4, 0xc7, 0x6e, 0xbd, 0xd9, 0xe3, 0xee, 0x05, 0xa4, 0x64, 0x01, 0x42, 0x5a, 0x88, 0x47, 0x30, 0xf3, 0x58, 0x13, 0xc6, 0x2f, 0xad, 0x57, 0xc7, 0x64, 0xd1,
0x8c, 0xec, 0x0e, 0xd4, 0x59, 0x41, 0x37, 0xb3, 0x8f, 0xad, 0x90, 0x2c, 0xdb, 0x3a, 0x5d, 0x2e, 0x0d, 0xe1, 0xbc, 0x97, 0x14, 0x80, 0x45, 0xab, 0xd1, 0x85, 0x65, 0x01, 0x45, 0xc3, 0x14, 0xc6, 0x79, 0x30, 0xf6, 0xda, 0xdd, 0x11,
0x40, 0x0e, 0x5d, 0x00, 0x29, 0xa5, 0x7c, 0xa6, 0x75, 0x38, 0x4c, 0xd2, 0x52, 0x74, 0x38, 0x1d, 0xc5, 0xe8, 0x53, 0xf7, 0x07, 0xe4, 0x70, 0xa2, 0x2e, 0x2a, 0xba, 0x5b, 0x7c, 0x5c, 0x0b, 0xc9, 0x8a, 0xbd, 0xd3, 0x17, 0xd6, 0x39,
0x7a, 0x5f, 0xd6, 0xff, 0xa2, 0x56, 0xc7, 0x71, 0x57, 0x92, 0x06, 0x72, 0x8b, 0xb3, 0xa8, 0x00, 0xd3, 0x32, 0x74, 0x1c, 0x16, 0x28, 0xa8, 0x4b, 0x20, 0xa5, 0x94, 0x2f, 0xb4, 0x0e, 0x87, 0x49, 0x5a, 0x8b, 0x1e, 0xa7, 0xb3, 0x18,
0x26, 0xb0, 0x57, 0xdd, 0x94, 0x12, 0x06, 0x9e, 0x83, 0x99, 0xfa, 0x6e, 0x3a, 0xe0, 0xed, 0xd5, 0x1b, 0x24, 0xaa, 0x7d, 0x40, 0x3f, 0x97, 0xf5, 0x9f, 0xa8, 0xd5, 0x59, 0x3c, 0xd4, 0xa4, 0x83, 0x44, 0xef, 0x2c, 0x1b, 0xc0, 0xb4,
0x82, 0xa5, 0x1d, 0x9d, 0x26, 0x41, 0xee, 0x11, 0x1e, 0x0f, 0xb6, 0x1b, 0xa9, 0xb9, 0x03, 0xd4, 0xc3, 0x6c, 0x4a, 0x9e, 0x3b, 0x13, 0x78, 0x57, 0xfd, 0x96, 0x12, 0x06, 0x9e, 0x83, 0x99, 0xba, 0x5e, 0x9e, 0xf0, 0xf6, 0xdb, 0x1d,
0x3c, 0xf7, 0x81, 0x1d, 0x49, 0xb3, 0xcc, 0x5f, 0x64, 0x47, 0xa4, 0x54, 0xaa, 0xdd, 0xb3, 0xee, 0x54, 0xf8, 0x43, 0x12, 0x4d, 0xc5, 0xf2, 0x9e, 0x4e, 0x93, 0x20, 0xef, 0x0c, 0x8f, 0x0f, 0xaf, 0x1b, 0xa9, 0xbd, 0x13, 0xd4, 0xa3,
0x10, 0x70, 0xd8, 0x1b, 0xe8, 0xef, 0x99, 0x8e, 0x8b, 0xdd, 0x99, 0x14, 0x7d, 0x52, 0xc3, 0xb6, 0x29, 0xec, 0x87, 0x62, 0x4a, 0x7c, 0xef, 0x0b, 0x6f, 0x24, 0x2f, 0x8a, 0x60, 0x59, 0x9c, 0x91, 0x52, 0x65, 0xef, 0xc8, 0xfa, 0x53,
0x4e, 0xee, 0xb3, 0xe0, 0xea, 0x94, 0x09, 0x7b, 0x8f, 0x67, 0xc2, 0x1e, 0x52, 0xb5, 0xcb, 0xcb, 0x6a, 0x13, 0xf7, 0x11, 0x8c, 0x40, 0xc0, 0xe9, 0xdd, 0xc0, 0xfc, 0xc8, 0x74, 0x58, 0x1c, 0x2e, 0xa4, 0xe8, 0xa3, 0x3a, 0x5f, 0x77,
0x74, 0x4e, 0x1a, 0xc2, 0x4f, 0xef, 0x59, 0xf0, 0x0a, 0xf8, 0xff, 0x2f, 0x29, 0xee, 0x0f, 0xa7, 0xb7, 0x2f, 0x48, 0x95, 0x6d, 0x7d, 0xe1, 0xe8, 0x3e, 0x0b, 0x5f, 0xdd, 0x97, 0xa5, 0xc1, 0xe3, 0x65, 0x69, 0x80, 0x54, 0x23, 0xf3,
0x6d, 0x5f, 0x98, 0xd5, 0x8c, 0x77, 0xca, 0x79, 0xe8, 0x41, 0xfa, 0x62, 0x58, 0xb0, 0xa5, 0xf7, 0x67, 0x40, 0xfb, 0xb2, 0xd9, 0x25, 0x03, 0x5d, 0x20, 0x46, 0xf0, 0x3b, 0x78, 0x16, 0xbe, 0x06, 0xfe, 0xff, 0x4a, 0xbd, 0xf9, 0xc3,
0xdf, 0x38, 0x06, 0x2f, 0xbc, 0x29, 0xbe, 0x44, 0xba, 0x31, 0x10, 0xe1, 0x60, 0x8a, 0x26, 0x57, 0x43, 0x3c, 0xf4, 0xc5, 0xe6, 0x2b, 0x2a, 0xcd, 0x57, 0x56, 0x19, 0xe3, 0x9d, 0x72, 0x1e, 0x66, 0x50, 0x4e, 0x18, 0x16, 0x6c, 0xe5,
0x90, 0xaa, 0x9a, 0xc6, 0x68, 0x82, 0xa7, 0x40, 0x30, 0xc6, 0xc1, 0x04, 0x26, 0x90, 0xef, 0xe1, 0xd1, 0x6b, 0x3f, 0xff, 0x2f, 0xa0, 0xfd, 0x77, 0x1c, 0xc3, 0x17, 0xfe, 0x14, 0xcf, 0x90, 0x1e, 0x0c, 0x44, 0x38, 0x9c, 0xa2, 0xc9,
0xc0, 0xe3, 0x11, 0x50, 0xf9, 0x2e, 0x0e, 0x7c, 0x64, 0x68, 0xc7, 0xd8, 0x07, 0x71, 0x8a, 0x24, 0x28, 0x01, 0xe8, 0xab, 0x11, 0x1e, 0xf9, 0x48, 0xb5, 0x30, 0x63, 0x34, 0x81, 0x7e, 0x0f, 0xf9, 0x63, 0x1c, 0x4e, 0x60, 0x03, 0x05,
0x24, 0xc0, 0xee, 0x04, 0xc4, 0x8d, 0xb1, 0x7b, 0x89, 0xa7, 0x63, 0x34, 0xc5, 0x13, 0x80, 0x0e, 0x0f, 0x47, 0x85, 0x3e, 0x8e, 0xde, 0x04, 0x21, 0x1e, 0x47, 0x40, 0x15, 0x78, 0x38, 0x0c, 0x90, 0xa1, 0x1d, 0xe3, 0x00, 0xc4, 0x29,
0x33, 0xc2, 0x1e, 0x4c, 0x07, 0x63, 0x32, 0xc5, 0xc3, 0x00, 0xe9, 0xc6, 0xc0, 0x31, 0x01, 0x11, 0x0e, 0x76, 0xbd, 0x92, 0xb0, 0x06, 0xa0, 0xb3, 0x10, 0x7b, 0x13, 0x10, 0x37, 0xc6, 0xde, 0x0c, 0x4f, 0xc7, 0x68, 0x8a, 0x27, 0x00,
0xd7, 0x01, 0xf6, 0x27, 0xa0, 0x77, 0x38, 0x7c, 0x01, 0x62, 0x2f, 0x87, 0xc8, 0xb4, 0x06, 0x5e, 0x50, 0x30, 0x7a, 0x1d, 0x1e, 0x45, 0x95, 0x13, 0x61, 0x1f, 0xb6, 0xc3, 0x31, 0x99, 0xe2, 0x51, 0x88, 0xf4, 0x60, 0xe0, 0x98, 0x80,
0x0c, 0x34, 0xff, 0x9f, 0x0b, 0x1a, 0x40, 0xe2, 0xa1, 0x00, 0x5f, 0x42, 0xec, 0x7a, 0x8a, 0xdf, 0xb4, 0x06, 0x37, 0x08, 0x07, 0x7b, 0xfe, 0x9b, 0x10, 0x07, 0x13, 0xd0, 0x3b, 0x1a, 0xbd, 0x00, 0xb1, 0xb3, 0x11, 0x32, 0xa3, 0x81,
0xcf, 0x43, 0xee, 0x1f, 0xc6, 0x2c, 0xf8, 0xe7, 0x62, 0xe6, 0x29, 0x04, 0xa0, 0x0b, 0xba, 0x41, 0x0e, 0xd2, 0x8d, 0x17, 0x14, 0x44, 0x8f, 0x81, 0x16, 0xfc, 0x75, 0x41, 0x03, 0x48, 0x7c, 0x14, 0xe2, 0x19, 0xc4, 0xae, 0xaf, 0xf8,
0xd1, 0x0d, 0xcc, 0xd3, 0xab, 0x4b, 0x34, 0x05, 0xae, 0xf1, 0x14, 0x5d, 0xa2, 0x91, 0x42, 0x17, 0xd8, 0x87, 0x86, 0xcd, 0x68, 0x70, 0xf3, 0x7d, 0xe4, 0xfd, 0x61, 0xcc, 0xc2, 0xbf, 0x2e, 0x66, 0xbe, 0x42, 0x00, 0xa6, 0xa0, 0x1b,
0xc9, 0x01, 0xa6, 0x2f, 0x84, 0x71, 0xf8, 0x37, 0x86, 0xf1, 0x31, 0x9f, 0xfe, 0xc6, 0x2e, 0xfd, 0x15, 0x57, 0x10, 0xe4, 0x20, 0x3d, 0x18, 0xdd, 0xc0, 0x3c, 0x7d, 0x35, 0x43, 0x53, 0xe0, 0x1a, 0x4f, 0xd1, 0x0c, 0x45, 0x0a, 0x5d,
0x94, 0x63, 0xba, 0x0c, 0x8b, 0x06, 0xe6, 0x15, 0xaf, 0xaa, 0x28, 0x78, 0x94, 0x43, 0x35, 0x02, 0xef, 0x7a, 0x0f, 0x60, 0x1f, 0x19, 0x26, 0x07, 0x98, 0xbe, 0x12, 0xc6, 0xd1, 0x9f, 0x18, 0xc6, 0xc7, 0x7c, 0xfa, 0x13, 0xbb, 0xf4,
0xb1, 0x34, 0xce, 0xbd, 0xf9, 0xbd, 0x2a, 0x1d, 0x28, 0xbd, 0x79, 0xa4, 0xd3, 0xf9, 0xfc, 0x26, 0xa7, 0xe8, 0xd5, 0xff, 0x48, 0x41, 0xd0, 0x8e, 0xe9, 0x36, 0x2c, 0x76, 0xcd, 0x95, 0x5e, 0x75, 0x51, 0x70, 0x43, 0x87, 0x6e, 0x04,
0xf5, 0x3b, 0x78, 0x08, 0x16, 0x05, 0xe2, 0xd5, 0x1a, 0xde, 0x9b, 0x5b, 0x24, 0x2b, 0xf5, 0x82, 0xe7, 0x50, 0x2a, 0x2e, 0xf9, 0x3e, 0x62, 0x79, 0x52, 0xfa, 0xe9, 0x67, 0xdd, 0x39, 0x50, 0xfa, 0x69, 0xac, 0xcb, 0x79, 0x7a, 0x53,
0xaa, 0x2e, 0x3c, 0x20, 0x50, 0x57, 0x2c, 0x60, 0x8c, 0xa3, 0x45, 0x33, 0x7f, 0x57, 0x50, 0x22, 0x28, 0x5a, 0xb2, 0x52, 0xf4, 0xfa, 0xfa, 0x1d, 0xdc, 0xca, 0xaa, 0x0a, 0xf1, 0x66, 0x0b, 0x97, 0xbf, 0x3d, 0x92, 0x8d, 0xba, 0xce,
0x15, 0x45, 0x4c, 0x42, 0x1d, 0x50, 0x52, 0x24, 0x99, 0x6a, 0x8e, 0x8c, 0x9a, 0xee, 0x6d, 0x25, 0x69, 0x88, 0xae, 0x73, 0xe8, 0x15, 0xd5, 0x14, 0xee, 0x0d, 0xa8, 0x6f, 0x16, 0x30, 0xc6, 0xf1, 0xb2, 0x4b, 0xdf, 0x55, 0x94, 0x08,
0xaa, 0x7a, 0xab, 0x85, 0x24, 0x39, 0xe1, 0x4b, 0x9a, 0x1e, 0x84, 0x29, 0xea, 0x6d, 0xd5, 0x36, 0xe8, 0x97, 0x17, 0x8a, 0x56, 0x6c, 0x43, 0x11, 0x93, 0xd0, 0x07, 0xd4, 0x14, 0x49, 0xa6, 0x86, 0x33, 0xa3, 0xa6, 0x83, 0x9e, 0x56,
0x6f, 0x5e, 0xab, 0x87, 0x36, 0x45, 0x4e, 0xa7, 0x6c, 0x23, 0xd1, 0x8f, 0x37, 0x2f, 0x50, 0x5b, 0xc3, 0xa6, 0x53, 0x2b, 0x31, 0xdd, 0x30, 0x58, 0x02, 0x62, 0xd2, 0xbe, 0xed, 0x8d, 0xcb, 0xd0, 0x58, 0x75, 0x4d, 0xa5, 0x84, 0x8e,
0x63, 0x5b, 0xb5, 0xa2, 0xcd, 0x1a, 0x2a, 0x4b, 0xaa, 0x48, 0x40, 0xb9, 0xa0, 0x52, 0x42, 0xa1, 0x21, 0x30, 0x94, 0x41, 0x59, 0x15, 0xa6, 0xb1, 0xba, 0x76, 0x22, 0xa2, 0x2f, 0x06, 0x89, 0xbb, 0x65, 0x05, 0x53, 0x97, 0xf9, 0x34,
0xce, 0xda, 0x13, 0x53, 0x75, 0x83, 0xbb, 0x20, 0x7e, 0xde, 0x95, 0xd7, 0x51, 0x1e, 0x18, 0xd7, 0xaf, 0x3b, 0x6a, 0xd6, 0xad, 0xa2, 0x92, 0xa0, 0xba, 0x15, 0xf3, 0xe5, 0x41, 0xcf, 0x2a, 0xca, 0x57, 0x70, 0x9b, 0x84, 0x77, 0x01,
0x70, 0x3d, 0x98, 0x47, 0xea, 0x39, 0x8d, 0x88, 0x7e, 0x84, 0xc4, 0x83, 0x35, 0xcb, 0x98, 0x7a, 0xb8, 0xcd, 0x23, 0xcd, 0x43, 0x46, 0xcb, 0xa6, 0x82, 0xe6, 0x24, 0xb9, 0xbe, 0xfe, 0xe9, 0xef, 0xea, 0x33, 0x85, 0x32, 0xe1, 0xcc,
0x5d, 0x8f, 0x2a, 0x09, 0xaa, 0x24, 0x32, 0x5f, 0x34, 0x74, 0xaf, 0xa0, 0x7c, 0x09, 0xaf, 0x64, 0xd8, 0x70, 0xa8, 0x09, 0x7d, 0xbe, 0x61, 0x54, 0x93, 0x9e, 0x6f, 0x3c, 0x32, 0x1f, 0x1c, 0x5a, 0xe8, 0xd3, 0xc1, 0xbf, 0xfc, 0x33,
0x50, 0x12, 0x9a, 0x57, 0x05, 0x54, 0x40, 0xf1, 0xf5, 0xf5, 0x0f, 0xff, 0x52, 0x9f, 0x3f, 0xc0, 0xcf, 0x13, 0x27, 0x29, 0xef, 0x4e, 0x9b, 0xbd, 0x24, 0xfd, 0x5f, 0x37, 0x9d, 0x86, 0x49, 0xac, 0x97, 0x35, 0x93, 0xe9, 0x35, 0x18,
0x3c, 0x29, 0x0c, 0xa3, 0xea, 0x74, 0x7c, 0xe3, 0xa1, 0xf9, 0x90, 0x51, 0xc3, 0x7b, 0x00, 0xfc, 0x4e, 0xef, 0x49, 0x18, 0xbb, 0xe6, 0x01, 0x38, 0xa7, 0x1c, 0x30, 0xb4, 0x65, 0xcf, 0x03, 0x60, 0xff, 0x72, 0xf3, 0x02, 0xfd, 0xda,
0x79, 0x77, 0x98, 0xec, 0x24, 0xe9, 0x5f, 0x5d, 0xd9, 0x1a, 0x26, 0xd1, 0x2e, 0x4a, 0x26, 0xe7, 0xd7, 0x60, 0x60, 0xc2, 0x09, 0xa6, 0x06, 0x7b, 0xed, 0x65, 0x4d, 0x65, 0xd9, 0xe4, 0xc9, 0xbb, 0x5f, 0xae, 0x6f, 0xce, 0x1e, 0xaf,
0x34, 0x30, 0x0b, 0xe0, 0x9c, 0x72, 0xc0, 0xd0, 0xe6, 0x1d, 0x0f, 0xec, 0xa8, 0x42, 0xec, 0x27, 0x8d, 0x98, 0xd9, 0x35, 0x11, 0xa2, 0x3c, 0x33, 0x1f, 0x42, 0xd6, 0x95, 0x64, 0x2d, 0xe9, 0xa4, 0x16, 0xeb, 0xa8, 0x10, 0x38, 0x79,
0x60, 0xed, 0x65, 0x49, 0x65, 0x5e, 0xa5, 0xf1, 0xbb, 0x1f, 0xaf, 0x6f, 0x8e, 0x1e, 0x77, 0xb0, 0x52, 0x9e, 0x98, 0xa4, 0x9f, 0x17, 0xac, 0xa2, 0xc6, 0xa9, 0x9e, 0xd1, 0x4d, 0xd1, 0x97, 0x6c, 0x3c, 0xe9, 0x7e, 0x60, 0xa5, 0x6b,
0x0f, 0x2c, 0x6d, 0x21, 0x59, 0x4d, 0x1a, 0xa9, 0xc5, 0x3a, 0x2a, 0xce, 0x0e, 0x1e, 0xe9, 0x75, 0xbd, 0x33, 0xda, 0x4e, 0x89, 0x6b, 0x8e, 0x8c, 0xab, 0x3f, 0x13, 0xfd, 0x0b, 0x65, 0x37, 0xa3, 0x8e, 0x36, 0x12, 0x00, 0x00};
0xa9, 0x8e, 0x71, 0x30, 0x47, 0x0f, 0xd9, 0x78, 0xd0, 0xfd, 0x99, 0x95, 0x03, 0x73, 0x14, 0x07, 0xe6, 0x5c, 0x0e,
0xf4, 0xe7, 0xa7, 0xdf, 0x01, 0xf1, 0x69, 0xfc, 0xac, 0x8e, 0x12, 0x00, 0x00};
} // namespace captive_portal } // namespace captive_portal
} // namespace esphome } // namespace esphome

View file

@ -6,7 +6,7 @@ from esphome.const import (
ICON_RADIATOR, ICON_RADIATOR,
ICON_RESTART, ICON_RESTART,
DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_MILLION,
UNIT_PARTS_PER_BILLION, UNIT_PARTS_PER_BILLION,
@ -43,7 +43,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_PARTS_PER_BILLION, unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR, icon=ICON_RADIATOR,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(

View file

@ -0,0 +1,69 @@
#include "animation.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace display {
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type)
: Image(data_start, width, height, type),
animation_data_start_(data_start),
current_frame_(0),
animation_frame_count_(animation_frame_count),
loop_start_frame_(0),
loop_end_frame_(animation_frame_count_),
loop_count_(0),
loop_current_iteration_(1) {}
void Animation::set_loop(uint32_t start_frame, uint32_t end_frame, int count) {
loop_start_frame_ = std::min(start_frame, animation_frame_count_);
loop_end_frame_ = std::min(end_frame, animation_frame_count_);
loop_count_ = count;
loop_current_iteration_ = 1;
}
uint32_t Animation::get_animation_frame_count() const { return this->animation_frame_count_; }
int Animation::get_current_frame() const { return this->current_frame_; }
void Animation::next_frame() {
this->current_frame_++;
if (loop_count_ && this->current_frame_ == loop_end_frame_ &&
(this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) {
this->current_frame_ = loop_start_frame_;
this->loop_current_iteration_++;
}
if (this->current_frame_ >= animation_frame_count_) {
this->loop_current_iteration_ = 1;
this->current_frame_ = 0;
}
this->update_data_start_();
}
void Animation::prev_frame() {
this->current_frame_--;
if (this->current_frame_ < 0) {
this->current_frame_ = this->animation_frame_count_ - 1;
}
this->update_data_start_();
}
void Animation::set_frame(int frame) {
unsigned abs_frame = abs(frame);
if (abs_frame < this->animation_frame_count_) {
if (frame >= 0) {
this->current_frame_ = frame;
} else {
this->current_frame_ = this->animation_frame_count_ - abs_frame;
}
}
this->update_data_start_();
}
void Animation::update_data_start_() {
const uint32_t image_size = image_type_to_width_stride(this->width_, this->type_) * this->height_;
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
}
} // namespace display
} // namespace esphome

View file

@ -0,0 +1,67 @@
#pragma once
#include "image.h"
#include "esphome/core/automation.h"
namespace esphome {
namespace display {
class Animation : public Image {
public:
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type);
uint32_t get_animation_frame_count() const;
int get_current_frame() const;
void next_frame();
void prev_frame();
/** Selects a specific frame within the animation.
*
* @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame.
*/
void set_frame(int frame);
void set_loop(uint32_t start_frame, uint32_t end_frame, int count);
protected:
void update_data_start_();
const uint8_t *animation_data_start_;
int current_frame_;
uint32_t animation_frame_count_;
uint32_t loop_start_frame_;
uint32_t loop_end_frame_;
int loop_count_;
int loop_current_iteration_;
};
template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> {
public:
AnimationNextFrameAction(Animation *parent) : parent_(parent) {}
void play(Ts... x) override { this->parent_->next_frame(); }
protected:
Animation *parent_;
};
template<typename... Ts> class AnimationPrevFrameAction : public Action<Ts...> {
public:
AnimationPrevFrameAction(Animation *parent) : parent_(parent) {}
void play(Ts... x) override { this->parent_->prev_frame(); }
protected:
Animation *parent_;
};
template<typename... Ts> class AnimationSetFrameAction : public Action<Ts...> {
public:
AnimationSetFrameAction(Animation *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(uint16_t, frame)
void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); }
protected:
Animation *parent_;
};
} // namespace display
} // namespace esphome

View file

@ -12,115 +12,9 @@ namespace display {
static const char *const TAG = "display"; static const char *const TAG = "display";
const Color COLOR_OFF(0, 0, 0, 255); const Color COLOR_OFF(0, 0, 0, 0);
const Color COLOR_ON(255, 255, 255, 255); const Color COLOR_ON(255, 255, 255, 255);
static int image_type_to_bpp(ImageType type) {
switch (type) {
case IMAGE_TYPE_BINARY:
return 1;
case IMAGE_TYPE_GRAYSCALE:
return 8;
case IMAGE_TYPE_RGB565:
return 16;
case IMAGE_TYPE_RGB24:
return 24;
case IMAGE_TYPE_RGBA:
return 32;
default:
return 0;
}
}
static int image_type_to_width_stride(int width, ImageType type) { return (width * image_type_to_bpp(type) + 7u) / 8u; }
void Rect::expand(int16_t horizontal, int16_t vertical) {
if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) {
this->x = this->x - horizontal;
this->y = this->y - vertical;
this->w = this->w + (2 * horizontal);
this->h = this->h + (2 * vertical);
}
}
void Rect::extend(Rect rect) {
if (!this->is_set()) {
this->x = rect.x;
this->y = rect.y;
this->w = rect.w;
this->h = rect.h;
} else {
if (this->x > rect.x) {
this->w = this->w + (this->x - rect.x);
this->x = rect.x;
}
if (this->y > rect.y) {
this->h = this->h + (this->y - rect.y);
this->y = rect.y;
}
if (this->x2() < rect.x2()) {
this->w = rect.x2() - this->x;
}
if (this->y2() < rect.y2()) {
this->h = rect.y2() - this->y;
}
}
}
void Rect::shrink(Rect rect) {
if (!this->inside(rect)) {
(*this) = Rect();
} else {
if (this->x2() > rect.x2()) {
this->w = rect.x2() - this->x;
}
if (this->x < rect.x) {
this->w = this->w + (this->x - rect.x);
this->x = rect.x;
}
if (this->y2() > rect.y2()) {
this->h = rect.y2() - this->y;
}
if (this->y < rect.y) {
this->h = this->h + (this->y - rect.y);
this->y = rect.y;
}
}
}
bool Rect::equal(Rect rect) {
return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
}
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) { // NOLINT
if (!this->is_set()) {
return true;
}
if (absolute) {
return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2()));
} else {
return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h));
}
}
bool Rect::inside(Rect rect, bool absolute) {
if (!this->is_set() || !rect.is_set()) {
return true;
}
if (absolute) {
return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y));
} else {
return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0));
}
}
void Rect::info(const std::string &prefix) {
if (this->is_set()) {
ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
this->y2());
} else
ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
}
void DisplayBuffer::init_internal_(uint32_t buffer_length) { void DisplayBuffer::init_internal_(uint32_t buffer_length) {
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->buffer_ = allocator.allocate(buffer_length); this->buffer_ = allocator.allocate(buffer_length);
@ -271,54 +165,14 @@ void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, Color
} while (dx <= 0); } while (dx <= 0);
} }
void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align, const char *text) { void DisplayBuffer::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) {
int x_start, y_start; int x_start, y_start;
int width, height; int width, height;
this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height); this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height);
font->print(x_start, y_start, this, color, text);
int i = 0;
int x_at = x_start;
while (text[i] != '\0') {
int match_length;
int glyph_n = font->match_next_glyph(text + i, &match_length);
if (glyph_n < 0) {
// Unknown char, skip
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
if (!font->get_glyphs().empty()) {
uint8_t glyph_width = font->get_glyphs()[0].glyph_data_->width;
for (int glyph_x = 0; glyph_x < glyph_width; glyph_x++) {
for (int glyph_y = 0; glyph_y < height; glyph_y++)
this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color);
}
x_at += glyph_width;
}
i++;
continue;
}
const Glyph &glyph = font->get_glyphs()[glyph_n];
int scan_x1, scan_y1, scan_width, scan_height;
glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
{
const int glyph_x_max = scan_x1 + scan_width;
const int glyph_y_max = scan_y1 + scan_height;
for (int glyph_x = scan_x1; glyph_x < glyph_x_max; glyph_x++) {
for (int glyph_y = scan_y1; glyph_y < glyph_y_max; glyph_y++) {
if (glyph.get_pixel(glyph_x, glyph_y)) {
this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color);
}
}
}
}
x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
i += match_length;
}
} }
void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg) { void DisplayBuffer::vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format,
va_list arg) {
char buffer[256]; char buffer[256];
int ret = vsnprintf(buffer, sizeof(buffer), format, arg); int ret = vsnprintf(buffer, sizeof(buffer), format, arg);
if (ret > 0) if (ret > 0)
@ -326,6 +180,37 @@ void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign al
} }
void DisplayBuffer::image(int x, int y, BaseImage *image, Color color_on, Color color_off) { void DisplayBuffer::image(int x, int y, BaseImage *image, Color color_on, Color color_off) {
this->image(x, y, image, ImageAlign::TOP_LEFT, color_on, color_off);
}
void DisplayBuffer::image(int x, int y, BaseImage *image, ImageAlign align, Color color_on, Color color_off) {
auto x_align = ImageAlign(int(align) & (int(ImageAlign::HORIZONTAL_ALIGNMENT)));
auto y_align = ImageAlign(int(align) & (int(ImageAlign::VERTICAL_ALIGNMENT)));
switch (x_align) {
case ImageAlign::RIGHT:
x -= image->get_width();
break;
case ImageAlign::CENTER_HORIZONTAL:
x -= image->get_width() / 2;
break;
case ImageAlign::LEFT:
default:
break;
}
switch (y_align) {
case ImageAlign::BOTTOM:
y -= image->get_height();
break;
case ImageAlign::CENTER_VERTICAL:
y -= image->get_height() / 2;
break;
case ImageAlign::TOP:
default:
break;
}
image->draw(x, y, this, color_on, color_off); image->draw(x, y, this, color_on, color_off);
} }
@ -342,7 +227,7 @@ void DisplayBuffer::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_
} }
#endif // USE_QR_CODE #endif // USE_QR_CODE
void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, void DisplayBuffer::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1,
int *width, int *height) { int *width, int *height) {
int x_offset, baseline; int x_offset, baseline;
font->measure(text, width, &x_offset, &baseline, height); font->measure(text, width, &x_offset, &baseline, height);
@ -380,34 +265,34 @@ void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font,
break; break;
} }
} }
void DisplayBuffer::print(int x, int y, Font *font, Color color, const char *text) { void DisplayBuffer::print(int x, int y, BaseFont *font, Color color, const char *text) {
this->print(x, y, font, color, TextAlign::TOP_LEFT, text); this->print(x, y, font, color, TextAlign::TOP_LEFT, text);
} }
void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char *text) { void DisplayBuffer::print(int x, int y, BaseFont *font, TextAlign align, const char *text) {
this->print(x, y, font, COLOR_ON, align, text); this->print(x, y, font, COLOR_ON, align, text);
} }
void DisplayBuffer::print(int x, int y, Font *font, const char *text) { void DisplayBuffer::print(int x, int y, BaseFont *font, const char *text) {
this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text);
} }
void DisplayBuffer::printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) { void DisplayBuffer::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) {
va_list arg; va_list arg;
va_start(arg, format); va_start(arg, format);
this->vprintf_(x, y, font, color, align, format, arg); this->vprintf_(x, y, font, color, align, format, arg);
va_end(arg); va_end(arg);
} }
void DisplayBuffer::printf(int x, int y, Font *font, Color color, const char *format, ...) { void DisplayBuffer::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) {
va_list arg; va_list arg;
va_start(arg, format); va_start(arg, format);
this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg); this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg);
va_end(arg); va_end(arg);
} }
void DisplayBuffer::printf(int x, int y, Font *font, TextAlign align, const char *format, ...) { void DisplayBuffer::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) {
va_list arg; va_list arg;
va_start(arg, format); va_start(arg, format);
this->vprintf_(x, y, font, COLOR_ON, align, format, arg); this->vprintf_(x, y, font, COLOR_ON, align, format, arg);
va_end(arg); va_end(arg);
} }
void DisplayBuffer::printf(int x, int y, Font *font, const char *format, ...) { void DisplayBuffer::printf(int x, int y, BaseFont *font, const char *format, ...) {
va_list arg; va_list arg;
va_start(arg, format); va_start(arg, format);
this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg); this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg);
@ -454,19 +339,20 @@ void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) {
if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to))
this->trigger(from, to); this->trigger(from, to);
} }
void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time) { void DisplayBuffer::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format,
ESPTime time) {
char buffer[64]; char buffer[64];
size_t ret = time.strftime(buffer, sizeof(buffer), format); size_t ret = time.strftime(buffer, sizeof(buffer), format);
if (ret > 0) if (ret > 0)
this->print(x, y, font, color, align, buffer); this->print(x, y, font, color, align, buffer);
} }
void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time) { void DisplayBuffer::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) {
this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time); this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time);
} }
void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time) { void DisplayBuffer::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) {
this->strftime(x, y, font, COLOR_ON, align, format, time); this->strftime(x, y, font, COLOR_ON, align, format, time);
} }
void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, ESPTime time) { void DisplayBuffer::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) {
this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time); this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time);
} }
@ -505,286 +391,6 @@ Rect DisplayBuffer::get_clipping() {
return this->clipping_rectangle_.back(); return this->clipping_rectangle_.back();
} }
} }
bool Glyph::get_pixel(int x, int y) const {
const int x_data = x - this->glyph_data_->offset_x;
const int y_data = y - this->glyph_data_->offset_y;
if (x_data < 0 || x_data >= this->glyph_data_->width || y_data < 0 || y_data >= this->glyph_data_->height)
return false;
const uint32_t width_8 = ((this->glyph_data_->width + 7u) / 8u) * 8u;
const uint32_t pos = x_data + y_data * width_8;
return progmem_read_byte(this->glyph_data_->data + (pos / 8u)) & (0x80 >> (pos % 8u));
}
const char *Glyph::get_char() const { return this->glyph_data_->a_char; }
bool Glyph::compare_to(const char *str) const {
// 1 -> this->char_
// 2 -> str
for (uint32_t i = 0;; i++) {
if (this->glyph_data_->a_char[i] == '\0')
return true;
if (str[i] == '\0')
return false;
if (this->glyph_data_->a_char[i] > str[i])
return false;
if (this->glyph_data_->a_char[i] < str[i])
return true;
}
// this should not happen
return false;
}
int Glyph::match_length(const char *str) const {
for (uint32_t i = 0;; i++) {
if (this->glyph_data_->a_char[i] == '\0')
return i;
if (str[i] != this->glyph_data_->a_char[i])
return 0;
}
// this should not happen
return 0;
}
void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
*x1 = this->glyph_data_->offset_x;
*y1 = this->glyph_data_->offset_y;
*width = this->glyph_data_->width;
*height = this->glyph_data_->height;
}
int Font::match_next_glyph(const char *str, int *match_length) {
int lo = 0;
int hi = this->glyphs_.size() - 1;
while (lo != hi) {
int mid = (lo + hi + 1) / 2;
if (this->glyphs_[mid].compare_to(str)) {
lo = mid;
} else {
hi = mid - 1;
}
}
*match_length = this->glyphs_[lo].match_length(str);
if (*match_length <= 0)
return -1;
return lo;
}
void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
*baseline = this->baseline_;
*height = this->height_;
int i = 0;
int min_x = 0;
bool has_char = false;
int x = 0;
while (str[i] != '\0') {
int match_length;
int glyph_n = this->match_next_glyph(str + i, &match_length);
if (glyph_n < 0) {
// Unknown char, skip
if (!this->get_glyphs().empty())
x += this->get_glyphs()[0].glyph_data_->width;
i++;
continue;
}
const Glyph &glyph = this->glyphs_[glyph_n];
if (!has_char) {
min_x = glyph.glyph_data_->offset_x;
} else {
min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
}
x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
i += match_length;
has_char = true;
}
*x_offset = min_x;
*width = x - min_x;
}
Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) {
glyphs_.reserve(data_nr);
for (int i = 0; i < data_nr; ++i)
glyphs_.emplace_back(&data[i]);
}
void Image::draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) {
switch (type_) {
case IMAGE_TYPE_BINARY: {
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
if (this->get_binary_pixel_(img_x, img_y)) {
display->draw_pixel_at(x + img_x, y + img_y, color_on);
} else if (!this->transparent_) {
display->draw_pixel_at(x + img_x, y + img_y, color_off);
}
}
}
break;
}
case IMAGE_TYPE_GRAYSCALE:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_grayscale_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
case IMAGE_TYPE_RGB565:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgb565_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
case IMAGE_TYPE_RGB24:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgb24_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
case IMAGE_TYPE_RGBA:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgba_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
}
}
Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return color_off;
switch (this->type_) {
case IMAGE_TYPE_BINARY:
return this->get_binary_pixel_(x, y) ? color_on : color_off;
case IMAGE_TYPE_GRAYSCALE:
return this->get_grayscale_pixel_(x, y);
case IMAGE_TYPE_RGB565:
return this->get_rgb565_pixel_(x, y);
case IMAGE_TYPE_RGB24:
return this->get_rgb24_pixel_(x, y);
case IMAGE_TYPE_RGBA:
return this->get_rgba_pixel_(x, y);
default:
return color_off;
}
}
bool Image::get_binary_pixel_(int x, int y) const {
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
const uint32_t pos = x + y * width_8;
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
}
Color Image::get_rgba_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 4;
return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3));
}
Color Image::get_rgb24_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 3;
Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
progmem_read_byte(this->data_start_ + pos + 2));
if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
// (0, 0, 1) has been defined as transparent color for non-alpha images.
// putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if)
color.w = 0;
} else {
color.w = 0xFF;
}
return color;
}
Color Image::get_rgb565_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 2;
uint16_t rgb565 =
progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F;
Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
if (rgb565 == 0x0020 && transparent_) {
// darkest green has been defined as transparent color for transparent RGB565 images.
color.w = 0;
} else {
color.w = 0xFF;
}
return color;
}
Color Image::get_grayscale_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF;
return Color(gray, gray, gray, alpha);
}
int Image::get_width() const { return this->width_; }
int Image::get_height() const { return this->height_; }
ImageType Image::get_type() const { return this->type_; }
Image::Image(const uint8_t *data_start, int width, int height, ImageType type)
: width_(width), height_(height), type_(type), data_start_(data_start) {}
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type)
: Image(data_start, width, height, type),
animation_data_start_(data_start),
current_frame_(0),
animation_frame_count_(animation_frame_count),
loop_start_frame_(0),
loop_end_frame_(animation_frame_count_),
loop_count_(0),
loop_current_iteration_(1) {}
void Animation::set_loop(uint32_t start_frame, uint32_t end_frame, int count) {
loop_start_frame_ = std::min(start_frame, animation_frame_count_);
loop_end_frame_ = std::min(end_frame, animation_frame_count_);
loop_count_ = count;
loop_current_iteration_ = 1;
}
uint32_t Animation::get_animation_frame_count() const { return this->animation_frame_count_; }
int Animation::get_current_frame() const { return this->current_frame_; }
void Animation::next_frame() {
this->current_frame_++;
if (loop_count_ && this->current_frame_ == loop_end_frame_ &&
(this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) {
this->current_frame_ = loop_start_frame_;
this->loop_current_iteration_++;
}
if (this->current_frame_ >= animation_frame_count_) {
this->loop_current_iteration_ = 1;
this->current_frame_ = 0;
}
this->update_data_start_();
}
void Animation::prev_frame() {
this->current_frame_--;
if (this->current_frame_ < 0) {
this->current_frame_ = this->animation_frame_count_ - 1;
}
this->update_data_start_();
}
void Animation::set_frame(int frame) {
unsigned abs_frame = abs(frame);
if (abs_frame < this->animation_frame_count_) {
if (frame >= 0) {
this->current_frame_ = frame;
} else {
this->current_frame_ = this->animation_frame_count_ - abs_frame;
}
}
this->update_data_start_();
}
void Animation::update_data_start_() {
const uint32_t image_size = image_type_to_width_stride(this->width_, this->type_) * this->height_;
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
}
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
void DisplayPage::show() { this->parent_->show_page(this); } void DisplayPage::show() { this->parent_->show_page(this); }

View file

@ -2,6 +2,7 @@
#include <cstdarg> #include <cstdarg>
#include <vector> #include <vector>
#include "rect.h"
#include "display_color_utils.h" #include "display_color_utils.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
@ -70,17 +71,52 @@ enum class TextAlign {
BOTTOM_RIGHT = BOTTOM | RIGHT, BOTTOM_RIGHT = BOTTOM | RIGHT,
}; };
/// Turn the pixel OFF. /** ImageAlign is used to tell the display class how to position a image. By default
extern const Color COLOR_OFF; * the coordinates you enter for the image() functions take the upper left corner of the image
/// Turn the pixel ON. * as the "anchor" point. You can customize this behavior to, for example, make the coordinates
extern const Color COLOR_ON; * refer to the *center* of the image.
*
* All image alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis
* these options are allowed:
*
* - LEFT (x-coordinate of anchor point is on left)
* - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the image)
* - RIGHT (x-coordinate of anchor point is on right)
*
* For the Y-Axis alignment these options are allowed:
*
* - TOP (y-coordinate of anchor is on the top of the image)
* - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the image)
* - BOTTOM (y-coordinate of anchor is on the bottom of the image)
*
* These options are then combined to create combined TextAlignment options like:
* - TOP_LEFT (default)
* - CENTER (anchor point is in the middle of the image bounds)
* - ...
*/
enum class ImageAlign {
TOP = 0x00,
CENTER_VERTICAL = 0x01,
BOTTOM = 0x02,
enum ImageType { LEFT = 0x00,
IMAGE_TYPE_BINARY = 0, CENTER_HORIZONTAL = 0x04,
IMAGE_TYPE_GRAYSCALE = 1, RIGHT = 0x08,
IMAGE_TYPE_RGB24 = 2,
IMAGE_TYPE_RGB565 = 3, TOP_LEFT = TOP | LEFT,
IMAGE_TYPE_RGBA = 4, TOP_CENTER = TOP | CENTER_HORIZONTAL,
TOP_RIGHT = TOP | RIGHT,
CENTER_LEFT = CENTER_VERTICAL | LEFT,
CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL,
CENTER_RIGHT = CENTER_VERTICAL | RIGHT,
BOTTOM_LEFT = BOTTOM | LEFT,
BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL,
BOTTOM_RIGHT = BOTTOM | RIGHT,
HORIZONTAL_ALIGNMENT = LEFT | CENTER_HORIZONTAL | RIGHT,
VERTICAL_ALIGNMENT = TOP | CENTER_VERTICAL | BOTTOM
}; };
enum DisplayType { enum DisplayType {
@ -96,35 +132,6 @@ enum DisplayRotation {
DISPLAY_ROTATION_270_DEGREES = 270, DISPLAY_ROTATION_270_DEGREES = 270,
}; };
static const int16_t VALUE_NO_SET = 32766;
class Rect {
public:
int16_t x; ///< X coordinate of corner
int16_t y; ///< Y coordinate of corner
int16_t w; ///< Width of region
int16_t h; ///< Height of region
Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT
inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {}
inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner
inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner
inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); }
void expand(int16_t horizontal, int16_t vertical);
void extend(Rect rect);
void shrink(Rect rect);
bool inside(Rect rect, bool absolute = true);
bool inside(int16_t test_x, int16_t test_y, bool absolute = true);
bool equal(Rect rect);
void info(const std::string &prefix = "rect info:");
};
class BaseImage;
class Font;
class DisplayBuffer; class DisplayBuffer;
class DisplayPage; class DisplayPage;
class DisplayOnPageChangeTrigger; class DisplayOnPageChangeTrigger;
@ -138,6 +145,24 @@ using display_writer_t = std::function<void(DisplayBuffer &)>;
ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \ ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \
} }
/// Turn the pixel OFF.
extern const Color COLOR_OFF;
/// Turn the pixel ON.
extern const Color COLOR_ON;
class BaseImage {
public:
virtual void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) = 0;
virtual int get_width() const = 0;
virtual int get_height() const = 0;
};
class BaseFont {
public:
virtual void print(int x, int y, DisplayBuffer *display, Color color, const char *text) = 0;
virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0;
};
class DisplayBuffer { class DisplayBuffer {
public: public:
/// Fill the entire screen with the given color. /// Fill the entire screen with the given color.
@ -184,7 +209,7 @@ class DisplayBuffer {
* @param align The alignment of the text. * @param align The alignment of the text.
* @param text The text to draw. * @param text The text to draw.
*/ */
void print(int x, int y, Font *font, Color color, TextAlign align, const char *text); void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text);
/** Print `text` with the top left at [x,y] with `font`. /** Print `text` with the top left at [x,y] with `font`.
* *
@ -194,7 +219,7 @@ class DisplayBuffer {
* @param color The color to draw the text with. * @param color The color to draw the text with.
* @param text The text to draw. * @param text The text to draw.
*/ */
void print(int x, int y, Font *font, Color color, const char *text); void print(int x, int y, BaseFont *font, Color color, const char *text);
/** Print `text` with the anchor point at [x,y] with `font`. /** Print `text` with the anchor point at [x,y] with `font`.
* *
@ -204,7 +229,7 @@ class DisplayBuffer {
* @param align The alignment of the text. * @param align The alignment of the text.
* @param text The text to draw. * @param text The text to draw.
*/ */
void print(int x, int y, Font *font, TextAlign align, const char *text); void print(int x, int y, BaseFont *font, TextAlign align, const char *text);
/** Print `text` with the top left at [x,y] with `font`. /** Print `text` with the top left at [x,y] with `font`.
* *
@ -213,7 +238,7 @@ class DisplayBuffer {
* @param font The font to draw the text with. * @param font The font to draw the text with.
* @param text The text to draw. * @param text The text to draw.
*/ */
void print(int x, int y, Font *font, const char *text); void print(int x, int y, BaseFont *font, const char *text);
/** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
* *
@ -225,7 +250,7 @@ class DisplayBuffer {
* @param format The format to use. * @param format The format to use.
* @param ... The arguments to use for the text formatting. * @param ... The arguments to use for the text formatting.
*/ */
void printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) void printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...)
__attribute__((format(printf, 7, 8))); __attribute__((format(printf, 7, 8)));
/** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
@ -237,7 +262,7 @@ class DisplayBuffer {
* @param format The format to use. * @param format The format to use.
* @param ... The arguments to use for the text formatting. * @param ... The arguments to use for the text formatting.
*/ */
void printf(int x, int y, Font *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7))); void printf(int x, int y, BaseFont *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7)));
/** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
* *
@ -248,7 +273,8 @@ class DisplayBuffer {
* @param format The format to use. * @param format The format to use.
* @param ... The arguments to use for the text formatting. * @param ... The arguments to use for the text formatting.
*/ */
void printf(int x, int y, Font *font, TextAlign align, const char *format, ...) __attribute__((format(printf, 6, 7))); void printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...)
__attribute__((format(printf, 6, 7)));
/** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
* *
@ -258,7 +284,7 @@ class DisplayBuffer {
* @param format The format to use. * @param format The format to use.
* @param ... The arguments to use for the text formatting. * @param ... The arguments to use for the text formatting.
*/ */
void printf(int x, int y, Font *font, const char *format, ...) __attribute__((format(printf, 5, 6))); void printf(int x, int y, BaseFont *font, const char *format, ...) __attribute__((format(printf, 5, 6)));
/** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
* *
@ -270,7 +296,7 @@ class DisplayBuffer {
* @param format The strftime format to use. * @param format The strftime format to use.
* @param time The time to format. * @param time The time to format.
*/ */
void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time) void strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time)
__attribute__((format(strftime, 7, 0))); __attribute__((format(strftime, 7, 0)));
/** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
@ -282,7 +308,7 @@ class DisplayBuffer {
* @param format The strftime format to use. * @param format The strftime format to use.
* @param time The time to format. * @param time The time to format.
*/ */
void strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time) void strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time)
__attribute__((format(strftime, 6, 0))); __attribute__((format(strftime, 6, 0)));
/** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
@ -294,7 +320,7 @@ class DisplayBuffer {
* @param format The strftime format to use. * @param format The strftime format to use.
* @param time The time to format. * @param time The time to format.
*/ */
void strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time) void strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time)
__attribute__((format(strftime, 6, 0))); __attribute__((format(strftime, 6, 0)));
/** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
@ -305,18 +331,29 @@ class DisplayBuffer {
* @param format The strftime format to use. * @param format The strftime format to use.
* @param time The time to format. * @param time The time to format.
*/ */
void strftime(int x, int y, Font *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0))); void strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0)));
/** Draw the `image` with the top-left corner at [x,y] to the screen. /** Draw the `image` with the top-left corner at [x,y] to the screen.
* *
* @param x The x coordinate of the upper left corner. * @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner. * @param y The y coordinate of the upper left corner.
* @param image The image to draw * @param image The image to draw.
* @param color_on The color to replace in binary images for the on bits. * @param color_on The color to replace in binary images for the on bits.
* @param color_off The color to replace in binary images for the off bits. * @param color_off The color to replace in binary images for the off bits.
*/ */
void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
/** Draw the `image` at [x,y] to the screen.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param image The image to draw.
* @param align The alignment of the image.
* @param color_on The color to replace in binary images for the on bits.
* @param color_off The color to replace in binary images for the off bits.
*/
void image(int x, int y, BaseImage *image, ImageAlign align, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
#ifdef USE_GRAPH #ifdef USE_GRAPH
/** Draw the `graph` with the top-left corner at [x,y] to the screen. /** Draw the `graph` with the top-left corner at [x,y] to the screen.
* *
@ -364,7 +401,7 @@ class DisplayBuffer {
* @param width A pointer to store the returned text width in. * @param width A pointer to store the returned text width in.
* @param height A pointer to store the returned text height in. * @param height A pointer to store the returned text height in.
*/ */
void get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, int *width, void get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width,
int *height); int *height);
/// Internal method to set the display writer lambda. /// Internal method to set the display writer lambda.
@ -439,7 +476,7 @@ class DisplayBuffer {
bool is_clipping() const { return !this->clipping_rectangle_.empty(); } bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
protected: protected:
void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); void vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg);
virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0; virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0;
@ -475,123 +512,6 @@ class DisplayPage {
DisplayPage *next_{nullptr}; DisplayPage *next_{nullptr};
}; };
struct GlyphData {
const char *a_char;
const uint8_t *data;
int offset_x;
int offset_y;
int width;
int height;
};
class Glyph {
public:
Glyph(const GlyphData *data) : glyph_data_(data) {}
bool get_pixel(int x, int y) const;
const char *get_char() const;
bool compare_to(const char *str) const;
int match_length(const char *str) const;
void scan_area(int *x1, int *y1, int *width, int *height) const;
protected:
friend Font;
friend DisplayBuffer;
const GlyphData *glyph_data_;
};
class Font {
public:
/** Construct the font with the given glyphs.
*
* @param glyphs A vector of glyphs, must be sorted lexicographically.
* @param baseline The y-offset from the top of the text to the baseline.
* @param bottom The y-offset from the top of the text to the bottom (i.e. height).
*/
Font(const GlyphData *data, int data_nr, int baseline, int height);
int match_next_glyph(const char *str, int *match_length);
void measure(const char *str, int *width, int *x_offset, int *baseline, int *height);
inline int get_baseline() { return this->baseline_; }
inline int get_height() { return this->height_; }
const std::vector<Glyph, ExternalRAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
protected:
std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_;
int baseline_;
int height_;
};
class BaseImage {
public:
virtual void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) = 0;
virtual int get_width() const = 0;
virtual int get_height() const = 0;
};
class Image : public BaseImage {
public:
Image(const uint8_t *data_start, int width, int height, ImageType type);
Color get_pixel(int x, int y, Color color_on = COLOR_ON, Color color_off = COLOR_OFF) const;
int get_width() const override;
int get_height() const override;
ImageType get_type() const;
void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) override;
void set_transparency(bool transparent) { transparent_ = transparent; }
bool has_transparency() const { return transparent_; }
protected:
bool get_binary_pixel_(int x, int y) const;
Color get_rgb24_pixel_(int x, int y) const;
Color get_rgba_pixel_(int x, int y) const;
Color get_rgb565_pixel_(int x, int y) const;
Color get_grayscale_pixel_(int x, int y) const;
int width_;
int height_;
ImageType type_;
const uint8_t *data_start_;
bool transparent_;
};
class Animation : public Image {
public:
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type);
uint32_t get_animation_frame_count() const;
int get_current_frame() const;
void next_frame();
void prev_frame();
/** Selects a specific frame within the animation.
*
* @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame.
*/
void set_frame(int frame);
void set_loop(uint32_t start_frame, uint32_t end_frame, int count);
protected:
void update_data_start_();
const uint8_t *animation_data_start_;
int current_frame_;
uint32_t animation_frame_count_;
uint32_t loop_start_frame_;
uint32_t loop_end_frame_;
int loop_count_;
int loop_current_iteration_;
};
template<typename... Ts> class DisplayPageShowAction : public Action<Ts...> { template<typename... Ts> class DisplayPageShowAction : public Action<Ts...> {
public: public:
TEMPLATABLE_VALUE(DisplayPage *, page) TEMPLATABLE_VALUE(DisplayPage *, page)

View file

@ -0,0 +1,147 @@
#include "font.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace display {
static const char *const TAG = "display";
void Glyph::draw(int x_at, int y_start, DisplayBuffer *display, Color color) const {
int scan_x1, scan_y1, scan_width, scan_height;
this->scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
const unsigned char *data = this->glyph_data_->data;
const int max_x = x_at + scan_x1 + scan_width;
const int max_y = y_start + scan_y1 + scan_height;
for (int glyph_y = y_start + scan_y1; glyph_y < max_y; glyph_y++) {
for (int glyph_x = x_at + scan_x1; glyph_x < max_x; data++, glyph_x += 8) {
uint8_t pixel_data = progmem_read_byte(data);
const int pixel_max_x = std::min(max_x, glyph_x + 8);
for (int pixel_x = glyph_x; pixel_x < pixel_max_x && pixel_data; pixel_x++, pixel_data <<= 1) {
if (pixel_data & 0x80) {
display->draw_pixel_at(pixel_x, glyph_y, color);
}
}
}
}
}
const char *Glyph::get_char() const { return this->glyph_data_->a_char; }
bool Glyph::compare_to(const char *str) const {
// 1 -> this->char_
// 2 -> str
for (uint32_t i = 0;; i++) {
if (this->glyph_data_->a_char[i] == '\0')
return true;
if (str[i] == '\0')
return false;
if (this->glyph_data_->a_char[i] > str[i])
return false;
if (this->glyph_data_->a_char[i] < str[i])
return true;
}
// this should not happen
return false;
}
int Glyph::match_length(const char *str) const {
for (uint32_t i = 0;; i++) {
if (this->glyph_data_->a_char[i] == '\0')
return i;
if (str[i] != this->glyph_data_->a_char[i])
return 0;
}
// this should not happen
return 0;
}
void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
*x1 = this->glyph_data_->offset_x;
*y1 = this->glyph_data_->offset_y;
*width = this->glyph_data_->width;
*height = this->glyph_data_->height;
}
Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) {
glyphs_.reserve(data_nr);
for (int i = 0; i < data_nr; ++i)
glyphs_.emplace_back(&data[i]);
}
int Font::match_next_glyph(const char *str, int *match_length) {
int lo = 0;
int hi = this->glyphs_.size() - 1;
while (lo != hi) {
int mid = (lo + hi + 1) / 2;
if (this->glyphs_[mid].compare_to(str)) {
lo = mid;
} else {
hi = mid - 1;
}
}
*match_length = this->glyphs_[lo].match_length(str);
if (*match_length <= 0)
return -1;
return lo;
}
void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
*baseline = this->baseline_;
*height = this->height_;
int i = 0;
int min_x = 0;
bool has_char = false;
int x = 0;
while (str[i] != '\0') {
int match_length;
int glyph_n = this->match_next_glyph(str + i, &match_length);
if (glyph_n < 0) {
// Unknown char, skip
if (!this->get_glyphs().empty())
x += this->get_glyphs()[0].glyph_data_->width;
i++;
continue;
}
const Glyph &glyph = this->glyphs_[glyph_n];
if (!has_char) {
min_x = glyph.glyph_data_->offset_x;
} else {
min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
}
x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
i += match_length;
has_char = true;
}
*x_offset = min_x;
*width = x - min_x;
}
void Font::print(int x_start, int y_start, DisplayBuffer *display, Color color, const char *text) {
int i = 0;
int x_at = x_start;
while (text[i] != '\0') {
int match_length;
int glyph_n = this->match_next_glyph(text + i, &match_length);
if (glyph_n < 0) {
// Unknown char, skip
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
if (!this->get_glyphs().empty()) {
uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->width;
display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color);
x_at += glyph_width;
}
i++;
continue;
}
const Glyph &glyph = this->get_glyphs()[glyph_n];
glyph.draw(x_at, y_start, display, color);
x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
i += match_length;
}
}
} // namespace display
} // namespace esphome

View file

@ -0,0 +1,67 @@
#pragma once
#include "esphome/core/datatypes.h"
#include "display_buffer.h"
namespace esphome {
namespace display {
class DisplayBuffer;
class Font;
struct GlyphData {
const char *a_char;
const uint8_t *data;
int offset_x;
int offset_y;
int width;
int height;
};
class Glyph {
public:
Glyph(const GlyphData *data) : glyph_data_(data) {}
void draw(int x, int y, DisplayBuffer *display, Color color) const;
const char *get_char() const;
bool compare_to(const char *str) const;
int match_length(const char *str) const;
void scan_area(int *x1, int *y1, int *width, int *height) const;
protected:
friend Font;
const GlyphData *glyph_data_;
};
class Font : public BaseFont {
public:
/** Construct the font with the given glyphs.
*
* @param glyphs A vector of glyphs, must be sorted lexicographically.
* @param baseline The y-offset from the top of the text to the baseline.
* @param bottom The y-offset from the top of the text to the bottom (i.e. height).
*/
Font(const GlyphData *data, int data_nr, int baseline, int height);
int match_next_glyph(const char *str, int *match_length);
void print(int x_start, int y_start, DisplayBuffer *display, Color color, const char *text) override;
void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) override;
inline int get_baseline() { return this->baseline_; }
inline int get_height() { return this->height_; }
const std::vector<Glyph, ExternalRAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
protected:
std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_;
int baseline_;
int height_;
};
} // namespace display
} // namespace esphome

View file

@ -0,0 +1,134 @@
#include "image.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace display {
void Image::draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) {
switch (type_) {
case IMAGE_TYPE_BINARY: {
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
if (this->get_binary_pixel_(img_x, img_y)) {
display->draw_pixel_at(x + img_x, y + img_y, color_on);
} else if (!this->transparent_) {
display->draw_pixel_at(x + img_x, y + img_y, color_off);
}
}
}
break;
}
case IMAGE_TYPE_GRAYSCALE:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_grayscale_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
case IMAGE_TYPE_RGB565:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgb565_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
case IMAGE_TYPE_RGB24:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgb24_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
case IMAGE_TYPE_RGBA:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgba_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
}
}
Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return color_off;
switch (this->type_) {
case IMAGE_TYPE_BINARY:
return this->get_binary_pixel_(x, y) ? color_on : color_off;
case IMAGE_TYPE_GRAYSCALE:
return this->get_grayscale_pixel_(x, y);
case IMAGE_TYPE_RGB565:
return this->get_rgb565_pixel_(x, y);
case IMAGE_TYPE_RGB24:
return this->get_rgb24_pixel_(x, y);
case IMAGE_TYPE_RGBA:
return this->get_rgba_pixel_(x, y);
default:
return color_off;
}
}
bool Image::get_binary_pixel_(int x, int y) const {
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
const uint32_t pos = x + y * width_8;
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
}
Color Image::get_rgba_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 4;
return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3));
}
Color Image::get_rgb24_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 3;
Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
progmem_read_byte(this->data_start_ + pos + 2));
if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
// (0, 0, 1) has been defined as transparent color for non-alpha images.
// putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if)
color.w = 0;
} else {
color.w = 0xFF;
}
return color;
}
Color Image::get_rgb565_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 2;
uint16_t rgb565 =
progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F;
Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
if (rgb565 == 0x0020 && transparent_) {
// darkest green has been defined as transparent color for transparent RGB565 images.
color.w = 0;
} else {
color.w = 0xFF;
}
return color;
}
Color Image::get_grayscale_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF;
return Color(gray, gray, gray, alpha);
}
int Image::get_width() const { return this->width_; }
int Image::get_height() const { return this->height_; }
ImageType Image::get_type() const { return this->type_; }
Image::Image(const uint8_t *data_start, int width, int height, ImageType type)
: width_(width), height_(height), type_(type), data_start_(data_start) {}
} // namespace display
} // namespace esphome

View file

@ -0,0 +1,62 @@
#pragma once
#include "esphome/core/color.h"
#include "display_buffer.h"
namespace esphome {
namespace display {
enum ImageType {
IMAGE_TYPE_BINARY = 0,
IMAGE_TYPE_GRAYSCALE = 1,
IMAGE_TYPE_RGB24 = 2,
IMAGE_TYPE_RGB565 = 3,
IMAGE_TYPE_RGBA = 4,
};
inline int image_type_to_bpp(ImageType type) {
switch (type) {
case IMAGE_TYPE_BINARY:
return 1;
case IMAGE_TYPE_GRAYSCALE:
return 8;
case IMAGE_TYPE_RGB565:
return 16;
case IMAGE_TYPE_RGB24:
return 24;
case IMAGE_TYPE_RGBA:
return 32;
}
return 0;
}
inline int image_type_to_width_stride(int width, ImageType type) { return (width * image_type_to_bpp(type) + 7u) / 8u; }
class Image : public BaseImage {
public:
Image(const uint8_t *data_start, int width, int height, ImageType type);
Color get_pixel(int x, int y, Color color_on = COLOR_ON, Color color_off = COLOR_OFF) const;
int get_width() const override;
int get_height() const override;
ImageType get_type() const;
void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) override;
void set_transparency(bool transparent) { transparent_ = transparent; }
bool has_transparency() const { return transparent_; }
protected:
bool get_binary_pixel_(int x, int y) const;
Color get_rgb24_pixel_(int x, int y) const;
Color get_rgba_pixel_(int x, int y) const;
Color get_rgb565_pixel_(int x, int y) const;
Color get_grayscale_pixel_(int x, int y) const;
int width_;
int height_;
ImageType type_;
const uint8_t *data_start_;
bool transparent_;
};
} // namespace display
} // namespace esphome

View file

@ -0,0 +1,98 @@
#include "rect.h"
#include "esphome/core/log.h"
namespace esphome {
namespace display {
static const char *const TAG = "display";
void Rect::expand(int16_t horizontal, int16_t vertical) {
if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) {
this->x = this->x - horizontal;
this->y = this->y - vertical;
this->w = this->w + (2 * horizontal);
this->h = this->h + (2 * vertical);
}
}
void Rect::extend(Rect rect) {
if (!this->is_set()) {
this->x = rect.x;
this->y = rect.y;
this->w = rect.w;
this->h = rect.h;
} else {
if (this->x > rect.x) {
this->w = this->w + (this->x - rect.x);
this->x = rect.x;
}
if (this->y > rect.y) {
this->h = this->h + (this->y - rect.y);
this->y = rect.y;
}
if (this->x2() < rect.x2()) {
this->w = rect.x2() - this->x;
}
if (this->y2() < rect.y2()) {
this->h = rect.y2() - this->y;
}
}
}
void Rect::shrink(Rect rect) {
if (!this->inside(rect)) {
(*this) = Rect();
} else {
if (this->x2() > rect.x2()) {
this->w = rect.x2() - this->x;
}
if (this->x < rect.x) {
this->w = this->w + (this->x - rect.x);
this->x = rect.x;
}
if (this->y2() > rect.y2()) {
this->h = rect.y2() - this->y;
}
if (this->y < rect.y) {
this->h = this->h + (this->y - rect.y);
this->y = rect.y;
}
}
}
bool Rect::equal(Rect rect) {
return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
}
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) { // NOLINT
if (!this->is_set()) {
return true;
}
if (absolute) {
return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2()));
} else {
return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h));
}
}
bool Rect::inside(Rect rect, bool absolute) {
if (!this->is_set() || !rect.is_set()) {
return true;
}
if (absolute) {
return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y));
} else {
return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0));
}
}
void Rect::info(const std::string &prefix) {
if (this->is_set()) {
ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
this->y2());
} else
ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
}
} // namespace display
} // namespace esphome

View file

@ -0,0 +1,36 @@
#pragma once
#include "esphome/core/helpers.h"
namespace esphome {
namespace display {
static const int16_t VALUE_NO_SET = 32766;
class Rect {
public:
int16_t x; ///< X coordinate of corner
int16_t y; ///< Y coordinate of corner
int16_t w; ///< Width of region
int16_t h; ///< Height of region
Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT
inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {}
inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner
inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner
inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); }
void expand(int16_t horizontal, int16_t vertical);
void extend(Rect rect);
void shrink(Rect rect);
bool inside(Rect rect, bool absolute = true);
bool inside(int16_t test_x, int16_t test_y, bool absolute = true);
bool equal(Rect rect);
void info(const std::string &prefix = "rect info:");
};
} // namespace display
} // namespace esphome

View file

@ -1,7 +1,7 @@
#include "ethernet_info_text_sensor.h" #include "ethernet_info_text_sensor.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef USE_ESP32_FRAMEWORK_ARDUINO #ifdef USE_ESP32
namespace esphome { namespace esphome {
namespace ethernet_info { namespace ethernet_info {
@ -13,4 +13,4 @@ void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IP
} // namespace ethernet_info } // namespace ethernet_info
} // namespace esphome } // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO #endif // USE_ESP32

View file

@ -4,7 +4,7 @@
#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/ethernet/ethernet_component.h" #include "esphome/components/ethernet/ethernet_component.h"
#ifdef USE_ESP32_FRAMEWORK_ARDUINO #ifdef USE_ESP32
namespace esphome { namespace esphome {
namespace ethernet_info { namespace ethernet_info {
@ -30,4 +30,4 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS
} // namespace ethernet_info } // namespace ethernet_info
} // namespace esphome } // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO #endif // USE_ESP32

View file

@ -151,11 +151,13 @@ void FujitsuGeneralClimate::transmit_state() {
case climate::CLIMATE_FAN_LOW: case climate::CLIMATE_FAN_LOW:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW); SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW);
break; break;
case climate::CLIMATE_FAN_QUIET:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_SILENT);
break;
case climate::CLIMATE_FAN_AUTO: case climate::CLIMATE_FAN_AUTO:
default: default:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO); SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO);
break; break;
// TODO Quiet / Silent
} }
// Set swing // Set swing
@ -345,8 +347,9 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) {
const uint8_t recv_fan_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_FAN_NIBBLE); const uint8_t recv_fan_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_FAN_NIBBLE);
ESP_LOGV(TAG, "Received fan mode %X", recv_fan_mode); ESP_LOGV(TAG, "Received fan mode %X", recv_fan_mode);
switch (recv_fan_mode) { switch (recv_fan_mode) {
// TODO No Quiet / Silent in ESPH
case FUJITSU_GENERAL_FAN_SILENT: case FUJITSU_GENERAL_FAN_SILENT:
this->fan_mode = climate::CLIMATE_FAN_QUIET;
break;
case FUJITSU_GENERAL_FAN_LOW: case FUJITSU_GENERAL_FAN_LOW:
this->fan_mode = climate::CLIMATE_FAN_LOW; this->fan_mode = climate::CLIMATE_FAN_LOW;
break; break;

View file

@ -52,7 +52,7 @@ class FujitsuGeneralClimate : public climate_ir::ClimateIR {
FujitsuGeneralClimate() FujitsuGeneralClimate()
: ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1.0f, true, true, : ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH}, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_QUIET},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL, {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_BOTH}) {} climate::CLIMATE_SWING_BOTH}) {}

View file

@ -1,5 +1,6 @@
#include "graph.h" #include "graph.h"
#include "esphome/components/display/display_buffer.h" #include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/font.h"
#include "esphome/core/color.h" #include "esphome/core/color.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"

View file

@ -9,11 +9,42 @@ static const char *const TAG = "growatt_solar";
static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04;
static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion
void GrowattSolar::loop() {
// If update() was unable to send we retry until we can send.
if (!this->waiting_to_update_)
return;
update();
}
void GrowattSolar::update() { void GrowattSolar::update() {
// If our last send has had no reply yet, and it wasn't that long ago, do nothing.
uint32_t now = millis();
if (now - this->last_send_ < this->get_update_interval() / 2) {
return;
}
// The bus might be slow, or there might be other devices, or other components might be talking to our device.
if (this->waiting_for_response()) {
this->waiting_to_update_ = true;
return;
}
this->waiting_to_update_ = false;
this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT[this->protocol_version_]); this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT[this->protocol_version_]);
this->last_send_ = millis();
} }
void GrowattSolar::on_modbus_data(const std::vector<uint8_t> &data) { void GrowattSolar::on_modbus_data(const std::vector<uint8_t> &data) {
// Other components might be sending commands to our device. But we don't get called with enough
// context to know what is what. So if we didn't do a send, we ignore the data.
if (!this->last_send_)
return;
this->last_send_ = 0;
// Also ignore the data if the message is too short. Otherwise we will publish invalid values.
if (data.size() < MODBUS_REGISTER_COUNT[this->protocol_version_] * 2)
return;
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void { auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void {
if (sensor == nullptr) if (sensor == nullptr)
return; return;

View file

@ -19,6 +19,7 @@ enum GrowattProtocolVersion {
class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { class GrowattSolar : public PollingComponent, public modbus::ModbusDevice {
public: public:
void loop() override;
void update() override; void update() override;
void on_modbus_data(const std::vector<uint8_t> &data) override; void on_modbus_data(const std::vector<uint8_t> &data) override;
void dump_config() override; void dump_config() override;
@ -55,6 +56,9 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice {
} }
protected: protected:
bool waiting_to_update_;
uint32_t last_send_;
struct GrowattPhase { struct GrowattPhase {
sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr};

View file

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

View file

@ -0,0 +1,130 @@
#pragma once
#include "esphome/core/automation.h"
#include "haier_base.h"
#include "hon_climate.h"
namespace esphome {
namespace haier {
template<typename... Ts> class DisplayOnAction : public Action<Ts...> {
public:
DisplayOnAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->set_display_state(true); }
protected:
HaierClimateBase *parent_;
};
template<typename... Ts> class DisplayOffAction : public Action<Ts...> {
public:
DisplayOffAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->set_display_state(false); }
protected:
HaierClimateBase *parent_;
};
template<typename... Ts> class BeeperOnAction : public Action<Ts...> {
public:
BeeperOnAction(HonClimate *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->set_beeper_state(true); }
protected:
HonClimate *parent_;
};
template<typename... Ts> class BeeperOffAction : public Action<Ts...> {
public:
BeeperOffAction(HonClimate *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->set_beeper_state(false); }
protected:
HonClimate *parent_;
};
template<typename... Ts> class VerticalAirflowAction : public Action<Ts...> {
public:
VerticalAirflowAction(HonClimate *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(AirflowVerticalDirection, direction)
void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); }
protected:
HonClimate *parent_;
};
template<typename... Ts> class HorizontalAirflowAction : public Action<Ts...> {
public:
HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(AirflowHorizontalDirection, direction)
void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); }
protected:
HonClimate *parent_;
};
template<typename... Ts> class HealthOnAction : public Action<Ts...> {
public:
HealthOnAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->set_health_mode(true); }
protected:
HaierClimateBase *parent_;
};
template<typename... Ts> class HealthOffAction : public Action<Ts...> {
public:
HealthOffAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->set_health_mode(false); }
protected:
HaierClimateBase *parent_;
};
template<typename... Ts> class StartSelfCleaningAction : public Action<Ts...> {
public:
StartSelfCleaningAction(HonClimate *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->start_self_cleaning(); }
protected:
HonClimate *parent_;
};
template<typename... Ts> class StartSteriCleaningAction : public Action<Ts...> {
public:
StartSteriCleaningAction(HonClimate *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->start_steri_cleaning(); }
protected:
HonClimate *parent_;
};
template<typename... Ts> class PowerOnAction : public Action<Ts...> {
public:
PowerOnAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->send_power_on_command(); }
protected:
HaierClimateBase *parent_;
};
template<typename... Ts> class PowerOffAction : public Action<Ts...> {
public:
PowerOffAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->send_power_off_command(); }
protected:
HaierClimateBase *parent_;
};
template<typename... Ts> class PowerToggleAction : public Action<Ts...> {
public:
PowerToggleAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->toggle_power(); }
protected:
HaierClimateBase *parent_;
};
} // namespace haier
} // namespace esphome

View file

@ -1,43 +1,364 @@
from esphome.components import climate import logging
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import uart import esphome.final_validate as fv
from esphome.components.climate import ClimateSwingMode from esphome.components import uart, sensor, climate, logger
from esphome.const import CONF_ID, CONF_SUPPORTED_SWING_MODES from esphome import automation
from esphome.const import (
CONF_BEEPER,
CONF_ID,
CONF_LEVEL,
CONF_LOGGER,
CONF_LOGS,
CONF_MAX_TEMPERATURE,
CONF_MIN_TEMPERATURE,
CONF_PROTOCOL,
CONF_SUPPORTED_MODES,
CONF_SUPPORTED_SWING_MODES,
CONF_VISUAL,
CONF_WIFI,
DEVICE_CLASS_TEMPERATURE,
ICON_THERMOMETER,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
from esphome.components.climate import (
ClimateSwingMode,
ClimateMode,
)
DEPENDENCIES = ["uart"] _LOGGER = logging.getLogger(__name__)
PROTOCOL_MIN_TEMPERATURE = 16.0
PROTOCOL_MAX_TEMPERATURE = 30.0
PROTOCOL_TEMPERATURE_STEP = 1.0
CODEOWNERS = ["@paveldn"]
AUTO_LOAD = ["sensor"]
DEPENDENCIES = ["climate", "uart"]
CONF_WIFI_SIGNAL = "wifi_signal"
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
CONF_VERTICAL_AIRFLOW = "vertical_airflow"
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
PROTOCOL_HON = "HON"
PROTOCOL_SMARTAIR2 = "SMARTAIR2"
PROTOCOLS_SUPPORTED = [PROTOCOL_HON, PROTOCOL_SMARTAIR2]
haier_ns = cg.esphome_ns.namespace("haier") haier_ns = cg.esphome_ns.namespace("haier")
HaierClimate = haier_ns.class_( HaierClimateBase = haier_ns.class_(
"HaierClimate", climate.Climate, cg.PollingComponent, uart.UARTDevice "HaierClimateBase", uart.UARTDevice, climate.Climate, cg.Component
) )
HonClimate = haier_ns.class_("HonClimate", HaierClimateBase)
Smartair2Climate = haier_ns.class_("Smartair2Climate", HaierClimateBase)
ALLOWED_CLIMATE_SWING_MODES = {
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, AirflowVerticalDirection = haier_ns.enum("AirflowVerticalDirection")
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, AIRFLOW_VERTICAL_DIRECTION_OPTIONS = {
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, "UP": AirflowVerticalDirection.UP,
"CENTER": AirflowVerticalDirection.CENTER,
"DOWN": AirflowVerticalDirection.DOWN,
} }
validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True) AirflowHorizontalDirection = haier_ns.enum("AirflowHorizontalDirection")
AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = {
"LEFT": AirflowHorizontalDirection.LEFT,
"CENTER": AirflowHorizontalDirection.CENTER,
"RIGHT": AirflowHorizontalDirection.RIGHT,
}
CONFIG_SCHEMA = cv.All( SUPPORTED_SWING_MODES_OPTIONS = {
"OFF": ClimateSwingMode.CLIMATE_SWING_OFF, # always available
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, # always available
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
}
SUPPORTED_CLIMATE_MODES_OPTIONS = {
"OFF": ClimateMode.CLIMATE_MODE_OFF, # always available
"AUTO": ClimateMode.CLIMATE_MODE_AUTO, # always available
"COOL": ClimateMode.CLIMATE_MODE_COOL,
"HEAT": ClimateMode.CLIMATE_MODE_HEAT,
"DRY": ClimateMode.CLIMATE_MODE_DRY,
"FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
}
def validate_visual(config):
if CONF_VISUAL in config:
visual_config = config[CONF_VISUAL]
if CONF_MIN_TEMPERATURE in visual_config:
min_temp = visual_config[CONF_MIN_TEMPERATURE]
if min_temp < PROTOCOL_MIN_TEMPERATURE:
raise cv.Invalid(
f"Configured visual minimum temperature {min_temp} is lower than supported by Haier protocol is {PROTOCOL_MIN_TEMPERATURE}"
)
else:
config[CONF_VISUAL][CONF_MIN_TEMPERATURE] = PROTOCOL_MIN_TEMPERATURE
if CONF_MAX_TEMPERATURE in visual_config:
max_temp = visual_config[CONF_MAX_TEMPERATURE]
if max_temp > PROTOCOL_MAX_TEMPERATURE:
raise cv.Invalid(
f"Configured visual maximum temperature {max_temp} is higher than supported by Haier protocol is {PROTOCOL_MAX_TEMPERATURE}"
)
else:
config[CONF_VISUAL][CONF_MAX_TEMPERATURE] = PROTOCOL_MAX_TEMPERATURE
else:
config[CONF_VISUAL] = {
CONF_MIN_TEMPERATURE: PROTOCOL_MIN_TEMPERATURE,
CONF_MAX_TEMPERATURE: PROTOCOL_MAX_TEMPERATURE,
}
return config
BASE_CONFIG_SCHEMA = (
climate.CLIMATE_SCHEMA.extend( climate.CLIMATE_SCHEMA.extend(
{ {
cv.GenerateID(): cv.declare_id(HaierClimate), cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list( cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True)
validate_swing_modes
), ),
cv.Optional(
CONF_SUPPORTED_SWING_MODES,
default=[
"OFF",
"VERTICAL",
"HORIZONTAL",
"BOTH",
],
): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)),
} }
) )
.extend(cv.polling_component_schema("5s")) .extend(uart.UART_DEVICE_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA), .extend(cv.COMPONENT_SCHEMA)
) )
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(Smartair2Climate),
}
),
PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(HonClimate),
cv.Optional(CONF_WIFI_SIGNAL, default=True): cv.boolean,
cv.Optional(CONF_BEEPER, default=True): cv.boolean,
cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
}
),
},
key=CONF_PROTOCOL,
default_type=PROTOCOL_SMARTAIR2,
upper=True,
),
validate_visual,
)
# Actions
DisplayOnAction = haier_ns.class_("DisplayOnAction", automation.Action)
DisplayOffAction = haier_ns.class_("DisplayOffAction", automation.Action)
BeeperOnAction = haier_ns.class_("BeeperOnAction", automation.Action)
BeeperOffAction = haier_ns.class_("BeeperOffAction", automation.Action)
StartSelfCleaningAction = haier_ns.class_("StartSelfCleaningAction", automation.Action)
StartSteriCleaningAction = haier_ns.class_(
"StartSteriCleaningAction", automation.Action
)
VerticalAirflowAction = haier_ns.class_("VerticalAirflowAction", automation.Action)
HorizontalAirflowAction = haier_ns.class_("HorizontalAirflowAction", automation.Action)
HealthOnAction = haier_ns.class_("HealthOnAction", automation.Action)
HealthOffAction = haier_ns.class_("HealthOffAction", automation.Action)
PowerOnAction = haier_ns.class_("PowerOnAction", automation.Action)
PowerOffAction = haier_ns.class_("PowerOffAction", automation.Action)
PowerToggleAction = haier_ns.class_("PowerToggleAction", automation.Action)
HAIER_BASE_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(HaierClimateBase),
}
)
HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(HonClimate),
}
)
@automation.register_action(
"climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA
)
@automation.register_action(
"climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA
)
async def display_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_action(
"climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA
)
@automation.register_action(
"climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA
)
async def beeper_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
# Start self cleaning or steri-cleaning action action
@automation.register_action(
"climate.haier.start_self_cleaning",
StartSelfCleaningAction,
HAIER_HON_BASE_ACTION_SCHEMA,
)
@automation.register_action(
"climate.haier.start_steri_cleaning",
StartSteriCleaningAction,
HAIER_HON_BASE_ACTION_SCHEMA,
)
async def start_cleaning_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
# Set vertical airflow direction action
@automation.register_action(
"climate.haier.set_vertical_airflow",
VerticalAirflowAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(HonClimate),
cv.Required(CONF_VERTICAL_AIRFLOW): cv.templatable(
cv.enum(AIRFLOW_VERTICAL_DIRECTION_OPTIONS, upper=True)
),
}
),
)
async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(
config[CONF_VERTICAL_AIRFLOW], args, AirflowVerticalDirection
)
cg.add(var.set_direction(template_))
return var
# Set horizontal airflow direction action
@automation.register_action(
"climate.haier.set_horizontal_airflow",
HorizontalAirflowAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(HonClimate),
cv.Required(CONF_HORIZONTAL_AIRFLOW): cv.templatable(
cv.enum(AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, upper=True)
),
}
),
)
async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(
config[CONF_HORIZONTAL_AIRFLOW], args, AirflowHorizontalDirection
)
cg.add(var.set_direction(template_))
return var
@automation.register_action(
"climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA
)
@automation.register_action(
"climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA
)
async def health_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_action(
"climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA
)
@automation.register_action(
"climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA
)
@automation.register_action(
"climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA
)
async def power_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
def _final_validate(config):
full_config = fv.full_config.get()
if CONF_LOGGER in full_config:
_level = "NONE"
logger_config = full_config[CONF_LOGGER]
if CONF_LOGS in logger_config:
if "haier.protocol" in logger_config[CONF_LOGS]:
_level = logger_config[CONF_LOGS]["haier.protocol"]
else:
_level = logger_config[CONF_LEVEL]
_LOGGER.info("Detected log level for Haier protocol: %s", _level)
if _level not in logger.LOG_LEVEL_SEVERITY:
raise cv.Invalid("Unknown log level for Haier protocol")
_severity = logger.LOG_LEVEL_SEVERITY.index(_level)
cg.add_build_flag(f"-DHAIER_LOG_LEVEL={_severity}")
else:
_LOGGER.info(
"No logger component found, logging for Haier protocol is disabled"
)
cg.add_build_flag("-DHAIER_LOG_LEVEL=0")
if (
(CONF_WIFI_SIGNAL in config)
and (config[CONF_WIFI_SIGNAL])
and CONF_WIFI not in full_config
):
raise cv.Invalid(
f"No WiFi configured, if you want to use haier climate without WiFi add {CONF_WIFI_SIGNAL}: false to climate configuration"
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config): async def to_code(config):
cg.add(haier_ns.init_haier_protocol_logging())
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await climate.register_climate(var, config)
await uart.register_uart_device(var, config) await uart.register_uart_device(var, config)
await climate.register_climate(var, config)
if (CONF_WIFI_SIGNAL in config) and (config[CONF_WIFI_SIGNAL]):
cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL]))
if CONF_BEEPER in config:
cg.add(var.set_beeper_state(config[CONF_BEEPER]))
if CONF_OUTDOOR_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
cg.add(var.set_outdoor_temperature_sensor(sens))
if CONF_SUPPORTED_MODES in config:
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
if CONF_SUPPORTED_SWING_MODES in config: if CONF_SUPPORTED_SWING_MODES in config:
cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
# https://github.com/paveldn/HaierProtocol
cg.add_library("pavlodn/HaierProtocol", "0.9.18")

View file

@ -1,302 +0,0 @@
#include <cmath>
#include "haier.h"
#include "esphome/core/macros.h"
namespace esphome {
namespace haier {
static const char *const TAG = "haier";
static const uint8_t TEMPERATURE = 13;
static const uint8_t HUMIDITY = 15;
static const uint8_t MODE = 23;
static const uint8_t FAN_SPEED = 25;
static const uint8_t SWING = 27;
static const uint8_t POWER = 29;
static const uint8_t POWER_MASK = 1;
static const uint8_t SET_TEMPERATURE = 35;
static const uint8_t DECIMAL_MASK = (1 << 5);
static const uint8_t CRC = 36;
static const uint8_t COMFORT_PRESET_MASK = (1 << 3);
static const uint8_t MIN_VALID_TEMPERATURE = 16;
static const uint8_t MAX_VALID_TEMPERATURE = 50;
static const float TEMPERATURE_STEP = 0.5f;
static const uint8_t POLL_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 1, 90};
static const uint8_t OFF_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 3, 92};
void HaierClimate::dump_config() {
ESP_LOGCONFIG(TAG, "Haier:");
ESP_LOGCONFIG(TAG, " Update interval: %u", this->get_update_interval());
this->dump_traits_(TAG);
this->check_uart_settings(9600);
}
void HaierClimate::loop() {
if (this->available() >= sizeof(this->data_)) {
this->read_array(this->data_, sizeof(this->data_));
if (this->data_[0] != 255 || this->data_[1] != 255)
return;
read_state_(this->data_, sizeof(this->data_));
}
}
void HaierClimate::update() {
this->write_array(POLL_REQ, sizeof(POLL_REQ));
dump_message_("Poll sent", POLL_REQ, sizeof(POLL_REQ));
}
climate::ClimateTraits HaierClimate::traits() {
auto traits = climate::ClimateTraits();
traits.set_visual_min_temperature(MIN_VALID_TEMPERATURE);
traits.set_visual_max_temperature(MAX_VALID_TEMPERATURE);
traits.set_visual_temperature_step(TEMPERATURE_STEP);
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL, climate::CLIMATE_MODE_COOL,
climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY});
traits.set_supported_fan_modes({
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH,
});
traits.set_supported_swing_modes(this->supported_swing_modes_);
traits.set_supports_current_temperature(true);
traits.set_supports_two_point_target_temperature(false);
traits.add_supported_preset(climate::CLIMATE_PRESET_NONE);
traits.add_supported_preset(climate::CLIMATE_PRESET_COMFORT);
return traits;
}
void HaierClimate::read_state_(const uint8_t *data, uint8_t size) {
dump_message_("Received state", data, size);
uint8_t check = data[CRC];
uint8_t crc = get_checksum_(data, size);
if (check != crc) {
ESP_LOGW(TAG, "Invalid checksum");
return;
}
this->current_temperature = data[TEMPERATURE];
this->target_temperature = data[SET_TEMPERATURE] + MIN_VALID_TEMPERATURE;
if (data[POWER] & DECIMAL_MASK) {
this->target_temperature += 0.5f;
}
switch (data[MODE]) {
case MODE_SMART:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
case MODE_COOL:
this->mode = climate::CLIMATE_MODE_COOL;
break;
case MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
case MODE_ONLY_FAN:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
case MODE_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
break;
default: // other modes are unsupported
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
}
switch (data[FAN_SPEED]) {
case FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
case FAN_MIN:
this->fan_mode = climate::CLIMATE_FAN_LOW;
break;
case FAN_MIDDLE:
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
break;
case FAN_MAX:
this->fan_mode = climate::CLIMATE_FAN_HIGH;
break;
}
switch (data[SWING]) {
case SWING_OFF:
this->swing_mode = climate::CLIMATE_SWING_OFF;
break;
case SWING_VERTICAL:
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
break;
case SWING_HORIZONTAL:
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
break;
case SWING_BOTH:
this->swing_mode = climate::CLIMATE_SWING_BOTH;
break;
}
if (data[POWER] & COMFORT_PRESET_MASK) {
this->preset = climate::CLIMATE_PRESET_COMFORT;
} else {
this->preset = climate::CLIMATE_PRESET_NONE;
}
if ((data[POWER] & POWER_MASK) == 0) {
this->mode = climate::CLIMATE_MODE_OFF;
}
this->publish_state();
}
void HaierClimate::control(const climate::ClimateCall &call) {
if (call.get_mode().has_value()) {
switch (call.get_mode().value()) {
case climate::CLIMATE_MODE_OFF:
send_data_(OFF_REQ, sizeof(OFF_REQ));
break;
case climate::CLIMATE_MODE_HEAT_COOL:
case climate::CLIMATE_MODE_AUTO:
data_[POWER] |= POWER_MASK;
data_[MODE] = MODE_SMART;
break;
case climate::CLIMATE_MODE_HEAT:
data_[POWER] |= POWER_MASK;
data_[MODE] = MODE_HEAT;
break;
case climate::CLIMATE_MODE_COOL:
data_[POWER] |= POWER_MASK;
data_[MODE] = MODE_COOL;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
data_[POWER] |= POWER_MASK;
data_[MODE] = MODE_ONLY_FAN;
break;
case climate::CLIMATE_MODE_DRY:
data_[POWER] |= POWER_MASK;
data_[MODE] = MODE_DRY;
break;
}
}
if (call.get_preset().has_value()) {
if (call.get_preset().value() == climate::CLIMATE_PRESET_COMFORT) {
data_[POWER] |= COMFORT_PRESET_MASK;
} else {
data_[POWER] &= ~COMFORT_PRESET_MASK;
}
}
if (call.get_target_temperature().has_value()) {
float target = call.get_target_temperature().value() - MIN_VALID_TEMPERATURE;
data_[SET_TEMPERATURE] = (uint8_t) target;
if ((int) target == std::lroundf(target)) {
data_[POWER] &= ~DECIMAL_MASK;
} else {
data_[POWER] |= DECIMAL_MASK;
}
}
if (call.get_fan_mode().has_value()) {
switch (call.get_fan_mode().value()) {
case climate::CLIMATE_FAN_AUTO:
data_[FAN_SPEED] = FAN_AUTO;
break;
case climate::CLIMATE_FAN_LOW:
data_[FAN_SPEED] = FAN_MIN;
break;
case climate::CLIMATE_FAN_MEDIUM:
data_[FAN_SPEED] = FAN_MIDDLE;
break;
case climate::CLIMATE_FAN_HIGH:
data_[FAN_SPEED] = FAN_MAX;
break;
default: // other modes are unsupported
break;
}
}
if (call.get_swing_mode().has_value()) {
switch (call.get_swing_mode().value()) {
case climate::CLIMATE_SWING_OFF:
data_[SWING] = SWING_OFF;
break;
case climate::CLIMATE_SWING_VERTICAL:
data_[SWING] = SWING_VERTICAL;
break;
case climate::CLIMATE_SWING_HORIZONTAL:
data_[SWING] = SWING_HORIZONTAL;
break;
case climate::CLIMATE_SWING_BOTH:
data_[SWING] = SWING_BOTH;
break;
}
}
// Parts of the message that must have specific values for "send" command.
// The meaning of those values is unknown at the moment.
data_[9] = 1;
data_[10] = 77;
data_[11] = 95;
data_[17] = 0;
// Compute checksum
uint8_t crc = get_checksum_(data_, sizeof(data_));
data_[CRC] = crc;
send_data_(data_, sizeof(data_));
}
void HaierClimate::send_data_(const uint8_t *message, uint8_t size) {
this->write_array(message, size);
dump_message_("Sent message", message, size);
}
void HaierClimate::dump_message_(const char *title, const uint8_t *message, uint8_t size) {
ESP_LOGV(TAG, "%s:", title);
for (int i = 0; i < size; i++) {
ESP_LOGV(TAG, " byte %02d - %d", i, message[i]);
}
}
uint8_t HaierClimate::get_checksum_(const uint8_t *message, size_t size) {
uint8_t position = size - 1;
uint8_t crc = 0;
for (int i = 2; i < position; i++)
crc += message[i];
return crc;
}
} // namespace haier
} // namespace esphome

View file

@ -1,37 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace haier {
enum Mode : uint8_t { MODE_SMART = 0, MODE_COOL = 1, MODE_HEAT = 2, MODE_ONLY_FAN = 3, MODE_DRY = 4 };
enum FanSpeed : uint8_t { FAN_MAX = 0, FAN_MIDDLE = 1, FAN_MIN = 2, FAN_AUTO = 3 };
enum SwingMode : uint8_t { SWING_OFF = 0, SWING_VERTICAL = 1, SWING_HORIZONTAL = 2, SWING_BOTH = 3 };
class HaierClimate : public climate::Climate, public uart::UARTDevice, public PollingComponent {
public:
void loop() override;
void update() override;
void dump_config() override;
void control(const climate::ClimateCall &call) override;
void set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
this->supported_swing_modes_ = modes;
}
protected:
climate::ClimateTraits traits() override;
void read_state_(const uint8_t *data, uint8_t size);
void send_data_(const uint8_t *message, uint8_t size);
void dump_message_(const char *title, const uint8_t *message, uint8_t size);
uint8_t get_checksum_(const uint8_t *message, size_t size);
private:
uint8_t data_[37];
std::set<climate::ClimateSwingMode> supported_swing_modes_{};
};
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,311 @@
#include <chrono>
#include <string>
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#include "haier_base.h"
using namespace esphome::climate;
using namespace esphome::uart;
namespace esphome {
namespace haier {
static const char *const TAG = "haier.climate";
constexpr size_t COMMUNICATION_TIMEOUT_MS = 60000;
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_ANSWER_INIT_1",
"SENDING_INIT_2",
"WAITING_ANSWER_INIT_2",
"SENDING_FIRST_STATUS_REQUEST",
"WAITING_FIRST_STATUS_ANSWER",
"SENDING_ALARM_STATUS_REQUEST",
"WAITING_ALARM_STATUS_ANSWER",
"IDLE",
"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",
"UNKNOWN" // Should be the last!
};
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
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) {
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,
climate::CLIMATE_MODE_AUTO});
this->traits_.set_supported_fan_modes(
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
this->traits_.set_supports_current_temperature(true);
}
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;
}
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);
}
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);
}
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);
}
bool HaierClimateBase::is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL);
}
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);
}
}
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);
}
}
void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; }
void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; }
void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; }
void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
this->traits_.set_supported_swing_modes(modes);
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); // Always available
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); // Always available
}
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) {
this->traits_.set_supported_modes(modes);
this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available
this->traits_.add_supported_mode(climate::CLIMATE_MODE_AUTO); // Always available
}
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,
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))
result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type))
result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_))
result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
if (is_message_invalid(answer_message_type))
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
if (this->protocol_phase_ > ProtocolPhases::IDLE) {
this->set_phase_(ProtocolPhases::IDLE);
} else {
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
}
return haier_protocol::HandlerError::HANDLER_OK;
}
void HaierClimateBase::setup() {
ESP_LOGI(TAG, "Haier initialization...");
// 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_answers_handlers();
this->haier_protocol_.set_default_timeout_handler(
std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1));
}
void HaierClimateBase::dump_config() {
LOG_CLIMATE("", "Haier Climate", this);
ESP_LOGCONFIG(TAG, " Device communication status: %s",
(this->protocol_phase_ >= ProtocolPhases::IDLE) ? "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_)) {
if (this->protocol_phase_ >= ProtocolPhases::IDLE) {
// No status too long, reseting protocol
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);
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) ||
(this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) ||
(this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) ||
(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_) {
ESP_LOGV(TAG, "Control packet is pending...");
this->set_phase_(ProtocolPhases::SENDING_CONTROL);
}
}
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;
}
switch (request) {
case ActionRequest::TURN_POWER_ON:
this->set_phase_(ProtocolPhases::SENDING_POWER_ON_COMMAND);
break;
case ActionRequest::TURN_POWER_OFF:
this->set_phase_(ProtocolPhases::SENDING_POWER_OFF_COMMAND);
break;
case ActionRequest::TOGGLE_POWER:
case ActionRequest::NO_ACTION:
// shouldn't get here, do nothing
break;
default:
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_);
break;
}
this->action_request_ = ActionRequest::NO_ACTION;
}
ClimateTraits HaierClimateBase::traits() { return traits_; }
void HaierClimateBase::control(const ClimateCall &call) {
ESP_LOGD("Control", "Control call");
if (this->protocol_phase_ < ProtocolPhases::IDLE) {
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 (call.get_mode().has_value())
this->hvac_settings_.mode = call.get_mode();
if (call.get_fan_mode().has_value())
this->hvac_settings_.fan_mode = call.get_fan_mode();
if (call.get_swing_mode().has_value())
this->hvac_settings_.swing_mode = call.get_swing_mode();
if (call.get_target_temperature().has_value())
this->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->first_control_attempt_ = true;
}
void HaierClimateBase::HvacSettings::reset() {
this->valid = false;
this->mode.reset();
this->fan_mode.reset();
this->swing_mode.reset();
this->target_temperature.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) {
this->haier_protocol_.send_message(command, use_crc);
this->last_request_timestamp_ = std::chrono::steady_clock::now();
}
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,142 @@
#pragma once
#include <chrono>
#include <set>
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
// HaierProtocol
#include <protocol/haier_protocol.h>
namespace esphome {
namespace haier {
enum class ActionRequest : uint8_t {
NO_ACTION = 0,
TURN_POWER_ON = 1,
TURN_POWER_OFF = 2,
TOGGLE_POWER = 3,
START_SELF_CLEAN = 4, // only hOn
START_STERI_CLEAN = 5, // only hOn
};
class HaierClimateBase : public esphome::Component,
public esphome::climate::Climate,
public esphome::uart::UARTDevice,
public haier_protocol::ProtocolStream {
public:
HaierClimateBase();
HaierClimateBase(const HaierClimateBase &) = delete;
HaierClimateBase &operator=(const HaierClimateBase &) = delete;
~HaierClimateBase();
void setup() override;
void loop() override;
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);
bool get_health_mode() const;
void send_power_on_command();
void send_power_off_command();
void toggle_power();
void reset_protocol() { this->reset_protocol_request_ = true; };
void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes);
void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes);
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;
};
void write_array(const uint8_t *data, size_t len) noexcept override {
esphome::uart::UARTDevice::write_array(data, len);
};
bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; };
protected:
enum class ProtocolPhases {
UNKNOWN = -1,
// INITIALIZATION
SENDING_INIT_1 = 0,
WAITING_ANSWER_INIT_1 = 1,
SENDING_INIT_2 = 2,
WAITING_ANSWER_INIT_2 = 3,
SENDING_FIRST_STATUS_REQUEST = 4,
WAITING_FIRST_STATUS_ANSWER = 5,
SENDING_ALARM_STATUS_REQUEST = 6,
WAITING_ALARM_STATUS_ANSWER = 7,
// FUNCTIONAL STATE
IDLE = 8,
SENDING_STATUS_REQUEST = 9,
WAITING_STATUS_ANSWER = 10,
SENDING_UPDATE_SIGNAL_REQUEST = 11,
WAITING_UPDATE_SIGNAL_ANSWER = 12,
SENDING_SIGNAL_LEVEL = 13,
WAITING_SIGNAL_LEVEL_ANSWER = 14,
SENDING_CONTROL = 15,
WAITING_CONTROL_ANSWER = 16,
SENDING_POWER_ON_COMMAND = 17,
WAITING_POWER_ON_ANSWER = 18,
SENDING_POWER_OFF_COMMAND = 19,
WAITING_POWER_OFF_ANSWER = 20,
NUM_PROTOCOL_PHASES
};
#if (HAIER_LOG_LEVEL > 4)
const char *phase_to_string_(ProtocolPhases phase);
#endif
virtual void set_answers_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();
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,
ProtocolPhases expected_phase);
// Timeout handler
haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type);
// Helper functions
void set_force_send_control_(bool status);
void send_message_(const haier_protocol::HaierMessage &command, bool use_crc);
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);
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_exceded_(std::chrono::steady_clock::time_point now);
struct HvacSettings {
esphome::optional<esphome::climate::ClimateMode> mode;
esphome::optional<esphome::climate::ClimateFanMode> fan_mode;
esphome::optional<esphome::climate::ClimateSwingMode> swing_mode;
esphome::optional<float> target_temperature;
esphome::optional<esphome::climate::ClimatePreset> preset;
bool valid;
HvacSettings() : valid(false){};
void reset();
};
haier_protocol::ProtocolHandler haier_protocol_;
ProtocolPhases protocol_phase_;
ActionRequest 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_;
esphome::climate::ClimateTraits traits_;
HvacSettings hvac_settings_;
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
};
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,857 @@
#include <chrono>
#include <string>
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#ifdef USE_WIFI
#include "esphome/components/wifi/wifi_component.h"
#endif
#include "hon_climate.h"
#include "hon_packet.h"
using namespace esphome::climate;
using namespace esphome::uart;
namespace esphome {
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;
hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) {
switch (direction) {
case AirflowVerticalDirection::HEALTH_UP:
return hon_protocol::VerticalSwingMode::HEALTH_UP;
case AirflowVerticalDirection::MAX_UP:
return hon_protocol::VerticalSwingMode::MAX_UP;
case AirflowVerticalDirection::UP:
return hon_protocol::VerticalSwingMode::UP;
case AirflowVerticalDirection::DOWN:
return hon_protocol::VerticalSwingMode::DOWN;
case AirflowVerticalDirection::HEALTH_DOWN:
return hon_protocol::VerticalSwingMode::HEALTH_DOWN;
default:
return hon_protocol::VerticalSwingMode::CENTER;
}
}
hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDirection direction) {
switch (direction) {
case AirflowHorizontalDirection::MAX_LEFT:
return hon_protocol::HorizontalSwingMode::MAX_LEFT;
case AirflowHorizontalDirection::LEFT:
return hon_protocol::HorizontalSwingMode::LEFT;
case AirflowHorizontalDirection::RIGHT:
return hon_protocol::HorizontalSwingMode::RIGHT;
case AirflowHorizontalDirection::MAX_RIGHT:
return hon_protocol::HorizontalSwingMode::MAX_RIGHT;
default:
return hon_protocol::HorizontalSwingMode::CENTER;
}
}
HonClimate::HonClimate()
: last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]),
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),
send_wifi_signal_(true) {
this->traits_.set_supported_presets({
climate::CLIMATE_PRESET_NONE,
climate::CLIMATE_PRESET_ECO,
climate::CLIMATE_PRESET_BOOST,
climate::CLIMATE_PRESET_SLEEP,
});
this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID;
this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
}
HonClimate::~HonClimate() {}
void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; }
bool HonClimate::get_beeper_state() const { return this->beeper_status_; }
void HonClimate::set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; }
AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this->vertical_direction_; };
void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) {
if (direction > AirflowVerticalDirection::DOWN) {
this->vertical_direction_ = AirflowVerticalDirection::CENTER;
} else {
this->vertical_direction_ = direction;
}
this->set_force_send_control_(true);
}
AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; }
void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) {
if (direction > AirflowHorizontalDirection::RIGHT) {
this->horizontal_direction_ = AirflowHorizontalDirection::CENTER;
} else {
this->horizontal_direction_ = direction;
}
this->set_force_send_control_(true);
}
std::string HonClimate::get_cleaning_status_text() const {
switch (this->cleaning_status_) {
case CleaningState::SELF_CLEAN:
return "Self clean";
case CleaningState::STERI_CLEAN:
return "56°C Steri-Clean";
default:
return "No cleaning";
}
}
CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_status_; }
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);
}
}
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);
}
}
void HonClimate::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; }
haier_protocol::HandlerError HonClimate::get_device_version_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) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type,
(uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_ANSWER_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
hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data;
char tmp[9];
tmp[8] = 0;
strncpy(tmp, answr->protocol_version, 8);
this->hvac_protocol_version_ = std::string(tmp);
strncpy(tmp, answr->software_version, 8);
this->hvac_software_version_ = std::string(tmp);
strncpy(tmp, answr->hardware_version, 8);
this->hvac_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->set_phase_(ProtocolPhases::SENDING_INIT_2);
return result;
} else {
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
: ProtocolPhases::SENDING_INIT_1);
return result;
}
}
haier_protocol::HandlerError HonClimate::get_device_id_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) hon_protocol::FrameType::GET_DEVICE_ID, message_type,
(uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_ANSWER_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);
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 result =
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type,
(uint8_t) hon_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);
} else {
if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) {
memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl));
} else {
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) {
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)) {
this->set_phase_(ProtocolPhases::IDLE);
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) {
this->set_phase_(ProtocolPhases::IDLE);
this->set_force_send_control_(false);
if (this->hvac_settings_.valid)
this->hvac_settings_.reset();
}
}
return result;
} else {
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
: ProtocolPhases::SENDING_INIT_1);
return result;
}
}
haier_protocol::HandlerError HonClimate::get_management_information_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) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION,
message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE,
ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
if (result == haier_protocol::HandlerError::HANDLER_OK) {
this->set_phase_(ProtocolPhases::SENDING_SIGNAL_LEVEL);
return result;
} else {
this->set_phase_(ProtocolPhases::IDLE);
return result;
}
}
haier_protocol::HandlerError HonClimate::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) 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) {
// Unexpected answer to request
this->set_phase_(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
}
if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) {
// Don't expect this answer now
this->set_phase_(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
}
memcpy(this->active_alarms_, data + 2, 8);
this->set_phase_(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::HANDLER_OK;
} else {
this->set_phase_(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
}
}
void HonClimate::set_answers_handlers() {
// Set handlers
this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_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),
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),
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),
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),
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),
std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
}
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, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
}
}
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_exceded_(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
// bit 2 - if 1 module support crc
// bit 3 - if 1 module support multiple devices
// 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));
this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_);
this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_1);
}
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);
this->send_message_(DEVICEID_REQUEST, this->use_crc_);
this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_2);
}
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::SubcomandsControl::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);
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)) {
static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00};
if (wifi::global_wifi_component->is_connected()) {
wifi_status_data[1] = 0;
int8_t rssi = wifi::global_wifi_component->wifi_rssi();
wifi_status_data[3] = uint8_t((128 + rssi) / 1.28f);
ESP_LOGD(TAG, "WiFi signal is: %ddBm => %d%%", rssi, wifi_status_data[3]);
} else {
ESP_LOGD(TAG, "WiFi is not connected");
wifi_status_data[1] = 1;
wifi_status_data[3] = 0;
}
haier_protocol::HaierMessage wifi_status_request((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS,
wifi_status_data, sizeof(wifi_status_data));
this->send_message_(wifi_status_request, this->use_crc_);
this->set_phase_(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
}
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);
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->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);
} 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);
}
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::SubcomandsControl::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);
}
break;
case ProtocolPhases::WAITING_ANSWER_INIT_1:
case ProtocolPhases::WAITING_ANSWER_INIT_2:
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)) {
this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST);
this->forced_request_status_ = false;
}
#ifdef USE_WIFI
else if (this->send_wifi_signal_ &&
(std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
SIGNAL_LEVEL_UPDATE_INTERVAL_MS))
this->set_phase_(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
#endif
} 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_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) {
has_hvac_settings = true;
HvacSettings climate_control;
climate_control = this->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_AUTO:
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_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) hon_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) hon_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) hon_protocol::ConditioningMode::FAN;
out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode
// Disabling boost and eco mode for Fan only
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
break;
case CLIMATE_MODE_COOL:
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::COOL;
out_data->fan_mode = this->other_modes_fan_speed_;
break;
default:
ESP_LOGE("Control", "Unsupported climate mode");
break;
}
}
// Set fan speed, if we are in fan mode, reject auto in fan mode
if (climate_control.fan_mode.has_value()) {
switch (climate_control.fan_mode.value()) {
case CLIMATE_FAN_LOW:
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_LOW;
break;
case CLIMATE_FAN_MEDIUM:
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_MID;
break;
case CLIMATE_FAN_HIGH:
out_data->fan_mode = (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
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
break;
default:
ESP_LOGE("Control", "Unsupported fan mode");
break;
}
}
// Set swing mode
if (climate_control.swing_mode.has_value()) {
switch (climate_control.swing_mode.value()) {
case CLIMATE_SWING_OFF:
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
break;
case CLIMATE_SWING_VERTICAL:
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO;
break;
case CLIMATE_SWING_HORIZONTAL:
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO;
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
break;
case CLIMATE_SWING_BOTH:
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO;
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO;
break;
}
}
if (climate_control.target_temperature.has_value()) {
out_data->set_point =
climate_control.target_temperature.value() - 16; // set the temperature at our offset, subtract 16.
}
if (out_data->ac_power == 0) {
// If AC is off - no presets alowed
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
} else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
break;
case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode
out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
break;
case CLIMATE_PRESET_BOOST:
out_data->quiet_mode = 0;
// Boost is not supported in Fan only mode
out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->sleep_mode = 0;
break;
case CLIMATE_PRESET_AWAY:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
break;
case CLIMATE_PRESET_SLEEP:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 1;
break;
default:
ESP_LOGE("Control", "Unsupported preset");
break;
}
}
} else {
if (out_data->vertical_swing_mode != (uint8_t) hon_protocol::VerticalSwingMode::AUTO)
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
if (out_data->horizontal_swing_mode != (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
}
out_data->beeper_status = ((!this->beeper_status_) || (!has_hvac_settings)) ? 1 : 0;
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,
(uint16_t) hon_protocol::SubcomandsControl::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))
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);
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;
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);
}
bool should_publish = false;
{
// Extra modes/presets
optional<ClimatePreset> old_preset = this->preset;
if (packet.control.quiet_mode != 0) {
this->preset = CLIMATE_PRESET_ECO;
} else if (packet.control.fast_mode != 0) {
this->preset = CLIMATE_PRESET_BOOST;
} else if (packet.control.sleep_mode != 0) {
this->preset = CLIMATE_PRESET_SLEEP;
} else {
this->preset = CLIMATE_PRESET_NONE;
}
should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value());
}
{
// Target temperature
float old_target_temperature = this->target_temperature;
this->target_temperature = packet.control.set_point + 16.0f;
should_publish = should_publish || (old_target_temperature != this->target_temperature);
}
{
// Current temperature
float old_current_temperature = this->current_temperature;
this->current_temperature = packet.sensors.room_temperature / 2.0f;
should_publish = should_publish || (old_current_temperature != this->current_temperature);
}
{
// Fan mode
optional<ClimateFanMode> old_fan_mode = this->fan_mode;
// remember the fan speed we last had for climate vs fan
if (packet.control.ac_mode == (uint8_t) hon_protocol::ConditioningMode::FAN) {
if (packet.control.fan_mode != (uint8_t) hon_protocol::FanMode::FAN_AUTO)
this->fan_mode_speed_ = packet.control.fan_mode;
} else {
this->other_modes_fan_speed_ = packet.control.fan_mode;
}
switch (packet.control.fan_mode) {
case (uint8_t) hon_protocol::FanMode::FAN_AUTO:
if (packet.control.ac_mode != (uint8_t) hon_protocol::ConditioningMode::FAN) {
this->fan_mode = CLIMATE_FAN_AUTO;
} else {
// Shouldn't accept fan speed auto in fan-only mode even if AC reports it
ESP_LOGI(TAG, "Fan speed Auto is not supported in Fan only AC mode, ignoring");
}
break;
case (uint8_t) hon_protocol::FanMode::FAN_MID:
this->fan_mode = CLIMATE_FAN_MEDIUM;
break;
case (uint8_t) hon_protocol::FanMode::FAN_LOW:
this->fan_mode = CLIMATE_FAN_LOW;
break;
case (uint8_t) hon_protocol::FanMode::FAN_HIGH:
this->fan_mode = CLIMATE_FAN_HIGH;
break;
}
should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
}
{
// Display status
// should be before "Climate mode" because it is changing this->mode
if (packet.control.ac_power != 0) {
// if AC is off display status always ON so process it only when AC is on
bool disp_status = packet.control.display_status != 0;
if (disp_status != this->display_status_) {
// 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);
} 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_);
}
{
CleaningState new_cleaning;
if (packet.control.steri_clean == 1) {
// Steri-cleaning
new_cleaning = CleaningState::STERI_CLEAN;
} else if (packet.control.self_cleaning_status == 1) {
// Self-cleaning
new_cleaning = CleaningState::SELF_CLEAN;
} else {
// No cleaning
new_cleaning = CleaningState::NO_CLEANING;
}
if (new_cleaning != this->cleaning_status_) {
ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning);
if (new_cleaning == CleaningState::NO_CLEANING) {
// Turnuin AC off after cleaning
this->action_request_ = ActionRequest::TURN_POWER_OFF;
}
this->cleaning_status_ = new_cleaning;
}
}
{
// Climate mode
ClimateMode old_mode = this->mode;
if (packet.control.ac_power == 0) {
this->mode = CLIMATE_MODE_OFF;
} else {
// Check current hvac mode
switch (packet.control.ac_mode) {
case (uint8_t) hon_protocol::ConditioningMode::COOL:
this->mode = CLIMATE_MODE_COOL;
break;
case (uint8_t) hon_protocol::ConditioningMode::HEAT:
this->mode = CLIMATE_MODE_HEAT;
break;
case (uint8_t) hon_protocol::ConditioningMode::DRY:
this->mode = CLIMATE_MODE_DRY;
break;
case (uint8_t) hon_protocol::ConditioningMode::FAN:
this->mode = CLIMATE_MODE_FAN_ONLY;
break;
case (uint8_t) hon_protocol::ConditioningMode::AUTO:
this->mode = CLIMATE_MODE_AUTO;
break;
}
}
should_publish = should_publish || (old_mode != this->mode);
}
{
// Swing mode
ClimateSwingMode old_swing_mode = this->swing_mode;
if (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) {
if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
this->swing_mode = CLIMATE_SWING_BOTH;
} else {
this->swing_mode = CLIMATE_SWING_HORIZONTAL;
}
} else {
if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
this->swing_mode = CLIMATE_SWING_VERTICAL;
} else {
this->swing_mode = CLIMATE_SWING_OFF;
}
}
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
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);
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::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);
break;
default:
HaierClimateBase::process_pending_action();
break;
}
}
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,95 @@
#pragma once
#include <chrono>
#include "esphome/components/sensor/sensor.h"
#include "haier_base.h"
namespace esphome {
namespace haier {
enum class AirflowVerticalDirection : uint8_t {
HEALTH_UP = 0,
MAX_UP = 1,
UP = 2,
CENTER = 3,
DOWN = 4,
HEALTH_DOWN = 5,
};
enum class AirflowHorizontalDirection : uint8_t {
MAX_LEFT = 0,
LEFT = 1,
CENTER = 2,
RIGHT = 3,
MAX_RIGHT = 4,
};
enum class CleaningState : uint8_t {
NO_CLEANING = 0,
SELF_CLEAN = 1,
STERI_CLEAN = 2,
};
class HonClimate : public HaierClimateBase {
public:
HonClimate();
HonClimate(const HonClimate &) = delete;
HonClimate &operator=(const HonClimate &) = delete;
~HonClimate();
void dump_config() override;
void set_beeper_state(bool state);
bool get_beeper_state() const;
void set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor);
AirflowVerticalDirection get_vertical_airflow() const;
void set_vertical_airflow(AirflowVerticalDirection direction);
AirflowHorizontalDirection get_horizontal_airflow() const;
void set_horizontal_airflow(AirflowHorizontalDirection direction);
std::string get_cleaning_status_text() const;
CleaningState get_cleaning_status() const;
void start_self_cleaning();
void start_steri_cleaning();
void set_send_wifi(bool send_wifi);
protected:
void set_answers_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;
// Answers handlers
haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t 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,
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,
size_t data_size);
haier_protocol::HandlerError get_management_information_answer_handler_(uint8_t request_type, uint8_t 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,
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_;
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_;
uint8_t active_alarms_[8];
esphome::sensor::Sensor *outdoor_sensor_;
bool send_wifi_signal_;
std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level
};
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,228 @@
#pragma once
#include <cstdint>
namespace esphome {
namespace haier {
namespace hon_protocol {
enum class VerticalSwingMode : uint8_t {
HEALTH_UP = 0x01,
MAX_UP = 0x02,
HEALTH_DOWN = 0x03,
UP = 0x04,
CENTER = 0x06,
DOWN = 0x08,
AUTO = 0x0C
};
enum class HorizontalSwingMode : uint8_t {
CENTER = 0x00,
MAX_LEFT = 0x03,
LEFT = 0x04,
RIGHT = 0x05,
MAX_RIGHT = 0x06,
AUTO = 0x07
};
enum class ConditioningMode : uint8_t {
AUTO = 0x00,
COOL = 0x01,
DRY = 0x02,
HEALTHY_DRY = 0x03,
HEAT = 0x04,
ENERGY_SAVING = 0x05,
FAN = 0x06
};
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 };
struct HaierPacketControl {
// Control bytes starts here
// 10
uint8_t set_point; // Target temperature with 16°C offset (0x00 = 16°C)
// 11
uint8_t vertical_swing_mode : 4; // See enum VerticalSwingMode
uint8_t : 0;
// 12
uint8_t fan_mode : 3; // See enum FanMode
uint8_t special_mode : 2; // See enum SpecialMode
uint8_t ac_mode : 3; // See enum ConditioningMode
// 13
uint8_t : 8;
// 14
uint8_t ten_degree : 1; // 10 degree status
uint8_t display_status : 1; // If 0 disables AC's display
uint8_t half_degree : 1; // Use half degree
uint8_t intelegence_status : 1; // Intelligence status
uint8_t pmv_status : 1; // Comfort/PMV status
uint8_t use_fahrenheit : 1; // Use Fahrenheit instead of Celsius
uint8_t : 1;
uint8_t steri_clean : 1;
// 15
uint8_t ac_power : 1; // Is ac on or off
uint8_t health_mode : 1; // Health mode (negative ions) on or off
uint8_t electric_heating_status : 1; // Electric heating status
uint8_t fast_mode : 1; // Fast mode
uint8_t quiet_mode : 1; // Quiet mode
uint8_t sleep_mode : 1; // Sleep mode
uint8_t lock_remote : 1; // Disable remote
uint8_t beeper_status : 1; // If 1 disables AC's command feedback beeper (need to be set on every control command)
// 16
uint8_t target_humidity; // Target humidity (0=30% .. 3C=90%, step = 1%)
// 17
uint8_t horizontal_swing_mode : 3; // See enum HorizontalSwingMode
uint8_t : 3;
uint8_t human_sensing_status : 2; // Human sensing status
// 18
uint8_t change_filter : 1; // Filter need replacement
uint8_t : 0;
// 19
uint8_t fresh_air_status : 1; // Fresh air status
uint8_t humidification_status : 1; // Humidification status
uint8_t pm2p5_cleaning_status : 1; // PM2.5 cleaning status
uint8_t ch2o_cleaning_status : 1; // CH2O cleaning status
uint8_t self_cleaning_status : 1; // Self cleaning status
uint8_t light_status : 1; // Light status
uint8_t energy_saving_status : 1; // Energy saving status
uint8_t cleaning_time_status : 1; // Cleaning time (0 - accumulation, 1 - clear)
};
struct HaierPacketSensors {
// 20
uint8_t room_temperature; // 0.5°C step
// 21
uint8_t room_humidity; // 0%-100% with 1% step
// 22
uint8_t outdoor_temperature; // 1°C step, -64°C offset (0=-64°C)
// 23
uint8_t pm2p5_level : 2; // Indoor PM2.5 grade (00: Excellent, 01: good, 02: Medium, 03: Bad)
uint8_t air_quality : 2; // Air quality grade (00: Excellent, 01: good, 02: Medium, 03: Bad)
uint8_t human_sensing : 2; // Human presence result (00: N/A, 01: not detected, 02: One, 03: Multiple)
uint8_t : 1;
uint8_t ac_type : 1; // 00 - Heat and cool, 01 - Cool only)
// 24
uint8_t error_status; // See enum ErrorStatus
// 25
uint8_t operation_source : 2; // who is controlling AC (00: Other, 01: Remote control, 02: Button, 03: ESP)
uint8_t operation_mode_hk : 2; // Homekit only, operation mode (00: Cool, 01: Dry, 02: Heat, 03: Fan)
uint8_t : 3;
uint8_t err_confirmation : 1; // If 1 clear error status
// 26
uint16_t total_cleaning_time; // Cleaning cumulative time (1h step)
// 28
uint16_t indoor_pm2p5_value; // Indoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step)
// 30
uint16_t outdoor_pm2p5_value; // Outdoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step)
// 32
uint16_t ch2o_value; // Formaldehyde value (0 ug/m3 - 10000 ug/m3, 1 ug/m3 step)
// 34
uint16_t voc_value; // VOC value (Volatile Organic Compounds) (0 ug/m3 - 1023 ug/m3, 1 ug/m3 step)
// 36
uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step)
};
struct HaierStatus {
uint16_t subcommand;
HaierPacketControl control;
HaierPacketSensors sensors;
};
struct DeviceVersionAnswer {
char protocol_version[8];
char software_version[8];
uint8_t encryption[3];
char hardware_version[8];
uint8_t : 8;
char device_name[8];
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_DOWNLIK = 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 SubcomandsControl : 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)
GET_BIG_DATA = 0x4DFE, // Request big data information from device (packet content: None)
SET_PARAMETERS = 0x5C01, // Set parameters of the device and device return parameters (packet content: parameter ID1
// + parameter data1 + parameter ID2 + parameter data 2 + ...)
SET_SINGLE_PARAMETER = 0x5D00, // Set single parameter (0x5DXX second byte define parameter ID) and return all user
// data (packet content: ???)
SET_GROUP_PARAMETERS = 0x6001, // Set group parameters to device (0x60XX second byte define parameter is group ID,
// the only group mentioned in document is 1) and return all user data (packet
// content: all values like in status packet)
};
} // namespace hon_protocol
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,33 @@
#include "logger_handler.h"
#include "esphome/core/log.h"
namespace esphome {
namespace haier {
void esphome_logger(haier_protocol::HaierLogLevel level, const char *tag, const char *message) {
switch (level) {
case haier_protocol::HaierLogLevel::LEVEL_ERROR:
esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, "%s", message);
break;
case haier_protocol::HaierLogLevel::LEVEL_WARNING:
esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, "%s", message);
break;
case haier_protocol::HaierLogLevel::LEVEL_INFO:
esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, "%s", message);
break;
case haier_protocol::HaierLogLevel::LEVEL_DEBUG:
esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, "%s", message);
break;
case haier_protocol::HaierLogLevel::LEVEL_VERBOSE:
esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, "%s", message);
break;
default:
// Just ignore everything else
break;
}
}
void init_haier_protocol_logging() { haier_protocol::set_log_handler(esphome::haier::esphome_logger); };
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,14 @@
#pragma once
// HaierProtocol
#include <utils/haier_log.h>
namespace esphome {
namespace haier {
// This file is called in the code generated by python script
// Do not use it directly!
void init_haier_protocol_logging();
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,457 @@
#include <chrono>
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#include "smartair2_climate.h"
#include "smartair2_packet.h"
using namespace esphome::climate;
using namespace esphome::uart;
namespace esphome {
namespace haier {
static const char *const TAG = "haier.climate";
Smartair2Climate::Smartair2Climate()
: last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]) {
this->traits_.set_supported_presets({
climate::CLIMATE_PRESET_NONE,
climate::CLIMATE_PRESET_BOOST,
climate::CLIMATE_PRESET_COMFORT,
});
}
haier_protocol::HandlerError Smartair2Climate::status_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::CONTROL, message_type,
(uint8_t) smartair2_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);
} else {
if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) {
memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl));
} else {
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) {
ESP_LOGI(TAG, "First HVAC status received");
this->set_phase_(ProtocolPhases::IDLE);
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) {
this->set_phase_(ProtocolPhases::IDLE);
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) {
this->set_phase_(ProtocolPhases::IDLE);
this->set_force_send_control_(false);
if (this->hvac_settings_.valid)
this->hvac_settings_.reset();
}
}
return result;
} else {
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
: ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
return result;
}
}
void Smartair2Climate::set_answers_handlers() {
this->haier_protocol_.set_answer_handler(
(uint8_t) (smartair2_protocol::FrameType::CONTROL),
std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
}
void Smartair2Climate::dump_config() {
HaierClimateBase::dump_config();
ESP_LOGCONFIG(TAG, " Protocol version: smartAir2");
}
void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) {
switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_INIT_1:
this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
break;
case ProtocolPhases::WAITING_ANSWER_INIT_1:
case ProtocolPhases::SENDING_INIT_2:
case ProtocolPhases::WAITING_ANSWER_INIT_2:
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER:
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
break;
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
case ProtocolPhases::SENDING_SIGNAL_LEVEL:
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
this->set_phase_(ProtocolPhases::IDLE);
break;
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) {
static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL,
0x4D01);
this->send_message_(STATUS_REQUEST, false);
this->last_status_request_ = now;
this->set_phase_(ProtocolPhases::WAITING_FIRST_STATUS_ANSWER);
}
break;
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);
this->last_status_request_ = now;
this->set_phase_(ProtocolPhases::WAITING_STATUS_ANSWER);
}
break;
case ProtocolPhases::SENDING_CONTROL:
if (this->first_control_attempt_) {
this->control_request_timestamp_ = now;
this->first_control_attempt_ = false;
}
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);
} 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);
}
break;
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);
this->forced_request_status_ = false;
}
} break;
default:
// Shouldn't get here
ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_);
this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
break;
}
}
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 (climate_control.mode.has_value()) {
switch (climate_control.mode.value()) {
case CLIMATE_MODE_OFF:
out_data->ac_power = 0;
break;
case CLIMATE_MODE_AUTO:
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;
out_data->fan_mode = this->other_modes_fan_speed_;
break;
default:
ESP_LOGE("Control", "Unsupported climate mode");
break;
}
}
// Set fan speed, if we are in fan mode, reject auto in fan mode
if (climate_control.fan_mode.has_value()) {
switch (climate_control.fan_mode.value()) {
case CLIMATE_FAN_LOW:
out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_LOW;
break;
case CLIMATE_FAN_MEDIUM:
out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_MID;
break;
case CLIMATE_FAN_HIGH:
out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_HIGH;
break;
case CLIMATE_FAN_AUTO:
if (this->mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode
out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_AUTO;
break;
default:
ESP_LOGE("Control", "Unsupported fan mode");
break;
}
}
// Set swing mode
if (climate_control.swing_mode.has_value()) {
switch (climate_control.swing_mode.value()) {
case CLIMATE_SWING_OFF:
out_data->use_swing_bits = 0;
out_data->swing_both = 0;
break;
case CLIMATE_SWING_VERTICAL:
out_data->swing_both = 0;
out_data->vertical_swing = 1;
out_data->horizontal_swing = 0;
break;
case CLIMATE_SWING_HORIZONTAL:
out_data->swing_both = 0;
out_data->vertical_swing = 0;
out_data->horizontal_swing = 1;
break;
case CLIMATE_SWING_BOTH:
out_data->swing_both = 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()) {
out_data->set_point =
climate_control.target_temperature.value() - 16; // set the temperature at our offset, subtract 16.
}
if (out_data->ac_power == 0) {
// If AC is off - no presets alowed
out_data->turbo_mode = 0;
out_data->quiet_mode = 0;
} else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE:
out_data->turbo_mode = 0;
out_data->quiet_mode = 0;
break;
case CLIMATE_PRESET_BOOST:
out_data->turbo_mode = 1;
out_data->quiet_mode = 0;
break;
case CLIMATE_PRESET_COMFORT:
out_data->turbo_mode = 0;
out_data->quiet_mode = 1;
break;
default:
ESP_LOGE("Control", "Unsupported preset");
out_data->turbo_mode = 0;
out_data->quiet_mode = 0;
break;
}
}
}
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,
sizeof(smartair2_protocol::HaierPacketControl));
}
haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
if (size < sizeof(smartair2_protocol::HaierStatus))
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
smartair2_protocol::HaierStatus packet;
memcpy(&packet, packet_buffer, size);
bool should_publish = false;
{
// Extra modes/presets
optional<ClimatePreset> old_preset = this->preset;
if (packet.control.turbo_mode != 0) {
this->preset = CLIMATE_PRESET_BOOST;
} else if (packet.control.quiet_mode != 0) {
this->preset = CLIMATE_PRESET_COMFORT;
} else {
this->preset = CLIMATE_PRESET_NONE;
}
should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value());
}
{
// Target temperature
float old_target_temperature = this->target_temperature;
this->target_temperature = packet.control.set_point + 16.0f;
should_publish = should_publish || (old_target_temperature != this->target_temperature);
}
{
// Current temperature
float old_current_temperature = this->current_temperature;
this->current_temperature = packet.control.room_temperature;
should_publish = should_publish || (old_current_temperature != this->current_temperature);
}
{
// Fan mode
optional<ClimateFanMode> old_fan_mode = this->fan_mode;
// remember the fan speed we last had for climate vs fan
if (packet.control.ac_mode == (uint8_t) smartair2_protocol::ConditioningMode::FAN) {
if (packet.control.fan_mode != (uint8_t) smartair2_protocol::FanMode::FAN_AUTO)
this->fan_mode_speed_ = packet.control.fan_mode;
} else {
this->other_modes_fan_speed_ = packet.control.fan_mode;
}
switch (packet.control.fan_mode) {
case (uint8_t) smartair2_protocol::FanMode::FAN_AUTO:
// Somtimes AC reports in fan only mode that fan speed is auto
// but never accept this value back
if (packet.control.ac_mode != (uint8_t) smartair2_protocol::ConditioningMode::FAN) {
this->fan_mode = CLIMATE_FAN_AUTO;
} else {
should_publish = true;
}
break;
case (uint8_t) smartair2_protocol::FanMode::FAN_MID:
this->fan_mode = CLIMATE_FAN_MEDIUM;
break;
case (uint8_t) smartair2_protocol::FanMode::FAN_LOW:
this->fan_mode = CLIMATE_FAN_LOW;
break;
case (uint8_t) smartair2_protocol::FanMode::FAN_HIGH:
this->fan_mode = CLIMATE_FAN_HIGH;
break;
}
should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
}
{
// Display status
// should be before "Climate mode" because it is changing this->mode
if (packet.control.ac_power != 0) {
// if AC is off display status always ON so process it only when AC is on
bool disp_status = packet.control.display_status == 0;
if (disp_status != this->display_status_) {
// 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);
} else {
this->display_status_ = disp_status;
}
}
}
}
{
// Climate mode
ClimateMode old_mode = this->mode;
if (packet.control.ac_power == 0) {
this->mode = CLIMATE_MODE_OFF;
} else {
// Check current hvac mode
switch (packet.control.ac_mode) {
case (uint8_t) smartair2_protocol::ConditioningMode::COOL:
this->mode = CLIMATE_MODE_COOL;
break;
case (uint8_t) smartair2_protocol::ConditioningMode::HEAT:
this->mode = CLIMATE_MODE_HEAT;
break;
case (uint8_t) smartair2_protocol::ConditioningMode::DRY:
this->mode = CLIMATE_MODE_DRY;
break;
case (uint8_t) smartair2_protocol::ConditioningMode::FAN:
this->mode = CLIMATE_MODE_FAN_ONLY;
break;
case (uint8_t) smartair2_protocol::ConditioningMode::AUTO:
this->mode = CLIMATE_MODE_AUTO;
break;
}
}
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 (packet.control.vertical_swing != 0) {
this->swing_mode = CLIMATE_SWING_VERTICAL;
} else if (packet.control.horizontal_swing != 0) {
this->swing_mode = CLIMATE_SWING_HORIZONTAL;
} else {
this->swing_mode = CLIMATE_SWING_OFF;
}
} 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
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);
return haier_protocol::HandlerError::HANDLER_OK;
}
bool Smartair2Climate::is_message_invalid(uint8_t message_type) {
return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID;
}
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,31 @@
#pragma once
#include <chrono>
#include "haier_base.h"
namespace esphome {
namespace haier {
class Smartair2Climate : public HaierClimateBase {
public:
Smartair2Climate();
Smartair2Climate(const Smartair2Climate &) = delete;
Smartair2Climate &operator=(const Smartair2Climate &) = delete;
~Smartair2Climate();
void dump_config() override;
protected:
void set_answers_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;
// Answers handlers
haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t 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_;
};
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,97 @@
#pragma once
namespace esphome {
namespace haier {
namespace smartair2_protocol {
enum class ConditioningMode : uint8_t { AUTO = 0x00, COOL = 0x01, HEAT = 0x02, FAN = 0x03, DRY = 0x04 };
enum class FanMode : uint8_t { FAN_HIGH = 0x00, FAN_MID = 0x01, FAN_LOW = 0x02, FAN_AUTO = 0x03 };
struct HaierPacketControl {
// Control bytes starts here
// 10
uint8_t : 8; // Temperature high byte
// 11
uint8_t room_temperature; // current room temperature 1°C step
// 12
uint8_t : 8; // Humidity high byte
// 13
uint8_t room_humidity; // Humidity 0%-100% with 1% step
// 14
uint8_t : 8;
// 15
uint8_t cntrl; // In AC => ESP packets - 0x7F, in ESP => AC packets - 0x00
// 16
uint8_t : 8;
// 17
uint8_t : 8;
// 18
uint8_t : 8;
// 19
uint8_t : 8;
// 20
uint8_t : 8;
// 21
uint8_t ac_mode; // See enum ConditioningMode
// 22
uint8_t : 8;
// 23
uint8_t fan_mode; // See enum FanMode
// 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
// 26
uint8_t : 3;
uint8_t use_fahrenheit : 1;
uint8_t : 3;
uint8_t lock_remote : 1; // Disable remote
// 27
uint8_t ac_power : 1; // Is ac on or off
uint8_t : 2;
uint8_t health_mode : 1; // Health mode on or off
uint8_t compressor : 1; // Compressor on or off ???
uint8_t : 1;
uint8_t ten_degree : 1; // 10 degree status (only work in heat mode)
uint8_t : 0;
// 28
uint8_t : 8;
// 29
uint8_t use_swing_bits : 1; // Indicate if horizontal_swing and vertical_swing should be used
uint8_t turbo_mode : 1; // Turbo mode
uint8_t quiet_mode : 1; // Sleep mode
uint8_t horizontal_swing : 1; // Horizontal swing (if swing_both == 0)
uint8_t vertical_swing : 1; // Vertical swing (if swing_both == 0) if vertical_swing and horizontal_swing both 0 =>
// swing off
uint8_t display_status : 1; // Led on or off
uint8_t : 0;
// 30
uint8_t : 8;
// 31
uint8_t : 8;
// 32
uint8_t : 8; // Target temperature high byte
// 33
uint8_t set_point; // Target temperature with 16°C offset, 1°C step
};
struct HaierStatus {
uint16_t subcommand;
HaierPacketControl control;
};
enum class FrameType : uint8_t {
CONTROL = 0x01,
STATUS = 0x02,
INVALID = 0x03,
CONFIRM = 0x05,
GET_DEVICE_VERSION = 0x61,
REPORT_NETWORK_STATUS = 0xF7,
NO_COMMAND = 0xFF,
};
} // namespace smartair2_protocol
} // namespace haier
} // namespace esphome

View file

@ -14,6 +14,14 @@ ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len
return bus_->read(address_, data, len); return bus_->read(address_, data, len);
} }
ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) {
a_register = convert_big_endian(a_register);
ErrorCode const err = this->write(reinterpret_cast<const uint8_t *>(&a_register), 2, stop);
if (err != ERROR_OK)
return err;
return bus_->read(address_, data, len);
}
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) { ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) {
WriteBuffer buffers[2]; WriteBuffer buffers[2];
buffers[0].data = &a_register; buffers[0].data = &a_register;
@ -23,6 +31,16 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz
return bus_->writev(address_, buffers, 2, stop); return bus_->writev(address_, buffers, 2, stop);
} }
ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) {
a_register = convert_big_endian(a_register);
WriteBuffer buffers[2];
buffers[0].data = reinterpret_cast<const uint8_t *>(&a_register);
buffers[0].len = 2;
buffers[1].data = data;
buffers[1].len = len;
return bus_->writev(address_, buffers, 2, stop);
}
bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) {
if (read_register(a_register, reinterpret_cast<uint8_t *>(data), len * 2) != ERROR_OK) if (read_register(a_register, reinterpret_cast<uint8_t *>(data), len * 2) != ERROR_OK)
return false; return false;
@ -60,5 +78,26 @@ uint8_t I2CRegister::get() const {
return value; return value;
} }
I2CRegister16 &I2CRegister16::operator=(uint8_t value) {
this->parent_->write_register16(this->register_, &value, 1);
return *this;
}
I2CRegister16 &I2CRegister16::operator&=(uint8_t value) {
value &= get();
this->parent_->write_register16(this->register_, &value, 1);
return *this;
}
I2CRegister16 &I2CRegister16::operator|=(uint8_t value) {
value |= get();
this->parent_->write_register16(this->register_, &value, 1);
return *this;
}
uint8_t I2CRegister16::get() const {
uint8_t value = 0x00;
this->parent_->read_register16(this->register_, &value, 1);
return value;
}
} // namespace i2c } // namespace i2c
} // namespace esphome } // namespace esphome

View file

@ -31,6 +31,25 @@ class I2CRegister {
uint8_t register_; uint8_t register_;
}; };
class I2CRegister16 {
public:
I2CRegister16 &operator=(uint8_t value);
I2CRegister16 &operator&=(uint8_t value);
I2CRegister16 &operator|=(uint8_t value);
explicit operator uint8_t() const { return get(); }
uint8_t get() const;
protected:
friend class I2CDevice;
I2CRegister16(I2CDevice *parent, uint16_t a_register) : parent_(parent), register_(a_register) {}
I2CDevice *parent_;
uint16_t register_;
};
// like ntohs/htons but without including networking headers. // like ntohs/htons but without including networking headers.
// ("i2c" byte order is big-endian) // ("i2c" byte order is big-endian)
inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); } inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); }
@ -44,12 +63,15 @@ class I2CDevice {
void set_i2c_bus(I2CBus *bus) { bus_ = bus; } void set_i2c_bus(I2CBus *bus) { bus_ = bus; }
I2CRegister reg(uint8_t a_register) { return {this, a_register}; } I2CRegister reg(uint8_t a_register) { return {this, a_register}; }
I2CRegister16 reg16(uint16_t a_register) { return {this, a_register}; }
ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); }
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true); ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true);
ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop = true);
ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); }
ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true); ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true);
ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop = true);
// Compat APIs // Compat APIs

View file

@ -44,6 +44,7 @@ MODELS = {
"ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI), "ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI),
"ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI), "ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI),
"ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI), "ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI),
"S3BOX": ili9XXX_ns.class_("ILI9XXXS3Box", ili9XXXSPI),
"S3BOX_LITE": ili9XXX_ns.class_("ILI9XXXS3BoxLite", ili9XXXSPI), "S3BOX_LITE": ili9XXX_ns.class_("ILI9XXXS3BoxLite", ili9XXXSPI),
} }

View file

@ -421,6 +421,17 @@ void ILI9XXXST7796::initialize() {
} }
} }
// 24_TFT rotated display
void ILI9XXXS3Box::initialize() {
this->init_lcd_(INITCMD_S3BOX);
if (this->width_ == 0) {
this->width_ = 320;
}
if (this->height_ == 0) {
this->height_ = 240;
}
}
// 24_TFT rotated display // 24_TFT rotated display
void ILI9XXXS3BoxLite::initialize() { void ILI9XXXS3BoxLite::initialize() {
this->init_lcd_(INITCMD_S3BOXLITE); this->init_lcd_(INITCMD_S3BOXLITE);

View file

@ -134,6 +134,11 @@ class ILI9XXXST7796 : public ILI9XXXDisplay {
void initialize() override; void initialize() override;
}; };
class ILI9XXXS3Box : public ILI9XXXDisplay {
protected:
void initialize() override;
};
class ILI9XXXS3BoxLite : public ILI9XXXDisplay { class ILI9XXXS3BoxLite : public ILI9XXXDisplay {
protected: protected:
void initialize() override; void initialize() override;

View file

@ -169,6 +169,36 @@ static const uint8_t PROGMEM INITCMD_ST7796[] = {
0x00 // End of list 0x00 // End of list
}; };
static const uint8_t PROGMEM INITCMD_S3BOX[] = {
0xEF, 3, 0x03, 0x80, 0x02,
0xCF, 3, 0x00, 0xC1, 0x30,
0xED, 4, 0x64, 0x03, 0x12, 0x81,
0xE8, 3, 0x85, 0x00, 0x78,
0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02,
0xF7, 1, 0x20,
0xEA, 2, 0x00, 0x00,
ILI9XXX_PWCTR1 , 1, 0x23, // Power control VRH[5:0]
ILI9XXX_PWCTR2 , 1, 0x10, // Power control SAP[2:0];BT[3:0]
ILI9XXX_VMCTR1 , 2, 0x3e, 0x28, // VCM control
ILI9XXX_VMCTR2 , 1, 0x86, // VCM control2
ILI9XXX_MADCTL , 1, 0xC8, // Memory Access Control
ILI9XXX_VSCRSADD, 1, 0x00, // Vertical scroll zero
ILI9XXX_PIXFMT , 1, 0x55,
ILI9XXX_FRMCTR1 , 2, 0x00, 0x18,
ILI9XXX_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control
0xF2, 1, 0x00, // 3Gamma Function Disable
ILI9XXX_GAMMASET , 1, 0x01, // Gamma curve selected
ILI9XXX_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma
0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03,
0x0E, 0x09, 0x00,
ILI9XXX_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma
0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C,
0x31, 0x36, 0x0F,
ILI9XXX_SLPOUT , 0x80, // Exit Sleep
ILI9XXX_DISPON , 0x80, // Display on
0x00 // End of list
};
static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = {
0xEF, 3, 0x03, 0x80, 0x02, 0xEF, 3, 0x03, 0x80, 0x02,
0xCF, 3, 0x00, 0xC1, 0x30, 0xCF, 3, 0x00, 0xC1, 0x30,

View file

@ -11,6 +11,7 @@ namespace mqtt {
static const char *const TAG = "mqtt.idf"; static const char *const TAG = "mqtt.idf";
bool MQTTBackendIDF::initialize_() { bool MQTTBackendIDF::initialize_() {
#if ESP_IDF_VERSION_MAJOR < 5
mqtt_cfg_.user_context = (void *) this; mqtt_cfg_.user_context = (void *) this;
mqtt_cfg_.buffer_size = MQTT_BUFFER_SIZE; mqtt_cfg_.buffer_size = MQTT_BUFFER_SIZE;
@ -47,6 +48,41 @@ bool MQTTBackendIDF::initialize_() {
} else { } else {
mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_TCP; mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_TCP;
} }
#else
mqtt_cfg_.broker.address.hostname = this->host_.c_str();
mqtt_cfg_.broker.address.port = this->port_;
mqtt_cfg_.session.keepalive = this->keep_alive_;
mqtt_cfg_.session.disable_clean_session = !this->clean_session_;
if (!this->username_.empty()) {
mqtt_cfg_.credentials.username = this->username_.c_str();
if (!this->password_.empty()) {
mqtt_cfg_.credentials.authentication.password = this->password_.c_str();
}
}
if (!this->lwt_topic_.empty()) {
mqtt_cfg_.session.last_will.topic = this->lwt_topic_.c_str();
this->mqtt_cfg_.session.last_will.qos = this->lwt_qos_;
this->mqtt_cfg_.session.last_will.retain = this->lwt_retain_;
if (!this->lwt_message_.empty()) {
mqtt_cfg_.session.last_will.msg = this->lwt_message_.c_str();
mqtt_cfg_.session.last_will.msg_len = this->lwt_message_.size();
}
}
if (!this->client_id_.empty()) {
mqtt_cfg_.credentials.client_id = this->client_id_.c_str();
}
if (ca_certificate_.has_value()) {
mqtt_cfg_.broker.verification.certificate = ca_certificate_.value().c_str();
mqtt_cfg_.broker.verification.skip_cert_common_name_check = skip_cert_cn_check_;
mqtt_cfg_.broker.address.transport = MQTT_TRANSPORT_OVER_SSL;
} else {
mqtt_cfg_.broker.address.transport = MQTT_TRANSPORT_OVER_TCP;
}
#endif
auto *mqtt_client = esp_mqtt_client_init(&mqtt_cfg_); auto *mqtt_client = esp_mqtt_client_init(&mqtt_cfg_);
if (mqtt_client) { if (mqtt_client) {
handler_.reset(mqtt_client); handler_.reset(mqtt_client);
@ -78,9 +114,8 @@ void MQTTBackendIDF::mqtt_event_handler_(const Event &event) {
case MQTT_EVENT_CONNECTED: case MQTT_EVENT_CONNECTED:
ESP_LOGV(TAG, "MQTT_EVENT_CONNECTED"); ESP_LOGV(TAG, "MQTT_EVENT_CONNECTED");
// TODO session present check
this->is_connected_ = true; this->is_connected_ = true;
this->on_connect_.call(!mqtt_cfg_.disable_clean_session); this->on_connect_.call(event.session_present);
break; break;
case MQTT_EVENT_DISCONNECTED: case MQTT_EVENT_DISCONNECTED:
ESP_LOGV(TAG, "MQTT_EVENT_DISCONNECTED"); ESP_LOGV(TAG, "MQTT_EVENT_DISCONNECTED");

View file

@ -22,6 +22,7 @@ struct Event {
bool retain; bool retain;
int qos; int qos;
bool dup; bool dup;
bool session_present;
esp_mqtt_error_codes_t error_handle; esp_mqtt_error_codes_t error_handle;
// Construct from esp_mqtt_event_t // Construct from esp_mqtt_event_t
@ -36,6 +37,7 @@ struct Event {
retain(event.retain), retain(event.retain),
qos(event.qos), qos(event.qos),
dup(event.dup), dup(event.dup),
session_present(event.session_present),
error_handle(*event.error_handle) {} error_handle(*event.error_handle) {}
}; };

View file

@ -118,7 +118,7 @@ bool MQTTComponent::send_discovery_() {
} else { } else {
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
char friendly_name_hash[9]; char friendly_name_hash[9];
sprintf(friendly_name_hash, "%08x", fnv1_hash(this->friendly_name())); sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name()));
friendly_name_hash[8] = 0; // ensure the hash-string ends with null friendly_name_hash[8] = 0; // ensure the hash-string ends with null
root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash;
} else { } else {

View file

@ -1,3 +1,4 @@
#include <cinttypes>
#include "mqtt_sensor.h" #include "mqtt_sensor.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@ -26,7 +27,7 @@ void MQTTSensorComponent::setup() {
void MQTTSensorComponent::dump_config() { void MQTTSensorComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Sensor '%s':", this->sensor_->get_name().c_str()); ESP_LOGCONFIG(TAG, "MQTT Sensor '%s':", this->sensor_->get_name().c_str());
if (this->get_expire_after() > 0) { if (this->get_expire_after() > 0) {
ESP_LOGCONFIG(TAG, " Expire After: %us", this->get_expire_after() / 1000); ESP_LOGCONFIG(TAG, " Expire After: %" PRIu32 "s", this->get_expire_after() / 1000);
} }
LOG_MQTT_COMPONENT(true, false) LOG_MQTT_COMPONENT(true, false)
} }

View file

@ -29,13 +29,10 @@ void PCA9685Output::setup() {
ESP_LOGCONFIG(TAG, "Setting up PCA9685OutputComponent..."); ESP_LOGCONFIG(TAG, "Setting up PCA9685OutputComponent...");
ESP_LOGV(TAG, " Resetting devices..."); ESP_LOGV(TAG, " Resetting devices...");
uint8_t address_tmp = this->address_;
this->set_i2c_address(0x00);
if (!this->write_bytes(PCA9685_REGISTER_SOFTWARE_RESET, nullptr, 0)) { if (!this->write_bytes(PCA9685_REGISTER_SOFTWARE_RESET, nullptr, 0)) {
this->mark_failed(); this->mark_failed();
return; return;
} }
this->set_i2c_address(address_tmp);
if (!this->write_byte(PCA9685_REGISTER_MODE1, PCA9685_MODE1_RESTART | PCA9685_MODE1_AUTOINC)) { if (!this->write_byte(PCA9685_REGISTER_MODE1, PCA9685_MODE1_RESTART | PCA9685_MODE1_AUTOINC)) {
this->mark_failed(); this->mark_failed();

View file

@ -16,8 +16,7 @@ from esphome.const import (
KEY_TARGET_PLATFORM, KEY_TARGET_PLATFORM,
) )
from esphome.core import CORE, coroutine_with_priority, EsphomeError from esphome.core import CORE, coroutine_with_priority, EsphomeError
from esphome.helpers import mkdir_p, write_file from esphome.helpers import mkdir_p, write_file, copy_file_if_changed
import esphome.platformio_api as api
from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns
@ -193,25 +192,20 @@ def generate_pio_files() -> bool:
pio_path = CORE.relative_build_path(f"src/pio/{key}.pio") pio_path = CORE.relative_build_path(f"src/pio/{key}.pio")
mkdir_p(os.path.dirname(pio_path)) mkdir_p(os.path.dirname(pio_path))
write_file(pio_path, data) write_file(pio_path, data)
_LOGGER.info("Assembling PIO assembly code")
retval = api.run_platformio_cli(
"pkg",
"exec",
"--package",
"earlephilhower/tool-pioasm-rp2040-earlephilhower",
"--",
"pioasm",
pio_path,
pio_path + ".h",
)
includes.append(f"pio/{key}.pio.h") includes.append(f"pio/{key}.pio.h")
if retval != 0:
raise EsphomeError("PIO assembly failed")
write_file( write_file(
CORE.relative_build_path("src/pio_includes.h"), CORE.relative_build_path("src/pio_includes.h"),
"#pragma once\n" + "\n".join([f'#include "{include}"' for include in includes]), "#pragma once\n" + "\n".join([f'#include "{include}"' for include in includes]),
) )
dir = os.path.dirname(__file__)
build_pio_file = os.path.join(dir, "build_pio.py.script")
copy_file_if_changed(
build_pio_file,
CORE.relative_build_path("build_pio.py"),
)
return True return True

View file

@ -0,0 +1,47 @@
"""
Custom pioasm compiler script for platformio.
(c) 2022 by P.Z.
Sourced 2023/06/23 from https://gist.github.com/hexeguitar/f4533bc697c956ac1245b6843e2ef438
Modified by jesserockz 2023/06/23
"""
from os.path import join
import glob
import sys
import subprocess
# pylint: disable=E0602
Import("env") # noqa
from SCons.Script import ARGUMENTS
platform = env.PioPlatform()
PROJ_SRC = env["PROJECT_SRC_DIR"]
PIO_FILES = glob.glob(join(PROJ_SRC, "**", "*.pio"), recursive=True)
verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0")))
if PIO_FILES:
if verbose:
print("==============================================")
print("PIO ASSEMBLY COMPILER")
try:
PIOASM_DIR = platform.get_package_dir("tool-pioasm-rp2040-earlephilhower")
except:
print("tool-pioasm-rp2040-earlephilhower not supported on your system!")
sys.exit()
PIOASM_EXE = join(PIOASM_DIR, "pioasm")
if verbose:
print("PIO files found:")
for filename in PIO_FILES:
if verbose:
print(f" {filename}")
subprocess.run([PIOASM_EXE, "-o", "c-sdk", filename, f"{filename}.h"])
if verbose:
print("==============================================")

View file

@ -0,0 +1,40 @@
import platform
import esphome.codegen as cg
DEPENDENCIES = ["rp2040"]
PIOASM_REPO_VERSION = "1.5.0-b"
PIOASM_REPO_BASE = f"https://github.com/earlephilhower/pico-quick-toolchain/releases/download/{PIOASM_REPO_VERSION}"
PIOASM_VERSION = "pioasm-2e6142b.230216"
PIOASM_DOWNLOADS = {
"linux": {
"aarch64": f"aarch64-linux-gnu.{PIOASM_VERSION}.tar.gz",
"armv7l": f"arm-linux-gnueabihf.{PIOASM_VERSION}.tar.gz",
"x86_64": f"x86_64-linux-gnu.{PIOASM_VERSION}.tar.gz",
},
"windows": {
"amd64": f"x86_64-w64-mingw32.{PIOASM_VERSION}.zip",
},
"darwin": {
"x86_64": f"x86_64-apple-darwin14.{PIOASM_VERSION}.tar.gz",
"arm64": f"x86_64-apple-darwin14.{PIOASM_VERSION}.tar.gz",
},
}
async def to_code(config):
# cg.add_platformio_option(
# "platform_packages",
# [
# "earlephilhower/tool-pioasm-rp2040-earlephilhower",
# ],
# )
file = PIOASM_DOWNLOADS[platform.system().lower()][platform.machine().lower()]
cg.add_platformio_option(
"platform_packages",
[f"earlephilhower/tool-pioasm-rp2040-earlephilhower@{PIOASM_REPO_BASE}/{file}"],
)
cg.add_platformio_option("extra_scripts", ["pre:build_pio.py"])

View file

@ -127,6 +127,7 @@ def time_to_cycles(time_us):
CONF_PIO = "pio" CONF_PIO = "pio"
AUTO_LOAD = ["rp2040_pio"]
CODEOWNERS = ["@Papa-DMan"] CODEOWNERS = ["@Papa-DMan"]
DEPENDENCIES = ["rp2040"] DEPENDENCIES = ["rp2040"]
@ -265,9 +266,3 @@ async def to_code(config):
time_to_cycles(config[CONF_BIT1_LOW]), time_to_cycles(config[CONF_BIT1_LOW]),
), ),
) )
cg.add_platformio_option(
"platform_packages",
[
"earlephilhower/tool-pioasm-rp2040-earlephilhower",
],
)

View file

@ -11,7 +11,7 @@ from esphome.const import (
CONF_TVOC, CONF_TVOC,
ICON_RADIATOR, ICON_RADIATOR,
DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_MILLION,
UNIT_PARTS_PER_BILLION, UNIT_PARTS_PER_BILLION,
@ -49,7 +49,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_PARTS_PER_BILLION, unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR, icon=ICON_RADIATOR,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema( cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema(

View file

@ -87,6 +87,30 @@ void SPIComponent::setup() {
return; return;
} }
#endif // USE_ESP32 #endif // USE_ESP32
#ifdef USE_RP2040
static uint8_t spi_bus_num = 0;
if (spi_bus_num >= 2) {
use_hw_spi = false;
}
if (use_hw_spi) {
SPIClassRP2040 *spi;
if (spi_bus_num == 0) {
spi = &SPI;
} else {
spi = &SPI1;
}
spi_bus_num++;
if (miso_pin != -1)
spi->setRX(miso_pin);
if (mosi_pin != -1)
spi->setTX(mosi_pin);
spi->setSCK(clk_pin);
this->hw_spi_ = spi;
this->hw_spi_->begin();
return;
}
#endif // USE_RP2040
#endif // USE_SPI_ARDUINO_BACKEND #endif // USE_SPI_ARDUINO_BACKEND
if (this->miso_ != nullptr) { if (this->miso_ != nullptr) {

View file

@ -14,6 +14,7 @@ from esphome.const import (
CONF_PASSWORD, CONF_PASSWORD,
CONF_INCLUDE_INTERNAL, CONF_INCLUDE_INTERNAL,
CONF_OTA, CONF_OTA,
CONF_LOG,
CONF_VERSION, CONF_VERSION,
CONF_LOCAL, CONF_LOCAL,
) )
@ -71,6 +72,7 @@ CONFIG_SCHEMA = cv.All(
), ),
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
cv.Optional(CONF_OTA, default=True): cv.boolean, cv.Optional(CONF_OTA, default=True): cv.boolean,
cv.Optional(CONF_LOG, default=True): cv.boolean,
cv.Optional(CONF_LOCAL): cv.boolean, cv.Optional(CONF_LOCAL): cv.boolean,
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
@ -81,6 +83,37 @@ CONFIG_SCHEMA = cv.All(
) )
def build_index_html(config) -> str:
html = "<!DOCTYPE html><html><head><meta charset=UTF-8><link rel=icon href=data:>"
css_include = config.get(CONF_CSS_INCLUDE)
js_include = config.get(CONF_JS_INCLUDE)
if css_include:
html += "<link rel=stylesheet href=/0.css>"
if config[CONF_CSS_URL]:
html += f'<link rel=stylesheet href="{config[CONF_CSS_URL]}">'
html += "</head><body>"
if js_include:
html += "<script type=module src=/0.js></script>"
html += "<esp-app></esp-app>"
if config[CONF_JS_URL]:
html += f'<script src="{config[CONF_JS_URL]}"></script>'
html += "</body></html>"
return html
def add_resource_as_progmem(resource_name: str, content: str) -> None:
"""Add a resource to progmem."""
content_encoded = content.encode("utf-8")
content_encoded_size = len(content_encoded)
bytes_as_int = ", ".join(str(x) for x in content_encoded)
uint8_t = f"const uint8_t ESPHOME_WEBSERVER_{resource_name}[{content_encoded_size}] PROGMEM = {{{bytes_as_int}}}"
size_t = (
f"const size_t ESPHOME_WEBSERVER_{resource_name}_SIZE = {content_encoded_size}"
)
cg.add_global(cg.RawExpression(uint8_t))
cg.add_global(cg.RawExpression(size_t))
@coroutine_with_priority(40.0) @coroutine_with_priority(40.0)
async def to_code(config): async def to_code(config):
paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
@ -89,27 +122,32 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
cg.add_define("USE_WEBSERVER") cg.add_define("USE_WEBSERVER")
version = config[CONF_VERSION]
cg.add(paren.set_port(config[CONF_PORT])) cg.add(paren.set_port(config[CONF_PORT]))
cg.add_define("USE_WEBSERVER") cg.add_define("USE_WEBSERVER")
cg.add_define("USE_WEBSERVER_PORT", config[CONF_PORT]) cg.add_define("USE_WEBSERVER_PORT", config[CONF_PORT])
cg.add_define("USE_WEBSERVER_VERSION", config[CONF_VERSION]) cg.add_define("USE_WEBSERVER_VERSION", version)
cg.add(var.set_css_url(config[CONF_CSS_URL])) if version == 2:
cg.add(var.set_js_url(config[CONF_JS_URL])) add_resource_as_progmem("INDEX_HTML", build_index_html(config))
else:
cg.add(var.set_css_url(config[CONF_CSS_URL]))
cg.add(var.set_js_url(config[CONF_JS_URL]))
cg.add(var.set_allow_ota(config[CONF_OTA])) cg.add(var.set_allow_ota(config[CONF_OTA]))
cg.add(var.set_expose_log(config[CONF_LOG]))
if CONF_AUTH in config: if CONF_AUTH in config:
cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME])) cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME]))
cg.add(paren.set_auth_password(config[CONF_AUTH][CONF_PASSWORD])) cg.add(paren.set_auth_password(config[CONF_AUTH][CONF_PASSWORD]))
if CONF_CSS_INCLUDE in config: if CONF_CSS_INCLUDE in config:
cg.add_define("USE_WEBSERVER_CSS_INCLUDE") cg.add_define("USE_WEBSERVER_CSS_INCLUDE")
path = CORE.relative_config_path(config[CONF_CSS_INCLUDE]) path = CORE.relative_config_path(config[CONF_CSS_INCLUDE])
with open(file=path, encoding="utf-8") as myfile: with open(file=path, encoding="utf-8") as css_file:
cg.add(var.set_css_include(myfile.read())) add_resource_as_progmem("CSS_INCLUDE", css_file.read())
if CONF_JS_INCLUDE in config: if CONF_JS_INCLUDE in config:
cg.add_define("USE_WEBSERVER_JS_INCLUDE") cg.add_define("USE_WEBSERVER_JS_INCLUDE")
path = CORE.relative_config_path(config[CONF_JS_INCLUDE]) path = CORE.relative_config_path(config[CONF_JS_INCLUDE])
with open(file=path, encoding="utf-8") as myfile: with open(file=path, encoding="utf-8") as js_file:
cg.add(var.set_js_include(myfile.read())) add_resource_as_progmem("JS_INCLUDE", js_file.read())
cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL])) cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL]))
if CONF_LOCAL in config and config[CONF_LOCAL]: if CONF_LOCAL in config and config[CONF_LOCAL]:
cg.add_define("USE_WEBSERVER_LOCAL") cg.add_define("USE_WEBSERVER_LOCAL")

File diff suppressed because it is too large Load diff

View file

@ -90,10 +90,27 @@ WebServer::WebServer(web_server_base::WebServerBase *base)
#endif #endif
} }
#if USE_WEBSERVER_VERSION == 1
void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; } void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; }
void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; }
void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; }
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE
void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; }
#endif
#ifdef USE_WEBSERVER_JS_INCLUDE
void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; } void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; }
#endif
std::string WebServer::get_config_json() {
return json::build_json([this](JsonObject root) {
root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
root["comment"] = App.get_comment();
root["ota"] = this->allow_ota_;
root["log"] = this->expose_log_;
root["lang"] = "en";
});
}
void WebServer::setup() { void WebServer::setup() {
ESP_LOGCONFIG(TAG, "Setting up web server..."); ESP_LOGCONFIG(TAG, "Setting up web server...");
@ -102,20 +119,13 @@ void WebServer::setup() {
this->events_.onConnect([this](AsyncEventSourceClient *client) { this->events_.onConnect([this](AsyncEventSourceClient *client) {
// Configure reconnect timeout and send config // Configure reconnect timeout and send config
client->send(this->get_config_json().c_str(), "ping", millis(), 30000);
client->send(json::build_json([this](JsonObject root) {
root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
root["comment"] = App.get_comment();
root["ota"] = this->allow_ota_;
root["lang"] = "en";
}).c_str(),
"ping", millis(), 30000);
this->entities_iterator_.begin(this->include_internal_); this->entities_iterator_.begin(this->include_internal_);
}); });
#ifdef USE_LOGGER #ifdef USE_LOGGER
if (logger::global_logger != nullptr) { if (logger::global_logger != nullptr && this->expose_log_) {
logger::global_logger->add_on_log_callback( logger::global_logger->add_on_log_callback(
[this](int level, const char *tag, const char *message) { this->events_.send(message, "log", millis()); }); [this](int level, const char *tag, const char *message) { this->events_.send(message, "log", millis()); });
} }
@ -159,20 +169,14 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
response->addHeader("Content-Encoding", "gzip"); response->addHeader("Content-Encoding", "gzip");
request->send(response); request->send(response);
} }
#else #elif USE_WEBSERVER_VERSION == 1
void WebServer::handle_index_request(AsyncWebServerRequest *request) { void WebServer::handle_index_request(AsyncWebServerRequest *request) {
AsyncResponseStream *stream = request->beginResponseStream("text/html"); AsyncResponseStream *stream = request->beginResponseStream("text/html");
// All content is controlled and created by user - so allowing all origins is fine here.
stream->addHeader("Access-Control-Allow-Origin", "*");
#if USE_WEBSERVER_VERSION == 1
const std::string &title = App.get_name(); const std::string &title = App.get_name();
stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta " stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta "
"name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>")); "name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>"));
stream->print(title.c_str()); stream->print(title.c_str());
stream->print(F("</title>")); stream->print(F("</title>"));
#else
stream->print(F("<!DOCTYPE html><html><head><meta charset=UTF-8><link rel=icon href=data:>"));
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE #ifdef USE_WEBSERVER_CSS_INCLUDE
stream->print(F("<link rel=\"stylesheet\" href=\"/0.css\">")); stream->print(F("<link rel=\"stylesheet\" href=\"/0.css\">"));
#endif #endif
@ -182,7 +186,6 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
stream->print(F("\">")); stream->print(F("\">"));
} }
stream->print(F("</head><body>")); stream->print(F("</head><body>"));
#if USE_WEBSERVER_VERSION == 1
stream->print(F("<article class=\"markdown-body\"><h1>")); stream->print(F("<article class=\"markdown-body\"><h1>"));
stream->print(title.c_str()); stream->print(title.c_str());
stream->print(F("</h1>")); stream->print(F("</h1>"));
@ -308,49 +311,40 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>")); "type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
} }
stream->print(F("<h2>Debug Log</h2><pre id=\"log\"></pre>")); stream->print(F("<h2>Debug Log</h2><pre id=\"log\"></pre>"));
#endif
#ifdef USE_WEBSERVER_JS_INCLUDE #ifdef USE_WEBSERVER_JS_INCLUDE
if (this->js_include_ != nullptr) { if (this->js_include_ != nullptr) {
stream->print(F("<script type=\"module\" src=\"/0.js\"></script>")); stream->print(F("<script type=\"module\" src=\"/0.js\"></script>"));
} }
#endif
#if USE_WEBSERVER_VERSION == 2
stream->print(F("<esp-app></esp-app>"));
#endif #endif
if (strlen(this->js_url_) > 0) { if (strlen(this->js_url_) > 0) {
stream->print(F("<script src=\"")); stream->print(F("<script src=\""));
stream->print(this->js_url_); stream->print(this->js_url_);
stream->print(F("\"></script>")); stream->print(F("\"></script>"));
} }
#if USE_WEBSERVER_VERSION == 1
stream->print(F("</article></body></html>")); stream->print(F("</article></body></html>"));
#else
stream->print(F("</body></html>"));
#endif
request->send(stream); request->send(stream);
} }
#elif USE_WEBSERVER_VERSION == 2
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response =
request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE);
request->send(response);
}
#endif #endif
#ifdef USE_WEBSERVER_CSS_INCLUDE #ifdef USE_WEBSERVER_CSS_INCLUDE
void WebServer::handle_css_request(AsyncWebServerRequest *request) { void WebServer::handle_css_request(AsyncWebServerRequest *request) {
AsyncResponseStream *stream = request->beginResponseStream("text/css"); AsyncWebServerResponse *response =
if (this->css_include_ != nullptr) { request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE);
stream->print(this->css_include_); request->send(response);
}
request->send(stream);
} }
#endif #endif
#ifdef USE_WEBSERVER_JS_INCLUDE #ifdef USE_WEBSERVER_JS_INCLUDE
void WebServer::handle_js_request(AsyncWebServerRequest *request) { void WebServer::handle_js_request(AsyncWebServerRequest *request) {
AsyncResponseStream *stream = request->beginResponseStream("text/javascript"); AsyncWebServerResponse *response =
if (this->js_include_ != nullptr) { request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE);
stream->addHeader("Access-Control-Allow-Origin", "*"); request->send(response);
stream->print(this->js_include_);
}
request->send(stream);
} }
#endif #endif

View file

@ -14,6 +14,22 @@
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
#endif #endif
#if USE_WEBSERVER_VERSION == 2
extern const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM;
extern const size_t ESPHOME_WEBSERVER_INDEX_HTML_SIZE;
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE
extern const uint8_t ESPHOME_WEBSERVER_CSS_INCLUDE[] PROGMEM;
extern const size_t ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE;
#endif
#ifdef USE_WEBSERVER_JS_INCLUDE
extern const uint8_t ESPHOME_WEBSERVER_JS_INCLUDE[] PROGMEM;
extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE;
#endif
namespace esphome { namespace esphome {
namespace web_server { namespace web_server {
@ -40,6 +56,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
public: public:
WebServer(web_server_base::WebServerBase *base); WebServer(web_server_base::WebServerBase *base);
#if USE_WEBSERVER_VERSION == 1
/** Set the URL to the CSS <link> that's sent to each client. Defaults to /** Set the URL to the CSS <link> that's sent to each client. Defaults to
* https://esphome.io/_static/webserver-v1.min.css * https://esphome.io/_static/webserver-v1.min.css
* *
@ -47,24 +64,29 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
*/ */
void set_css_url(const char *css_url); void set_css_url(const char *css_url);
/** Set local path to the script that's embedded in the index page. Defaults to
*
* @param css_include Local path to web server script.
*/
void set_css_include(const char *css_include);
/** Set the URL to the script that's embedded in the index page. Defaults to /** Set the URL to the script that's embedded in the index page. Defaults to
* https://esphome.io/_static/webserver-v1.min.js * https://esphome.io/_static/webserver-v1.min.js
* *
* @param js_url The url to the web server script. * @param js_url The url to the web server script.
*/ */
void set_js_url(const char *js_url); void set_js_url(const char *js_url);
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE
/** Set local path to the script that's embedded in the index page. Defaults to
*
* @param css_include Local path to web server script.
*/
void set_css_include(const char *css_include);
#endif
#ifdef USE_WEBSERVER_JS_INCLUDE
/** Set local path to the script that's embedded in the index page. Defaults to /** Set local path to the script that's embedded in the index page. Defaults to
* *
* @param js_include Local path to web server script. * @param js_include Local path to web server script.
*/ */
void set_js_include(const char *js_include); void set_js_include(const char *js_include);
#endif
/** Determine whether internal components should be displayed on the web server. /** Determine whether internal components should be displayed on the web server.
* Defaults to false. * Defaults to false.
@ -77,6 +99,11 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
* @param allow_ota. * @param allow_ota.
*/ */
void set_allow_ota(bool allow_ota) { this->allow_ota_ = allow_ota; } void set_allow_ota(bool allow_ota) { this->allow_ota_ = allow_ota; }
/** Set whether or not the webserver should expose the Log.
*
* @param expose_log.
*/
void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; }
// ========== INTERNAL METHODS ========== // ========== INTERNAL METHODS ==========
// (In most use cases you won't need these) // (In most use cases you won't need these)
@ -92,6 +119,9 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
/// Handle an index request under '/'. /// Handle an index request under '/'.
void handle_index_request(AsyncWebServerRequest *request); void handle_index_request(AsyncWebServerRequest *request);
/// Return the webserver configuration as JSON.
std::string get_config_json();
#ifdef USE_WEBSERVER_CSS_INCLUDE #ifdef USE_WEBSERVER_CSS_INCLUDE
/// Handle included css request under '/0.css'. /// Handle included css request under '/0.css'.
void handle_css_request(AsyncWebServerRequest *request); void handle_css_request(AsyncWebServerRequest *request);
@ -240,12 +270,19 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
web_server_base::WebServerBase *base_; web_server_base::WebServerBase *base_;
AsyncEventSource events_{"/events"}; AsyncEventSource events_{"/events"};
ListEntitiesIterator entities_iterator_; ListEntitiesIterator entities_iterator_;
#if USE_WEBSERVER_VERSION == 1
const char *css_url_{nullptr}; const char *css_url_{nullptr};
const char *css_include_{nullptr};
const char *js_url_{nullptr}; const char *js_url_{nullptr};
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE
const char *css_include_{nullptr};
#endif
#ifdef USE_WEBSERVER_JS_INCLUDE
const char *js_include_{nullptr}; const char *js_include_{nullptr};
#endif
bool include_internal_{false}; bool include_internal_{false};
bool allow_ota_{true}; bool allow_ota_{true};
bool expose_log_{true};
#ifdef USE_ESP32 #ifdef USE_ESP32
std::deque<std::function<void()>> to_schedule_; std::deque<std::function<void()>> to_schedule_;
SemaphoreHandle_t to_schedule_lock_; SemaphoreHandle_t to_schedule_lock_;

View file

@ -83,6 +83,7 @@ class WebServerBase : public Component {
return; return;
} }
this->server_ = std::make_shared<AsyncWebServer>(this->port_); this->server_ = std::make_shared<AsyncWebServer>(this->port_);
// All content is controlled and created by user - so allowing all origins is fine here.
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
this->server_->begin(); this->server_->begin();

View file

@ -58,7 +58,9 @@ struct IDFWiFiEvent {
wifi_event_ap_probe_req_rx_t ap_probe_req_rx; wifi_event_ap_probe_req_rx_t ap_probe_req_rx;
wifi_event_bss_rssi_low_t bss_rssi_low; wifi_event_bss_rssi_low_t bss_rssi_low;
ip_event_got_ip_t ip_got_ip; ip_event_got_ip_t ip_got_ip;
#if LWIP_IPV6
ip_event_got_ip6_t ip_got_ip6; ip_event_got_ip6_t ip_got_ip6;
#endif
ip_event_ap_staipassigned_t ip_ap_staipassigned; ip_event_ap_staipassigned_t ip_ap_staipassigned;
} data; } data;
}; };
@ -82,8 +84,10 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi
memcpy(&event.data.sta_disconnected, event_data, sizeof(wifi_event_sta_disconnected_t)); memcpy(&event.data.sta_disconnected, event_data, sizeof(wifi_event_sta_disconnected_t));
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
memcpy(&event.data.ip_got_ip, event_data, sizeof(ip_event_got_ip_t)); memcpy(&event.data.ip_got_ip, event_data, sizeof(ip_event_got_ip_t));
#if LWIP_IPV6
} else if (event_base == IP_EVENT && event_id == IP_EVENT_GOT_IP6) { } else if (event_base == IP_EVENT && event_id == IP_EVENT_GOT_IP6) {
memcpy(&event.data.ip_got_ip6, event_data, sizeof(ip_event_got_ip6_t)); memcpy(&event.data.ip_got_ip6, event_data, sizeof(ip_event_got_ip6_t));
#endif
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { // NOLINT(bugprone-branch-clone) } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { // NOLINT(bugprone-branch-clone)
// no data // no data
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) {
@ -504,7 +508,9 @@ const char *get_auth_mode_str(uint8_t mode) {
} }
std::string format_ip4_addr(const esp_ip4_addr_t &ip) { return str_snprintf(IPSTR, 15, IP2STR(&ip)); } std::string format_ip4_addr(const esp_ip4_addr_t &ip) { return str_snprintf(IPSTR, 15, IP2STR(&ip)); }
#if LWIP_IPV6
std::string format_ip6_addr(const esp_ip6_addr_t &ip) { return str_snprintf(IPV6STR, 39, IPV62STR(ip)); } std::string format_ip6_addr(const esp_ip6_addr_t &ip) { return str_snprintf(IPV6STR, 39, IPV62STR(ip)); }
#endif
const char *get_disconnect_reason_str(uint8_t reason) { const char *get_disconnect_reason_str(uint8_t reason) {
switch (reason) { switch (reason) {
case WIFI_REASON_AUTH_EXPIRE: case WIFI_REASON_AUTH_EXPIRE:
@ -644,9 +650,11 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
format_ip4_addr(it.ip_info.gw).c_str()); format_ip4_addr(it.ip_info.gw).c_str());
s_sta_got_ip = true; s_sta_got_ip = true;
#if LWIP_IPV6
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) { } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) {
const auto &it = data->data.ip_got_ip6; const auto &it = data->data.ip_got_ip6;
ESP_LOGV(TAG, "Event: Got IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); ESP_LOGV(TAG, "Event: Got IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str());
#endif
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) { } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) {
ESP_LOGV(TAG, "Event: Lost IP"); ESP_LOGV(TAG, "Event: Lost IP");

View file

@ -0,0 +1,73 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
)
from esphome import pins
CONF_XL9535 = "xl9535"
DEPENDENCIES = ["i2c"]
CODEOWNERS = ["@mreditor97"]
xl9535_ns = cg.esphome_ns.namespace(CONF_XL9535)
XL9535Component = xl9535_ns.class_("XL9535Component", cg.Component, i2c.I2CDevice)
XL9535GPIOPin = xl9535_ns.class_("XL9535GPIOPin", cg.GPIOPin)
MULTI_CONF = True
CONFIG_SCHEMA = (
cv.Schema({cv.Required(CONF_ID): cv.declare_id(XL9535Component)})
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x20))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
def validate_mode(mode):
if not (mode[CONF_INPUT] or mode[CONF_OUTPUT]) or (
mode[CONF_INPUT] and mode[CONF_OUTPUT]
):
raise cv.Invalid("Mode must be either a input or a output")
return mode
XL9535_PIN_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(XL9535GPIOPin),
cv.Required(CONF_XL9535): cv.use_id(XL9535Component),
cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15),
cv.Optional(CONF_MODE, default={}): cv.All(
{
cv.Optional(CONF_INPUT, default=False): cv.boolean,
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
},
validate_mode,
),
cv.Optional(CONF_INVERTED, default=False): cv.boolean,
}
)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_XL9535, XL9535_PIN_SCHEMA)
async def xl9535_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_XL9535])
cg.add(var.set_parent(parent))
cg.add(var.set_pin(config[CONF_NUMBER]))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View file

@ -0,0 +1,122 @@
#include "xl9535.h"
#include "esphome/core/log.h"
namespace esphome {
namespace xl9535 {
static const char *const TAG = "xl9535";
void XL9535Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up XL9535...");
// Check to see if the device can read from the register
uint8_t port = 0;
if (this->read_register(XL9535_INPUT_PORT_0_REGISTER, &port, 1) != i2c::ERROR_OK) {
this->mark_failed();
return;
}
}
void XL9535Component::dump_config() {
ESP_LOGCONFIG(TAG, "XL9535:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with XL9535 failed!");
}
}
bool XL9535Component::digital_read(uint8_t pin) {
bool state = false;
uint8_t port = 0;
if (pin > 7) {
if (this->read_register(XL9535_INPUT_PORT_1_REGISTER, &port, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return state;
}
state = (port & (pin - 10)) != 0;
} else {
if (this->read_register(XL9535_INPUT_PORT_0_REGISTER, &port, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return state;
}
state = (port & pin) != 0;
}
this->status_clear_warning();
return state;
}
void XL9535Component::digital_write(uint8_t pin, bool value) {
uint8_t port = 0;
uint8_t register_data = 0;
if (pin > 7) {
if (this->read_register(XL9535_OUTPUT_PORT_1_REGISTER, &register_data, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
register_data = register_data & (~(1 << (pin - 10)));
port = register_data | value << (pin - 10);
if (this->write_register(XL9535_OUTPUT_PORT_1_REGISTER, &port, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
} else {
if (this->read_register(XL9535_OUTPUT_PORT_0_REGISTER, &register_data, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
register_data = register_data & (~(1 << pin));
port = register_data | value << pin;
if (this->write_register(XL9535_OUTPUT_PORT_0_REGISTER, &port, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
}
this->status_clear_warning();
}
void XL9535Component::pin_mode(uint8_t pin, gpio::Flags mode) {
uint8_t port = 0;
if (pin > 7) {
this->read_register(XL9535_CONFIG_PORT_1_REGISTER, &port, 1);
if (mode == gpio::FLAG_INPUT) {
port = port | (1 << (pin - 10));
} else if (mode == gpio::FLAG_OUTPUT) {
port = port & (~(1 << (pin - 10)));
}
this->write_register(XL9535_CONFIG_PORT_1_REGISTER, &port, 1);
} else {
this->read_register(XL9535_CONFIG_PORT_0_REGISTER, &port, 1);
if (mode == gpio::FLAG_INPUT) {
port = port | (1 << pin);
} else if (mode == gpio::FLAG_OUTPUT) {
port = port & (~(1 << pin));
}
this->write_register(XL9535_CONFIG_PORT_0_REGISTER, &port, 1);
}
}
void XL9535GPIOPin::setup() { this->pin_mode(this->flags_); }
std::string XL9535GPIOPin::dump_summary() const { return str_snprintf("%u via XL9535", 15, this->pin_); }
void XL9535GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool XL9535GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void XL9535GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
} // namespace xl9535
} // namespace esphome

View file

@ -0,0 +1,54 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace xl9535 {
enum {
XL9535_INPUT_PORT_0_REGISTER = 0x00,
XL9535_INPUT_PORT_1_REGISTER = 0x01,
XL9535_OUTPUT_PORT_0_REGISTER = 0x02,
XL9535_OUTPUT_PORT_1_REGISTER = 0x03,
XL9535_INVERSION_PORT_0_REGISTER = 0x04,
XL9535_INVERSION_PORT_1_REGISTER = 0x05,
XL9535_CONFIG_PORT_0_REGISTER = 0x06,
XL9535_CONFIG_PORT_1_REGISTER = 0x07,
};
class XL9535Component : public Component, public i2c::I2CDevice {
public:
bool digital_read(uint8_t pin);
void digital_write(uint8_t pin, bool value);
void pin_mode(uint8_t pin, gpio::Flags mode);
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
};
class XL9535GPIOPin : public GPIOPin {
public:
void set_parent(XL9535Component *parent) { this->parent_ = parent; }
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_inverted(bool inverted) { this->inverted_ = inverted; }
void set_flags(gpio::Flags flags) { this->flags_ = flags; }
void setup() override;
std::string dump_summary() const override;
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
protected:
XL9535Component *parent_;
uint8_t pin_;
bool inverted_;
gpio::Flags flags_;
};
} // namespace xl9535
} // namespace esphome

View file

@ -368,6 +368,7 @@ CONF_LINE_TYPE = "line_type"
CONF_LOADED_INTEGRATIONS = "loaded_integrations" CONF_LOADED_INTEGRATIONS = "loaded_integrations"
CONF_LOCAL = "local" CONF_LOCAL = "local"
CONF_LOCK_ACTION = "lock_action" CONF_LOCK_ACTION = "lock_action"
CONF_LOG = "log"
CONF_LOG_TOPIC = "log_topic" CONF_LOG_TOPIC = "log_topic"
CONF_LOGGER = "logger" CONF_LOGGER = "logger"
CONF_LOGS = "logs" CONF_LOGS = "logs"

View file

@ -99,7 +99,7 @@ void Application::loop() {
if (this->dump_config_at_ < this->components_.size()) { if (this->dump_config_at_ < this->components_.size()) {
if (this->dump_config_at_ == 0) { if (this->dump_config_at_ == 0) {
ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str()); ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_);
#ifdef ESPHOME_PROJECT_NAME #ifdef ESPHOME_PROJECT_NAME
ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION); ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION);
#endif #endif

View file

@ -56,7 +56,7 @@ namespace esphome {
class Application { class Application {
public: public:
void pre_setup(const std::string &name, const std::string &friendly_name, const std::string &comment, void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment,
const char *compilation_time, bool name_add_mac_suffix) { const char *compilation_time, bool name_add_mac_suffix) {
arch_init(); arch_init();
this->name_add_mac_suffix_ = name_add_mac_suffix; this->name_add_mac_suffix_ = name_add_mac_suffix;
@ -154,11 +154,11 @@ class Application {
/// Get the friendly name of this Application set by pre_setup(). /// Get the friendly name of this Application set by pre_setup().
const std::string &get_friendly_name() const { return this->friendly_name_; } const std::string &get_friendly_name() const { return this->friendly_name_; }
/// Get the comment of this Application set by pre_setup(). /// Get the comment of this Application set by pre_setup().
const std::string &get_comment() const { return this->comment_; } std::string get_comment() const { return this->comment_; }
bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; } bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; }
const std::string &get_compilation_time() const { return this->compilation_time_; } std::string get_compilation_time() const { return this->compilation_time_; }
/** Set the target interval with which to run the loop() calls. /** Set the target interval with which to run the loop() calls.
* If the loop() method takes longer than the target interval, ESPHome won't * If the loop() method takes longer than the target interval, ESPHome won't
@ -376,8 +376,8 @@ class Application {
std::string name_; std::string name_;
std::string friendly_name_; std::string friendly_name_;
std::string comment_; const char *comment_{nullptr};
std::string compilation_time_; const char *compilation_time_{nullptr};
bool name_add_mac_suffix_; bool name_add_mac_suffix_;
uint32_t last_loop_{0}; uint32_t last_loop_{0};
uint32_t loop_interval_{16}; uint32_t loop_interval_{16};

View file

@ -2,6 +2,16 @@
namespace esphome { namespace esphome {
static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); }
static uint8_t days_in_month(uint8_t month, uint16_t year) {
static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
uint8_t days = DAYS_IN_MONTH[month];
if (month == 2 && is_leap_year(year))
return 29;
return days;
}
size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) {
struct tm c_tm = this->to_c_tm(); struct tm c_tm = this->to_c_tm();
return ::strftime(buffer, buffer_len, format, &c_tm); return ::strftime(buffer, buffer_len, format, &c_tm);
@ -158,14 +168,4 @@ template<typename T> bool increment_time_value(T &current, uint16_t begin, uint1
return false; return false;
} }
static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); }
static uint8_t days_in_month(uint8_t month, uint16_t year) {
static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
uint8_t days = DAYS_IN_MONTH[month];
if (month == 2 && is_leap_year(year))
return 29;
return days;
}
} // namespace esphome } // namespace esphome

View file

@ -8,10 +8,6 @@ namespace esphome {
template<typename T> bool increment_time_value(T &current, uint16_t begin, uint16_t end); template<typename T> bool increment_time_value(T &current, uint16_t begin, uint16_t end);
static bool is_leap_year(uint32_t year);
static uint8_t days_in_month(uint8_t month, uint16_t year);
/// A more user-friendly version of struct tm from time.h /// A more user-friendly version of struct tm from time.h
struct ESPTime { struct ESPTime {
/** seconds after the minute [0-60] /** seconds after the minute [0-60]

View file

@ -3,6 +3,7 @@ import binascii
import codecs import codecs
import collections import collections
import functools import functools
import gzip
import hashlib import hashlib
import hmac import hmac
import json import json
@ -485,6 +486,7 @@ class DownloadBinaryRequestHandler(BaseHandler):
@bind_config @bind_config
def get(self, configuration=None): def get(self, configuration=None):
type = self.get_argument("type", "firmware.bin") type = self.get_argument("type", "firmware.bin")
compressed = self.get_argument("compressed", "0") == "1"
storage_path = ext_storage_path(settings.config_dir, configuration) storage_path = ext_storage_path(settings.config_dir, configuration)
storage_json = StorageJSON.load(storage_path) storage_json = StorageJSON.load(storage_path)
@ -534,6 +536,8 @@ class DownloadBinaryRequestHandler(BaseHandler):
self.send_error(404) self.send_error(404)
return return
filename = filename + ".gz" if compressed else filename
self.set_header("Content-Type", "application/octet-stream") self.set_header("Content-Type", "application/octet-stream")
self.set_header("Content-Disposition", f'attachment; filename="{filename}"') self.set_header("Content-Disposition", f'attachment; filename="{filename}"')
self.set_header("Cache-Control", "no-cache") self.set_header("Cache-Control", "no-cache")
@ -543,9 +547,20 @@ class DownloadBinaryRequestHandler(BaseHandler):
with open(path, "rb") as f: with open(path, "rb") as f:
while True: while True:
data = f.read(16384) # For a 528KB image used as benchmark:
# - using 256KB blocks resulted in the smallest file size.
# - blocks larger than 256KB didn't improve the size of compressed file.
# - blocks smaller than 256KB hindered compression, making the output file larger.
# Read file in blocks of 256KB.
data = f.read(256 * 1024)
if not data: if not data:
break break
if compressed:
data = gzip.compress(data, 9)
self.write(data) self.write(data)
self.finish() self.finish()

View file

@ -39,6 +39,7 @@ lib_deps =
bblanchon/ArduinoJson@6.18.5 ; json bblanchon/ArduinoJson@6.18.5 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
pavlodn/HaierProtocol@0.9.18 ; haier
; This is using the repository until a new release is published to PlatformIO ; This is using the repository until a new release is published to PlatformIO
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
build_flags = build_flags =

View file

@ -9,9 +9,9 @@ pyserial==3.5
platformio==6.1.7 # When updating platformio, also update Dockerfile platformio==6.1.7 # When updating platformio, also update Dockerfile
esptool==4.6 esptool==4.6
click==8.1.3 click==8.1.3
esphome-dashboard==20230516.0 esphome-dashboard==20230621.0
aioesphomeapi==14.0.0 aioesphomeapi==14.1.0
zeroconf==0.63.0 zeroconf==0.69.0
# esp-idf requires this, but doesn't bundle it by default # esp-idf requires this, but doesn't bundle it by default
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24

View file

@ -5,7 +5,7 @@ pyupgrade==3.4.0 # also change in .pre-commit-config.yaml when updating
pre-commit pre-commit
# Unit tests # Unit tests
pytest==7.3.1 pytest==7.3.2
pytest-cov==4.1.0 pytest-cov==4.1.0
pytest-mock==3.10.0 pytest-mock==3.10.0
pytest-asyncio==0.21.0 pytest-asyncio==0.21.0

View file

@ -5,12 +5,13 @@ set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
if [ ! -n "$DEVCONTAINER" ] && [ ! -n "$VIRTUAL_ENV" ]; then if [ ! -n "$DEVCONTAINER" ] && [ ! -n "$VIRTUAL_ENV" ] && [ ! "$ESPHOME_NO_VENV" ]; then
python3 -m venv venv python3 -m venv venv
source venv/bin/activate source venv/bin/activate
fi fi
pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
pip3 install setuptools wheel
pip3 install --no-use-pep517 -e . pip3 install --no-use-pep517 -e .
pre-commit install pre-commit install

View file

@ -312,7 +312,8 @@ sensor:
id: freezer_temp_source id: freezer_temp_source
reference_voltage: 3.19 reference_voltage: 3.19
number: 0 number: 0
- platform: airthings_wave_plus - id: airthingswp
platform: airthings_wave_plus
ble_client_id: airthings01 ble_client_id: airthings01
update_interval: 5min update_interval: 5min
temperature: temperature:
@ -329,7 +330,8 @@ sensor:
name: Wave Plus CO2 name: Wave Plus CO2
tvoc: tvoc:
name: Wave Plus VOC name: Wave Plus VOC
- platform: airthings_wave_mini - id: airthingswm
platform: airthings_wave_mini
ble_client_id: airthingsmini01 ble_client_id: airthingsmini01
update_interval: 5min update_interval: 5min
temperature: temperature:
@ -695,6 +697,13 @@ image:
file: mdi:alert-outline file: mdi:alert-outline
type: BINARY type: BINARY
graph:
- id: my_graph
sensor: ha_hello_world_temperature
duration: 1h
width: 100
height: 100
cap1188: cap1188:
id: cap1188_component id: cap1188_component
address: 0x29 address: 0x29

View file

@ -944,13 +944,29 @@ climate:
kd_multiplier: 0.0 kd_multiplier: 0.0
deadband_output_averaging_samples: 1 deadband_output_averaging_samples: 1
- platform: haier - platform: haier
protocol: hOn
name: Haier AC name: Haier AC
supported_swing_modes:
- vertical
- horizontal
- both
update_interval: 10s
uart_id: uart_12 uart_id: uart_12
wifi_signal: true
beeper: true
outdoor_temperature:
name: Haier AC outdoor temperature
visual:
min_temperature: 16 °C
max_temperature: 30 °C
temperature_step: 1 °C
supported_modes:
- 'OFF'
- AUTO
- COOL
- HEAT
- DRY
- FAN_ONLY
supported_swing_modes:
- 'OFF'
- VERTICAL
- HORIZONTAL
- BOTH
sprinkler: sprinkler:
- id: yard_sprinkler_ctrlr - id: yard_sprinkler_ctrlr

View file

@ -384,6 +384,15 @@ binary_sensor:
pullup: true pullup: true
inverted: false inverted: false
- platform: gpio
name: XL9535 Pin 0
pin:
xl9535: xl9535_hub
number: 0
mode:
input: true
inverted: false
climate: climate:
- platform: tuya - platform: tuya
id: tuya_climate id: tuya_climate
@ -745,3 +754,7 @@ voice_assistant:
max6956: max6956:
- id: max6956_1 - id: max6956_1
address: 0x40 address: 0x40
xl9535:
- id: xl9535_hub
address: 0x20