mirror of
https://github.com/esphome/esphome.git
synced 2024-12-27 07:51:43 +01:00
Merge branch 'dev' into optolink
This commit is contained in:
commit
4973dd4a5d
88 changed files with 5472 additions and 2194 deletions
38
.github/actions/restore-python/action.yml
vendored
Normal file
38
.github/actions/restore-python/action.yml
vendored
Normal 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 .
|
90
.github/workflows/ci.yml
vendored
90
.github/workflows/ci.yml
vendored
|
@ -26,10 +26,16 @@ jobs:
|
|||
common:
|
||||
name: Create common environment
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
@ -39,7 +45,7 @@ jobs:
|
|||
with:
|
||||
path: venv
|
||||
# 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
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
|
@ -66,12 +72,11 @@ jobs:
|
|||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Restore Python virtual environment
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Run black
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
@ -88,12 +93,11 @@ jobs:
|
|||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Restore Python virtual environment
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Run flake8
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
@ -110,12 +114,11 @@ jobs:
|
|||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Restore Python virtual environment
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Run pylint
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
@ -132,12 +135,11 @@ jobs:
|
|||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Restore Python virtual environment
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Run pyupgrade
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
@ -154,12 +156,11 @@ jobs:
|
|||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Restore Python virtual environment
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Register matcher
|
||||
run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json"
|
||||
- name: Run script/ci-custom
|
||||
|
@ -176,12 +177,11 @@ jobs:
|
|||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Restore Python virtual environment
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Register matcher
|
||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||
- name: Run pytest
|
||||
|
@ -197,12 +197,11 @@ jobs:
|
|||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Restore Python virtual environment
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Install clang-format
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
@ -237,12 +236,11 @@ jobs:
|
|||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Restore Python virtual environment
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache@v3.3.1
|
||||
with:
|
||||
|
@ -300,13 +298,11 @@ jobs:
|
|||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Restore Python virtual environment
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
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
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache@v3.3.1
|
||||
with:
|
||||
|
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
@ -49,9 +49,11 @@ jobs:
|
|||
with:
|
||||
python-version: "3.x"
|
||||
- name: Set up python environment
|
||||
env:
|
||||
ESPHOME_NO_VENV: 1
|
||||
run: |
|
||||
script/setup
|
||||
pip install setuptools wheel twine
|
||||
pip install twine
|
||||
- name: Build
|
||||
run: python setup.py sdist bdist_wheel
|
||||
- name: Upload
|
||||
|
|
|
@ -17,6 +17,7 @@ esphome/components/adc/* @esphome/core
|
|||
esphome/components/adc128s102/* @DeerMaximum
|
||||
esphome/components/addressable_light/* @justfalter
|
||||
esphome/components/airthings_ble/* @jeromelaban
|
||||
esphome/components/airthings_wave_base/* @jeromelaban @ncareau
|
||||
esphome/components/airthings_wave_mini/* @ncareau
|
||||
esphome/components/airthings_wave_plus/* @jeromelaban
|
||||
esphome/components/alarm_control_panel/* @grahambrown11
|
||||
|
@ -102,7 +103,7 @@ esphome/components/gpio/* @esphome/core
|
|||
esphome/components/gps/* @coogle
|
||||
esphome/components/graph/* @synco
|
||||
esphome/components/growatt_solar/* @leeuwte
|
||||
esphome/components/haier/* @Yarikx
|
||||
esphome/components/haier/* @paveldn
|
||||
esphome/components/havells_solar/* @sourabhjaiswal
|
||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||
esphome/components/hbridge/light/* @DotNetDann
|
||||
|
@ -319,4 +320,5 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl
|
|||
esphome/components/xiaomi_mhoc303/* @drug123
|
||||
esphome/components/xiaomi_mhoc401/* @vevsvevs
|
||||
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
|
||||
esphome/components/xl9535/* @mreditor97
|
||||
esphome/components/xpt2046/* @nielsnl68 @numo68
|
||||
|
|
83
esphome/components/airthings_wave_base/__init__.py
Normal file
83
esphome/components/airthings_wave_base/__init__.py
Normal 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))
|
|
@ -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
|
50
esphome/components/airthings_wave_base/airthings_wave_base.h
Normal file
50
esphome/components/airthings_wave_base/airthings_wave_base.h
Normal 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
|
|
@ -7,105 +7,47 @@ namespace 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,
|
||||
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) {
|
||||
void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) {
|
||||
auto *value = (WaveMiniReadings *) raw_value;
|
||||
|
||||
if (sizeof(WaveMiniReadings) <= value_len) {
|
||||
this->humidity_sensor_->publish_state(value->humidity / 100.0f);
|
||||
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
|
||||
this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f);
|
||||
if (is_valid_voc_value_(value->voc)) {
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
this->humidity_sensor_->publish_state(value->humidity / 100.0f);
|
||||
}
|
||||
|
||||
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 instance must not stay connected
|
||||
// so other clients can connect to it (e.g. the
|
||||
// mobile app).
|
||||
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);
|
||||
this->parent()->set_enabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
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(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
||||
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
|
||||
}
|
||||
|
||||
AirthingsWaveMini::AirthingsWaveMini()
|
||||
: PollingComponent(10000),
|
||||
service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)),
|
||||
sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {}
|
||||
AirthingsWaveMini::AirthingsWaveMini() {
|
||||
this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID);
|
||||
this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
|
||||
}
|
||||
|
||||
} // namespace airthings_wave_mini
|
||||
} // namespace esphome
|
||||
|
|
|
@ -2,14 +2,7 @@
|
|||
|
||||
#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"
|
||||
#include "esphome/components/airthings_wave_base/airthings_wave_base.h"
|
||||
|
||||
namespace esphome {
|
||||
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 CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba";
|
||||
|
||||
class AirthingsWaveMini : public PollingComponent, public ble_client::BLEClientNode {
|
||||
class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase {
|
||||
public:
|
||||
AirthingsWaveMini();
|
||||
|
||||
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:
|
||||
bool is_valid_voc_value_(uint16_t voc);
|
||||
|
||||
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_;
|
||||
void read_sensors(uint8_t *value, uint16_t value_len) override;
|
||||
|
||||
struct WaveMiniReadings {
|
||||
uint16_t unused01;
|
||||
|
|
|
@ -1,82 +1,28 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, ble_client
|
||||
from esphome.components import airthings_wave_base
|
||||
|
||||
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_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")
|
||||
AirthingsWaveMini = airthings_wave_mini_ns.class_(
|
||||
"AirthingsWaveMini", cg.PollingComponent, ble_client.BLEClientNode
|
||||
"AirthingsWaveMini", airthings_wave_base.AirthingsWaveBase
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
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),
|
||||
CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AirthingsWaveMini),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
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))
|
||||
await airthings_wave_base.wave_base_to_code(var, config)
|
||||
|
|
|
@ -7,55 +7,7 @@ namespace 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,
|
||||
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) {
|
||||
void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) {
|
||||
auto *value = (WavePlusReadings *) raw_value;
|
||||
|
||||
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) {
|
||||
ESP_LOGD(TAG, "ambient light = %d", value->ambientLight);
|
||||
|
||||
this->humidity_sensor_->publish_state(value->humidity / 2.0f);
|
||||
if (is_valid_radon_value_(value->radon)) {
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
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);
|
||||
}
|
||||
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->temperature_sensor_->publish_state(value->temperature / 100.0f);
|
||||
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
|
||||
if (is_valid_co2_value_(value->co2)) {
|
||||
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
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);
|
||||
}
|
||||
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 instance must not stay connected
|
||||
// so other clients can connect to it (e.g. the
|
||||
// mobile app).
|
||||
parent()->set_enabled(false);
|
||||
this->parent()->set_enabled(false);
|
||||
} else {
|
||||
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_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 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() {
|
||||
// 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(" ", "Radon", this->radon_sensor_);
|
||||
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
||||
LOG_SENSOR(" ", "CO2", this->co2_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()
|
||||
: PollingComponent(10000),
|
||||
service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)),
|
||||
sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {}
|
||||
AirthingsWavePlus::AirthingsWavePlus() {
|
||||
this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID);
|
||||
this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
|
||||
}
|
||||
|
||||
} // namespace airthings_wave_plus
|
||||
} // namespace esphome
|
||||
|
|
|
@ -2,14 +2,7 @@
|
|||
|
||||
#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"
|
||||
#include "esphome/components/airthings_wave_base/airthings_wave_base.h"
|
||||
|
||||
namespace esphome {
|
||||
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 CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba";
|
||||
|
||||
class AirthingsWavePlus : public PollingComponent, public ble_client::BLEClientNode {
|
||||
class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
|
||||
public:
|
||||
AirthingsWavePlus();
|
||||
|
||||
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_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_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
|
||||
|
||||
protected:
|
||||
bool is_valid_radon_value_(uint16_t radon);
|
||||
bool is_valid_voc_value_(uint16_t voc);
|
||||
bool is_valid_co2_value_(uint16_t co2);
|
||||
|
||||
void read_sensors_(uint8_t *value, uint16_t value_len);
|
||||
void request_read_values_();
|
||||
void read_sensors(uint8_t *value, uint16_t value_len) override;
|
||||
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *radon_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 *tvoc_sensor_{nullptr};
|
||||
|
||||
uint16_t handle_;
|
||||
esp32_ble_tracker::ESPBTUUID service_uuid_;
|
||||
esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_;
|
||||
|
||||
struct WavePlusReadings {
|
||||
uint8_t version;
|
||||
|
|
|
@ -1,116 +1,64 @@
|
|||
import esphome.codegen as cg
|
||||
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 (
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_PERCENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_HECTOPASCAL,
|
||||
ICON_RADIOACTIVE,
|
||||
CONF_ID,
|
||||
CONF_RADON,
|
||||
CONF_RADON_LONG_TERM,
|
||||
CONF_HUMIDITY,
|
||||
CONF_TVOC,
|
||||
CONF_CO2,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
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")
|
||||
AirthingsWavePlus = airthings_wave_plus_ns.class_(
|
||||
"AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode
|
||||
"AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
|
||||
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_RADON): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
icon=ICON_RADIOACTIVE,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
icon=ICON_RADIOACTIVE,
|
||||
accuracy_decimals=0,
|
||||
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),
|
||||
CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
|
||||
cv.Optional(CONF_RADON): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
icon=ICON_RADIOACTIVE,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
icon=ICON_RADIOACTIVE,
|
||||
accuracy_decimals=0,
|
||||
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,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
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:
|
||||
sens = await sensor.new_sensor(config[CONF_RADON])
|
||||
cg.add(var.set_radon(sens))
|
||||
if CONF_RADON_LONG_TERM in config:
|
||||
sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM])
|
||||
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:
|
||||
sens = await sensor.new_sensor(config[CONF_CO2])
|
||||
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))
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import logging
|
||||
|
||||
from esphome import core
|
||||
from esphome import automation, core
|
||||
from esphome.components import display, font
|
||||
import esphome.components.image as espImage
|
||||
from esphome.components.image import CONF_USE_TRANSPARENCY
|
||||
|
@ -18,15 +18,28 @@ from esphome.core import CORE, HexInt
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@syndlex"]
|
||||
DEPENDENCIES = ["display"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_LOOP = "loop"
|
||||
CONF_START_FRAME = "start_frame"
|
||||
CONF_END_FRAME = "end_frame"
|
||||
CONF_FRAME = "frame"
|
||||
|
||||
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):
|
||||
"""
|
||||
|
@ -74,7 +87,35 @@ ANIMATION_SCHEMA = cv.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):
|
||||
|
|
|
@ -94,3 +94,5 @@ async def to_code(config):
|
|||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_pressure_sensor(sens))
|
||||
cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING]))
|
||||
|
||||
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))
|
||||
|
|
|
@ -6,102 +6,100 @@ namespace esphome {
|
|||
namespace captive_portal {
|
||||
|
||||
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,
|
||||
0xac, 0x92, 0x74, 0x34, 0x8d, 0xc5, 0xd1, 0x31, 0x97, 0x35, 0xd2, 0x14, 0x89, 0x37, 0x45, 0x0b, 0x24, 0x69, 0x00,
|
||||
0xbb, 0x5d, 0x14, 0x69, 0x00, 0x73, 0x24, 0x6a, 0xc4, 0x58, 0xa2, 0x54, 0x91, 0x9a, 0x23, 0x83, 0xd9, 0xdf, 0xde,
|
||||
0x47, 0x52, 0x73, 0x38, 0x6b, 0x2f, 0x90, 0x62, 0x8b, 0xa2, 0x4d, 0x6c, 0x9a, 0xc7, 0x3b, 0x3f, 0xf2, 0xf1, 0x3d,
|
||||
0x2a, 0xfa, 0x2a, 0xad, 0x12, 0xb9, 0xad, 0x29, 0xca, 0x65, 0x59, 0xcc, 0x23, 0xd5, 0xa2, 0x82, 0xf0, 0x65, 0x4c,
|
||||
0x39, 0x8c, 0x28, 0x49, 0xe7, 0x51, 0x49, 0x25, 0x41, 0x49, 0x4e, 0x1a, 0x41, 0x65, 0xfc, 0xd3, 0xcd, 0x77, 0xce,
|
||||
0x14, 0x0d, 0xe6, 0x51, 0xc1, 0xf8, 0x1d, 0x6a, 0x68, 0x11, 0xb3, 0xa4, 0xe2, 0x28, 0x6f, 0x68, 0x16, 0xa7, 0x44,
|
||||
0x92, 0x90, 0x95, 0x64, 0x49, 0x15, 0x81, 0x66, 0xe3, 0xa4, 0xa4, 0xf1, 0x8a, 0xd1, 0x75, 0x5d, 0x35, 0x12, 0x01,
|
||||
0xa5, 0xa4, 0x5c, 0xc6, 0xd6, 0x9a, 0xa5, 0x32, 0x8f, 0x53, 0xba, 0x62, 0x09, 0x75, 0xf4, 0xe0, 0x82, 0x71, 0x26,
|
||||
0x19, 0x29, 0x1c, 0x91, 0x90, 0x82, 0xc6, 0xde, 0x45, 0x2b, 0x68, 0xa3, 0x07, 0x64, 0x01, 0x63, 0x5e, 0x59, 0x20,
|
||||
0x52, 0x24, 0x0d, 0xab, 0x25, 0x52, 0xf6, 0xc6, 0x65, 0x95, 0xb6, 0x05, 0x9d, 0x67, 0x2d, 0x4f, 0x24, 0x03, 0x0b,
|
||||
0x84, 0xcd, 0xfb, 0xbb, 0x82, 0x4a, 0x44, 0xe3, 0x37, 0x44, 0xe6, 0xb8, 0x24, 0x1b, 0xdb, 0x74, 0x18, 0xb7, 0xfd,
|
||||
0x6f, 0x6c, 0xfe, 0xdc, 0x73, 0xdd, 0xfe, 0x85, 0x6e, 0xdc, 0xfe, 0x00, 0xfe, 0xce, 0x1a, 0x2a, 0xdb, 0x86, 0x23,
|
||||
0x62, 0xdf, 0x46, 0x35, 0x50, 0xa2, 0x34, 0xb6, 0x4a, 0xcf, 0xc7, 0xae, 0x3b, 0x45, 0xde, 0x25, 0xf6, 0x47, 0x8e,
|
||||
0xe7, 0xe1, 0xc0, 0xf1, 0x46, 0xc9, 0xc4, 0x19, 0x21, 0x6f, 0x08, 0x8d, 0xef, 0xe3, 0x11, 0x72, 0x3f, 0x59, 0x28,
|
||||
0x63, 0x45, 0x11, 0x5b, 0xbc, 0xe2, 0xd4, 0x42, 0x42, 0x36, 0xd5, 0x1d, 0x8d, 0xad, 0xa4, 0x6d, 0x1a, 0xf0, 0xee,
|
||||
0xaa, 0x2a, 0xaa, 0x06, 0xac, 0xfd, 0x95, 0xa3, 0x7b, 0xff, 0xbe, 0x58, 0x87, 0x6c, 0x08, 0x17, 0x59, 0xd5, 0x94,
|
||||
0xb1, 0xa5, 0x41, 0xb1, 0x9f, 0xee, 0xe8, 0x1e, 0xa9, 0xa6, 0x7f, 0xb6, 0xe8, 0x54, 0x0d, 0x5b, 0x32, 0x1e, 0x5b,
|
||||
0x9e, 0x8f, 0xbc, 0x29, 0xe8, 0xbd, 0xed, 0xef, 0x8f, 0xa0, 0x10, 0x05, 0x4a, 0xe7, 0x66, 0x65, 0xbf, 0xbf, 0x8d,
|
||||
0xc4, 0x6a, 0x89, 0x36, 0x65, 0xc1, 0x45, 0x6c, 0xe5, 0x52, 0xd6, 0xe1, 0x60, 0xb0, 0x5e, 0xaf, 0xf1, 0x3a, 0xc0,
|
||||
0x55, 0xb3, 0x1c, 0xf8, 0xae, 0xeb, 0x0e, 0x80, 0xc2, 0x42, 0x66, 0x7f, 0x2c, 0x7f, 0x68, 0xa1, 0x9c, 0xb2, 0x65,
|
||||
0x2e, 0x75, 0x7f, 0xfe, 0x74, 0xc7, 0xf7, 0x91, 0xa2, 0x98, 0xdf, 0x7e, 0x38, 0xd3, 0xd2, 0x9c, 0x69, 0xe1, 0xdf,
|
||||
0x12, 0xdb, 0x3a, 0xb8, 0xda, 0x7b, 0xa3, 0x8c, 0x9a, 0x10, 0x1f, 0xf9, 0xc8, 0xd5, 0xff, 0x7d, 0x47, 0xf5, 0xbb,
|
||||
0x91, 0xf3, 0xd9, 0x08, 0x9d, 0x8d, 0xe0, 0xaf, 0x02, 0xd0, 0x2f, 0xc7, 0xce, 0xe5, 0x91, 0xdf, 0x53, 0xeb, 0x2b,
|
||||
0xcf, 0x3d, 0x4d, 0x28, 0xa6, 0xef, 0xc7, 0xe7, 0x63, 0xc7, 0xff, 0x59, 0x11, 0x68, 0xf4, 0x8f, 0x5c, 0x8e, 0x9f,
|
||||
0x7b, 0x3f, 0x8f, 0xc9, 0x08, 0x8d, 0xba, 0x99, 0x91, 0xa3, 0xfa, 0xc7, 0x91, 0xd6, 0x85, 0x46, 0x2b, 0x20, 0x2b,
|
||||
0x9d, 0xb1, 0x33, 0x22, 0x01, 0x0a, 0x3a, 0xab, 0xa0, 0x07, 0xd3, 0x63, 0xe0, 0x3e, 0x9b, 0x73, 0x82, 0x4f, 0xbd,
|
||||
0xc1, 0xdc, 0xea, 0x87, 0x96, 0x75, 0x82, 0xa1, 0x3a, 0x87, 0x01, 0x7f, 0xac, 0xe0, 0xdc, 0x59, 0x56, 0x7f, 0x6f,
|
||||
0x7d, 0x2b, 0xc8, 0x8a, 0x5a, 0x71, 0x1c, 0x43, 0xa8, 0xb5, 0x25, 0x9c, 0x10, 0x5c, 0x54, 0x09, 0x51, 0x2c, 0x58,
|
||||
0x50, 0xd2, 0x24, 0xf9, 0xd7, 0x5f, 0xdb, 0xc7, 0xa5, 0x25, 0x95, 0xaf, 0x0a, 0xaa, 0xba, 0xe2, 0xe5, 0xf6, 0x86,
|
||||
0x2c, 0xdf, 0x42, 0x00, 0xd9, 0x16, 0x11, 0x2c, 0xa5, 0x56, 0xff, 0xbd, 0xfb, 0x01, 0x0b, 0xb9, 0x2d, 0x28, 0x4e,
|
||||
0x99, 0xa8, 0x0b, 0xb2, 0x8d, 0xad, 0x05, 0xc8, 0xba, 0xb3, 0xfa, 0x17, 0x19, 0x95, 0x49, 0x6e, 0x5b, 0x03, 0x08,
|
||||
0xb1, 0x8c, 0x2d, 0xf1, 0x47, 0x51, 0x71, 0xab, 0x8f, 0x65, 0x4e, 0xb9, 0x6d, 0x1f, 0x2c, 0x54, 0xf6, 0x71, 0xbd,
|
||||
0x64, 0x3f, 0xb4, 0x74, 0xb4, 0x41, 0x32, 0xa9, 0x42, 0x0e, 0xab, 0xe0, 0xbd, 0x38, 0xce, 0x2e, 0xaa, 0x74, 0xfb,
|
||||
0x88, 0x79, 0xb9, 0x67, 0x6c, 0x63, 0x9c, 0xd3, 0xe6, 0x86, 0x6e, 0xe0, 0xb8, 0xfc, 0x9b, 0x7d, 0xc7, 0xd0, 0x5b,
|
||||
0x2a, 0xd7, 0x55, 0x73, 0x27, 0x42, 0x64, 0x3d, 0x37, 0xe2, 0x66, 0x26, 0x42, 0x39, 0x26, 0xb5, 0xc0, 0xa2, 0x80,
|
||||
0xf0, 0xb7, 0xbd, 0x3e, 0xc4, 0x6a, 0x7d, 0xdf, 0x14, 0x83, 0xe2, 0x6d, 0x94, 0xb2, 0x15, 0x4a, 0x0a, 0x22, 0xe0,
|
||||
0xb8, 0x72, 0x23, 0xcb, 0x42, 0x87, 0xb8, 0xaa, 0x78, 0x02, 0xfc, 0x77, 0xb1, 0xf5, 0x00, 0x76, 0x2f, 0xb7, 0x3f,
|
||||
0xa4, 0x76, 0x4f, 0x00, 0x6a, 0xbd, 0x3e, 0x5e, 0x91, 0xa2, 0xa5, 0x28, 0x46, 0x32, 0x67, 0xe2, 0x64, 0xe2, 0xec,
|
||||
0x51, 0xb6, 0x5a, 0xdc, 0x01, 0x57, 0x06, 0xcb, 0xc2, 0xee, 0x5b, 0xc7, 0x38, 0x8e, 0x88, 0xb9, 0xe5, 0xac, 0x27,
|
||||
0xd6, 0x67, 0x36, 0x39, 0x05, 0xcd, 0xa4, 0x75, 0x16, 0xf0, 0x4f, 0x77, 0x70, 0x1b, 0xe1, 0x06, 0xf4, 0xf7, 0xf7,
|
||||
0xa7, 0xd9, 0x48, 0xd4, 0x84, 0x7f, 0xce, 0xaa, 0x6c, 0xd4, 0x81, 0x85, 0x55, 0x4f, 0x45, 0x17, 0x10, 0x9d, 0x74,
|
||||
0x0e, 0xc8, 0xb1, 0xff, 0x74, 0x07, 0x71, 0xa6, 0x8e, 0xce, 0xdd, 0x49, 0x68, 0x34, 0x00, 0x84, 0xe6, 0xb7, 0xfb,
|
||||
0x7e, 0xff, 0xe4, 0xce, 0x6f, 0x2d, 0x6d, 0xb6, 0xd7, 0xb4, 0xa0, 0x89, 0xac, 0x1a, 0xdb, 0x7a, 0x02, 0x9a, 0xe0,
|
||||
0x24, 0x68, 0xbf, 0xbf, 0xbf, 0x79, 0xf3, 0x3a, 0xae, 0x6c, 0xda, 0xbf, 0x78, 0x8c, 0x5a, 0xdd, 0xea, 0xef, 0xe1,
|
||||
0x56, 0xff, 0x4f, 0xdc, 0x53, 0xf7, 0x7a, 0xef, 0x03, 0xb0, 0x1a, 0xaf, 0x4f, 0x97, 0xbb, 0xba, 0x00, 0x9e, 0xc3,
|
||||
0x25, 0x72, 0x61, 0x3d, 0x17, 0xb6, 0x33, 0x1e, 0xf5, 0x41, 0x3d, 0xfc, 0x80, 0xe9, 0xfa, 0x7a, 0x86, 0x6b, 0x5a,
|
||||
0x1d, 0xd1, 0xf9, 0x37, 0xbb, 0x45, 0xb5, 0x71, 0x04, 0xfb, 0xc4, 0xf8, 0x32, 0x64, 0x3c, 0xa7, 0x0d, 0x93, 0x7b,
|
||||
0x30, 0x17, 0x6e, 0xfa, 0xba, 0x95, 0xbb, 0x9a, 0xa4, 0xa9, 0x5a, 0x19, 0xd5, 0x9b, 0x59, 0x06, 0x79, 0x41, 0x51,
|
||||
0xd2, 0xd0, 0xa3, 0xe5, 0xde, 0xac, 0xeb, 0x2b, 0x28, 0xbc, 0x1c, 0x3d, 0xdb, 0xab, 0x83, 0xb7, 0x93, 0xb0, 0x65,
|
||||
0x0e, 0x29, 0xd8, 0x92, 0x87, 0x09, 0xd8, 0x4d, 0x1b, 0xc3, 0x94, 0x91, 0x92, 0x15, 0xdb, 0x50, 0xc0, 0x65, 0xe8,
|
||||
0x40, 0xc2, 0x60, 0xd9, 0x7e, 0xd1, 0x4a, 0x59, 0x71, 0xd0, 0xdd, 0xa4, 0xb4, 0x09, 0xdd, 0x99, 0xe9, 0x38, 0x0d,
|
||||
0x49, 0x59, 0x2b, 0x42, 0x1c, 0x34, 0xb4, 0x9c, 0x2d, 0x48, 0x72, 0xb7, 0x6c, 0xaa, 0x96, 0xa7, 0x4e, 0xa2, 0x6e,
|
||||
0xeb, 0xf0, 0x89, 0x97, 0x91, 0x80, 0x26, 0xb3, 0x6e, 0x94, 0x65, 0xd9, 0x0c, 0x90, 0xa0, 0x8e, 0xb9, 0xfc, 0x42,
|
||||
0x1f, 0x0f, 0x15, 0xdb, 0x99, 0x99, 0xd8, 0x57, 0x13, 0xc6, 0x46, 0x48, 0x25, 0xcf, 0x66, 0x07, 0x77, 0xdc, 0x19,
|
||||
0xa4, 0x01, 0x01, 0x42, 0x6a, 0x88, 0x7f, 0x30, 0x73, 0x5f, 0x12, 0xc6, 0xcf, 0xad, 0x57, 0x67, 0x65, 0xd6, 0x85,
|
||||
0x2f, 0xc0, 0xa2, 0xd5, 0xe8, 0x20, 0x9e, 0x41, 0xa2, 0x32, 0xb9, 0x30, 0xf4, 0xc7, 0x6e, 0xbd, 0xd9, 0xe3, 0xee,
|
||||
0x8c, 0xec, 0x0e, 0xd4, 0x59, 0x41, 0x37, 0xb3, 0x8f, 0xad, 0x90, 0x2c, 0xdb, 0x3a, 0x5d, 0x2e, 0x0d, 0xe1, 0xbc,
|
||||
0x40, 0x0e, 0x5d, 0x00, 0x29, 0xa5, 0x7c, 0xa6, 0x75, 0x38, 0x4c, 0xd2, 0x52, 0x74, 0x38, 0x1d, 0xc5, 0xe8, 0x53,
|
||||
0x7a, 0x5f, 0xd6, 0xff, 0xa2, 0x56, 0xc7, 0x71, 0x57, 0x92, 0x06, 0x72, 0x8b, 0xb3, 0xa8, 0x00, 0xd3, 0x32, 0x74,
|
||||
0x26, 0xb0, 0x57, 0xdd, 0x94, 0x12, 0x06, 0x9e, 0x83, 0x99, 0xfa, 0x6e, 0x3a, 0xe0, 0xed, 0xd5, 0x1b, 0x24, 0xaa,
|
||||
0x82, 0xa5, 0x1d, 0x9d, 0x26, 0x41, 0xee, 0x11, 0x1e, 0x0f, 0xb6, 0x1b, 0xa9, 0xb9, 0x03, 0xd4, 0xc3, 0x6c, 0x4a,
|
||||
0x3c, 0xf7, 0x81, 0x1d, 0x49, 0xb3, 0xcc, 0x5f, 0x64, 0x47, 0xa4, 0x54, 0xaa, 0xdd, 0xb3, 0xee, 0x54, 0xf8, 0x43,
|
||||
0x10, 0x70, 0xd8, 0x1b, 0xe8, 0xef, 0x99, 0x8e, 0x8b, 0xdd, 0x99, 0x14, 0x7d, 0x52, 0xc3, 0xb6, 0x29, 0xec, 0x87,
|
||||
0x4e, 0xee, 0xb3, 0xe0, 0xea, 0x94, 0x09, 0x7b, 0x8f, 0x67, 0xc2, 0x1e, 0x52, 0xb5, 0xcb, 0xcb, 0x6a, 0x13, 0xf7,
|
||||
0x74, 0x4e, 0x1a, 0xc2, 0x4f, 0xef, 0x59, 0xf0, 0x0a, 0xf8, 0xff, 0x2f, 0x29, 0xee, 0x0f, 0xa7, 0xb7, 0x2f, 0x48,
|
||||
0x6d, 0x5f, 0x98, 0xd5, 0x8c, 0x77, 0xca, 0x79, 0xe8, 0x41, 0xfa, 0x62, 0x58, 0xb0, 0xa5, 0xf7, 0x67, 0x40, 0xfb,
|
||||
0xdf, 0x38, 0x06, 0x2f, 0xbc, 0x29, 0xbe, 0x44, 0xba, 0x31, 0x10, 0xe1, 0x60, 0x8a, 0x26, 0x57, 0x43, 0x3c, 0xf4,
|
||||
0x90, 0xaa, 0x9a, 0xc6, 0x68, 0x82, 0xa7, 0x40, 0x30, 0xc6, 0xc1, 0x04, 0x26, 0x90, 0xef, 0xe1, 0xd1, 0x6b, 0x3f,
|
||||
0xc0, 0xe3, 0x11, 0x50, 0xf9, 0x2e, 0x0e, 0x7c, 0x64, 0x68, 0xc7, 0xd8, 0x07, 0x71, 0x8a, 0x24, 0x28, 0x01, 0xe8,
|
||||
0x24, 0xc0, 0xee, 0x04, 0xc4, 0x8d, 0xb1, 0x7b, 0x89, 0xa7, 0x63, 0x34, 0xc5, 0x13, 0x80, 0x0e, 0x0f, 0x47, 0x85,
|
||||
0x33, 0xc2, 0x1e, 0x4c, 0x07, 0x63, 0x32, 0xc5, 0xc3, 0x00, 0xe9, 0xc6, 0xc0, 0x31, 0x01, 0x11, 0x0e, 0x76, 0xbd,
|
||||
0xd7, 0x01, 0xf6, 0x27, 0xa0, 0x77, 0x38, 0x7c, 0x01, 0x62, 0x2f, 0x87, 0xc8, 0xb4, 0x06, 0x5e, 0x50, 0x30, 0x7a,
|
||||
0x0c, 0x34, 0xff, 0x9f, 0x0b, 0x1a, 0x40, 0xe2, 0xa1, 0x00, 0x5f, 0x42, 0xec, 0x7a, 0x8a, 0xdf, 0xb4, 0x06, 0x37,
|
||||
0xcf, 0x43, 0xee, 0x1f, 0xc6, 0x2c, 0xf8, 0xe7, 0x62, 0xe6, 0x29, 0x04, 0xa0, 0x0b, 0xba, 0x41, 0x0e, 0xd2, 0x8d,
|
||||
0xd1, 0x0d, 0xcc, 0xd3, 0xab, 0x4b, 0x34, 0x05, 0xae, 0xf1, 0x14, 0x5d, 0xa2, 0x91, 0x42, 0x17, 0xd8, 0x87, 0x86,
|
||||
0xc9, 0x01, 0xa6, 0x2f, 0x84, 0x71, 0xf8, 0x37, 0x86, 0xf1, 0x31, 0x9f, 0xfe, 0xc6, 0x2e, 0xfd, 0x15, 0x57, 0x10,
|
||||
0x94, 0x63, 0xba, 0x0c, 0x8b, 0x06, 0xe6, 0x15, 0xaf, 0xaa, 0x28, 0x78, 0x94, 0x43, 0x35, 0x02, 0xef, 0x7a, 0x0f,
|
||||
0xb1, 0x34, 0xce, 0xbd, 0xf9, 0xbd, 0x2a, 0x1d, 0x28, 0xbd, 0x79, 0xa4, 0xd3, 0xf9, 0xfc, 0x26, 0xa7, 0xe8, 0xd5,
|
||||
0xf5, 0x3b, 0x78, 0x08, 0x16, 0x05, 0xe2, 0xd5, 0x1a, 0xde, 0x9b, 0x5b, 0x24, 0x2b, 0xf5, 0x82, 0xe7, 0x50, 0x2a,
|
||||
0xaa, 0x2e, 0x3c, 0x20, 0x50, 0x57, 0x2c, 0x60, 0x8c, 0xa3, 0x45, 0x33, 0x7f, 0x57, 0x50, 0x22, 0x28, 0x5a, 0xb2,
|
||||
0x15, 0x45, 0x4c, 0x42, 0x1d, 0x50, 0x52, 0x24, 0x99, 0x6a, 0x8e, 0x8c, 0x9a, 0xee, 0x6d, 0x25, 0x69, 0x88, 0xae,
|
||||
0xaa, 0x7a, 0xab, 0x85, 0x24, 0x39, 0xe1, 0x4b, 0x9a, 0x1e, 0x84, 0x29, 0xea, 0x6d, 0xd5, 0x36, 0xe8, 0x97, 0x17,
|
||||
0x6f, 0x5e, 0xab, 0x87, 0x36, 0x45, 0x4e, 0xa7, 0x6c, 0x23, 0xd1, 0x8f, 0x37, 0x2f, 0x50, 0x5b, 0xc3, 0xa6, 0x53,
|
||||
0x63, 0x5b, 0xb5, 0xa2, 0xcd, 0x1a, 0x2a, 0x4b, 0xaa, 0x48, 0x40, 0xb9, 0xa0, 0x52, 0x42, 0xa1, 0x21, 0x30, 0x94,
|
||||
0xce, 0xda, 0x13, 0x53, 0x75, 0x83, 0xbb, 0x20, 0x7e, 0xde, 0x95, 0xd7, 0x51, 0x1e, 0x18, 0xd7, 0xaf, 0x3b, 0x6a,
|
||||
0x70, 0x3d, 0x98, 0x47, 0xea, 0x39, 0x8d, 0x88, 0x7e, 0x84, 0xc4, 0x83, 0x35, 0xcb, 0x98, 0x7a, 0xb8, 0xcd, 0x23,
|
||||
0x5d, 0x8f, 0x2a, 0x09, 0xaa, 0x24, 0x32, 0x5f, 0x34, 0x74, 0xaf, 0xa0, 0x7c, 0x09, 0xaf, 0x64, 0xd8, 0x70, 0xa8,
|
||||
0x50, 0x12, 0x9a, 0x57, 0x05, 0x54, 0x40, 0xf1, 0xf5, 0xf5, 0x0f, 0xff, 0x52, 0x9f, 0x3f, 0xc0, 0xcf, 0x13, 0x27,
|
||||
0x3c, 0x29, 0x0c, 0xa3, 0xea, 0x74, 0x7c, 0xe3, 0xa1, 0xf9, 0x90, 0x51, 0xc3, 0x7b, 0x00, 0xfc, 0x4e, 0xef, 0x49,
|
||||
0x79, 0x77, 0x98, 0xec, 0x24, 0xe9, 0x5f, 0x5d, 0xd9, 0x1a, 0x26, 0xd1, 0x2e, 0x4a, 0x26, 0xe7, 0xd7, 0x60, 0x60,
|
||||
0x34, 0x30, 0x0b, 0xe0, 0x9c, 0x72, 0xc0, 0xd0, 0xe6, 0x1d, 0x0f, 0xec, 0xa8, 0x42, 0xec, 0x27, 0x8d, 0x98, 0xd9,
|
||||
0x60, 0xed, 0x65, 0x49, 0x65, 0x5e, 0xa5, 0xf1, 0xbb, 0x1f, 0xaf, 0x6f, 0x8e, 0x1e, 0x77, 0xb0, 0x52, 0x9e, 0x98,
|
||||
0x0f, 0x2c, 0x6d, 0x21, 0x59, 0x4d, 0x1a, 0xa9, 0xc5, 0x3a, 0x2a, 0xce, 0x0e, 0x1e, 0xe9, 0x75, 0xbd, 0x33, 0xda,
|
||||
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};
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xdd, 0x58, 0x5b, 0x8f, 0xdb, 0x36, 0x16, 0x7e, 0xef,
|
||||
0xaf, 0xe0, 0x2a, 0x49, 0x2d, 0x37, 0x23, 0xea, 0x66, 0xf9, 0x2a, 0xa9, 0x48, 0xb2, 0x29, 0x5a, 0x20, 0x69, 0x03,
|
||||
0xcc, 0xb4, 0xfb, 0x10, 0x04, 0x18, 0x5a, 0xa2, 0x2c, 0x66, 0x24, 0x4a, 0x15, 0xe9, 0x5b, 0x0c, 0xef, 0x6f, 0xdf,
|
||||
0x43, 0x52, 0xf6, 0x38, 0xb3, 0x99, 0x05, 0x52, 0xec, 0x62, 0xd1, 0x4e, 0x26, 0x1c, 0x92, 0x3a, 0xd7, 0x4f, 0x3c,
|
||||
0x17, 0x2a, 0xfe, 0x5b, 0xde, 0x64, 0x72, 0xdf, 0x52, 0x54, 0xca, 0xba, 0x4a, 0x63, 0x35, 0xa2, 0x8a, 0xf0, 0x55,
|
||||
0x42, 0x39, 0xac, 0x28, 0xc9, 0xd3, 0xb8, 0xa6, 0x92, 0xa0, 0xac, 0x24, 0x9d, 0xa0, 0x32, 0xf9, 0xf5, 0xe6, 0x07,
|
||||
0x67, 0x8a, 0xdc, 0x34, 0xae, 0x18, 0xbf, 0x43, 0x1d, 0xad, 0x12, 0x96, 0x35, 0x1c, 0x95, 0x1d, 0x2d, 0x92, 0x9c,
|
||||
0x48, 0x32, 0x67, 0x35, 0x59, 0x51, 0x45, 0xa0, 0xd9, 0x38, 0xa9, 0x69, 0xb2, 0x61, 0x74, 0xdb, 0x36, 0x9d, 0x44,
|
||||
0x40, 0x29, 0x29, 0x97, 0x89, 0xb5, 0x65, 0xb9, 0x2c, 0x93, 0x9c, 0x6e, 0x58, 0x46, 0x1d, 0xbd, 0xb8, 0x62, 0x9c,
|
||||
0x49, 0x46, 0x2a, 0x47, 0x64, 0xa4, 0xa2, 0x89, 0x7f, 0xb5, 0x16, 0xb4, 0xd3, 0x0b, 0xb2, 0x84, 0x35, 0x6f, 0x2c,
|
||||
0x10, 0x29, 0xb2, 0x8e, 0xb5, 0x12, 0x29, 0x7b, 0x93, 0xba, 0xc9, 0xd7, 0x15, 0x4d, 0x5d, 0x97, 0x08, 0xb0, 0x4b,
|
||||
0xb8, 0x8c, 0xe7, 0x74, 0x87, 0xa7, 0xb3, 0x68, 0x32, 0x9e, 0xe6, 0x13, 0xfc, 0x51, 0x7c, 0x03, 0x9e, 0xad, 0x6b,
|
||||
0x50, 0x87, 0xab, 0x26, 0x23, 0x92, 0x35, 0x1c, 0x0b, 0x4a, 0xba, 0xac, 0x4c, 0x92, 0xc4, 0xfa, 0x5e, 0x90, 0x0d,
|
||||
0xb5, 0xbe, 0xfd, 0xd6, 0x3e, 0x13, 0xad, 0xa8, 0x7c, 0x5d, 0x51, 0x35, 0x15, 0x2f, 0xf7, 0x37, 0x64, 0xf5, 0x33,
|
||||
0x58, 0x6e, 0x5b, 0x44, 0xb0, 0x9c, 0x5a, 0xc3, 0xf7, 0xde, 0x07, 0x2c, 0xe4, 0xbe, 0xa2, 0x38, 0x67, 0xa2, 0xad,
|
||||
0xc8, 0x3e, 0xb1, 0x96, 0x20, 0xf5, 0xce, 0x1a, 0x2e, 0x8a, 0x35, 0xcf, 0x94, 0x70, 0x24, 0x6c, 0x3a, 0x3c, 0x54,
|
||||
0x14, 0xcc, 0x4b, 0xde, 0x12, 0x59, 0xe2, 0x9a, 0xec, 0x6c, 0x33, 0x61, 0xdc, 0x0e, 0xbe, 0xb3, 0xe9, 0x73, 0xdf,
|
||||
0xf3, 0x86, 0x57, 0x7a, 0xf0, 0x86, 0x2e, 0xfc, 0x5d, 0x74, 0x54, 0xae, 0x3b, 0x8e, 0x88, 0x7d, 0x1b, 0xb7, 0x40,
|
||||
0x89, 0xf2, 0xc4, 0xaa, 0xfd, 0x00, 0x7b, 0xde, 0x14, 0xf9, 0x33, 0x1c, 0x44, 0x8e, 0xef, 0xe3, 0xd0, 0xf1, 0xa3,
|
||||
0x6c, 0xe2, 0x44, 0xc8, 0x1f, 0xc1, 0x10, 0x04, 0x38, 0x42, 0xde, 0x27, 0x0b, 0x15, 0xac, 0xaa, 0x12, 0x8b, 0x37,
|
||||
0x9c, 0x5a, 0x48, 0xc8, 0xae, 0xb9, 0xa3, 0x89, 0x95, 0xad, 0xbb, 0x0e, 0xec, 0x7f, 0xd5, 0x54, 0x4d, 0x07, 0x70,
|
||||
0x7d, 0x83, 0x3e, 0xfb, 0xf9, 0x6a, 0x15, 0xb2, 0x23, 0x5c, 0x14, 0x4d, 0x57, 0x27, 0x96, 0x7e, 0x29, 0xf6, 0xd3,
|
||||
0x83, 0x3c, 0x22, 0x35, 0x0c, 0x2f, 0x1e, 0x3a, 0x4d, 0xc7, 0x56, 0x8c, 0x27, 0x96, 0x1f, 0x20, 0x7f, 0x0a, 0x6a,
|
||||
0x6f, 0x87, 0xc7, 0x33, 0x26, 0x44, 0x61, 0xd2, 0x7b, 0xd9, 0xd8, 0xef, 0x6f, 0x63, 0xb1, 0x59, 0xa1, 0x5d, 0x5d,
|
||||
0x71, 0x91, 0x58, 0xa5, 0x94, 0xed, 0xdc, 0x75, 0xb7, 0xdb, 0x2d, 0xde, 0x86, 0xb8, 0xe9, 0x56, 0x6e, 0xe0, 0x79,
|
||||
0x9e, 0x0b, 0x14, 0x16, 0x32, 0xe7, 0xc3, 0x0a, 0x46, 0x16, 0x2a, 0x29, 0x5b, 0x95, 0x52, 0xcf, 0xd3, 0xa7, 0x07,
|
||||
0x7a, 0x8c, 0x15, 0x45, 0x7a, 0xfb, 0xe1, 0x42, 0x4b, 0x77, 0xa1, 0x85, 0x7e, 0x7f, 0x81, 0xe6, 0xe0, 0xad, 0x32,
|
||||
0x6a, 0x42, 0x02, 0x14, 0x20, 0x4f, 0xff, 0x0b, 0x1c, 0x35, 0xef, 0x57, 0xce, 0x83, 0x15, 0xba, 0x58, 0xc1, 0x5f,
|
||||
0xc0, 0x2f, 0xa8, 0xc7, 0xce, 0xec, 0xcc, 0xee, 0xab, 0xc7, 0x1b, 0xdf, 0xbb, 0xdf, 0x50, 0x3c, 0x3f, 0x8e, 0x2f,
|
||||
0xd7, 0x4e, 0xf0, 0x9b, 0x22, 0x50, 0xd8, 0x9f, 0x99, 0x9c, 0xa0, 0xf4, 0x7f, 0x1b, 0x93, 0x08, 0x45, 0xfd, 0x4e,
|
||||
0xe4, 0xa8, 0xf9, 0x79, 0xa5, 0x34, 0xa1, 0x68, 0x03, 0x54, 0xb5, 0x33, 0x76, 0x22, 0x12, 0xa2, 0xb0, 0x37, 0x09,
|
||||
0x66, 0xb0, 0x3d, 0x06, 0xe6, 0x8b, 0x3d, 0x27, 0xfc, 0x34, 0x50, 0x30, 0xcf, 0x2d, 0xeb, 0x1e, 0x83, 0xe6, 0x12,
|
||||
0x03, 0xfc, 0xb1, 0x81, 0x33, 0x67, 0x59, 0x80, 0x11, 0x95, 0x59, 0x69, 0x5b, 0x2e, 0x44, 0x5e, 0xc1, 0x56, 0x10,
|
||||
0x15, 0x0d, 0xb7, 0x86, 0x58, 0x96, 0x94, 0xdb, 0x27, 0x56, 0xc5, 0x48, 0xf5, 0x13, 0xfb, 0xe1, 0x13, 0x39, 0x3c,
|
||||
0x9c, 0xe3, 0x43, 0x32, 0x09, 0x71, 0x28, 0xb1, 0x8a, 0xe8, 0xab, 0xf3, 0xee, 0xb2, 0xc9, 0xf7, 0x8f, 0x84, 0x4e,
|
||||
0xe9, 0x9b, 0xb8, 0x61, 0x9c, 0xd3, 0xee, 0x86, 0xee, 0xe0, 0x1d, 0xfe, 0x83, 0xfd, 0xc0, 0xd0, 0xcf, 0x54, 0x6e,
|
||||
0x9b, 0xee, 0x4e, 0xcc, 0x91, 0xf5, 0xdc, 0x88, 0x5b, 0xa8, 0xa8, 0x61, 0x20, 0x9b, 0xb4, 0x02, 0x8b, 0x0a, 0x72,
|
||||
0x82, 0xed, 0x0f, 0x21, 0x7e, 0xda, 0x7b, 0x4b, 0xf8, 0xc9, 0xb9, 0xdb, 0x38, 0x67, 0x1b, 0x94, 0x55, 0x10, 0xf5,
|
||||
0x70, 0xfc, 0x8d, 0x28, 0x0b, 0xf5, 0x47, 0xbd, 0xe1, 0x19, 0x70, 0xdf, 0x25, 0xd6, 0x17, 0xa2, 0xfa, 0xe5, 0xfe,
|
||||
0xa7, 0xdc, 0x1e, 0x08, 0x88, 0xe7, 0xc1, 0x10, 0x6f, 0x48, 0xb5, 0xa6, 0x28, 0x41, 0xb2, 0x64, 0xe2, 0xde, 0xc0,
|
||||
0xc5, 0xa3, 0x6c, 0xad, 0xb8, 0x03, 0xae, 0x02, 0x1e, 0x0b, 0x7b, 0x68, 0x9d, 0x22, 0x2b, 0x26, 0x26, 0xef, 0x59,
|
||||
0x4f, 0xac, 0x07, 0x16, 0x39, 0x15, 0x2d, 0xa4, 0x75, 0x1f, 0x81, 0x4f, 0x0f, 0xc2, 0xe6, 0xb8, 0x03, 0xed, 0xc3,
|
||||
0xe3, 0x79, 0x33, 0x16, 0x2d, 0xe1, 0x0f, 0x19, 0x95, 0x81, 0xea, 0xa0, 0x43, 0xb2, 0x82, 0x99, 0x3a, 0xed, 0x40,
|
||||
0x74, 0x56, 0xe8, 0x92, 0xd3, 0xf4, 0xe9, 0xa1, 0x03, 0x89, 0x2a, 0x07, 0x9d, 0x25, 0xc6, 0x2e, 0x40, 0x93, 0xde,
|
||||
0x1e, 0x87, 0xf7, 0x7e, 0xfc, 0xbe, 0xa6, 0xdd, 0xfe, 0x9a, 0x56, 0x34, 0x93, 0x4d, 0x67, 0x5b, 0x4f, 0x40, 0x0b,
|
||||
0xbc, 0x7e, 0xed, 0xf0, 0x8f, 0x37, 0x6f, 0xdf, 0x24, 0x8d, 0xcd, 0x86, 0x57, 0x8f, 0x51, 0xab, 0x0c, 0xff, 0x1e,
|
||||
0x32, 0xfc, 0x3f, 0x93, 0x81, 0xca, 0xf1, 0x83, 0x0f, 0xc0, 0xaa, 0xfd, 0xbd, 0xbd, 0x4f, 0xf4, 0x2a, 0x18, 0x9f,
|
||||
0x43, 0x40, 0x5f, 0x29, 0x0f, 0x9d, 0x71, 0x34, 0x3c, 0x82, 0x7e, 0xb0, 0x00, 0xec, 0xd6, 0xb9, 0x1a, 0x72, 0xb6,
|
||||
0x4a, 0x9b, 0xe9, 0x77, 0x87, 0x65, 0xb3, 0x73, 0x04, 0xfb, 0xc4, 0xf8, 0x6a, 0xce, 0x78, 0x49, 0x3b, 0x26, 0x8f,
|
||||
0x60, 0x2e, 0xa4, 0xfd, 0x76, 0x2d, 0x0f, 0x2d, 0xc9, 0x73, 0xf5, 0x24, 0x6a, 0x77, 0x8b, 0x02, 0x8a, 0x84, 0xa2,
|
||||
0xa4, 0x73, 0x9f, 0xd6, 0x47, 0xf3, 0x5c, 0xe7, 0x83, 0xf9, 0x2c, 0x7a, 0x76, 0x54, 0x07, 0xee, 0x20, 0xe1, 0x65,
|
||||
0x39, 0xa4, 0x62, 0x2b, 0x3e, 0xcf, 0xc0, 0x70, 0xda, 0x19, 0xa6, 0x82, 0xd4, 0xac, 0xda, 0xcf, 0x05, 0x64, 0x26,
|
||||
0x07, 0xaa, 0x07, 0x2b, 0x8e, 0xcb, 0xb5, 0x94, 0x0d, 0x07, 0xdd, 0x5d, 0x4e, 0xbb, 0xb9, 0xb7, 0x30, 0x13, 0xa7,
|
||||
0x23, 0x39, 0x5b, 0x8b, 0x39, 0x0e, 0x3b, 0x5a, 0x2f, 0x96, 0x24, 0xbb, 0x5b, 0x75, 0xcd, 0x9a, 0xe7, 0x4e, 0xa6,
|
||||
0x32, 0xe7, 0xfc, 0x89, 0x5f, 0x90, 0x90, 0x66, 0x8b, 0x7e, 0x55, 0x14, 0xc5, 0x02, 0xa0, 0xa0, 0x8e, 0xc9, 0x44,
|
||||
0xf3, 0x00, 0x8f, 0x14, 0xdb, 0x85, 0x99, 0x38, 0x50, 0x1b, 0xc6, 0x46, 0x48, 0xeb, 0xcf, 0x16, 0x27, 0x77, 0xbc,
|
||||
0x05, 0xa4, 0x64, 0x01, 0x42, 0x5a, 0x88, 0x47, 0x30, 0xf3, 0x58, 0x13, 0xc6, 0x2f, 0xad, 0x57, 0xc7, 0x64, 0xd1,
|
||||
0x97, 0x14, 0x80, 0x45, 0xab, 0xd1, 0x85, 0x65, 0x01, 0x45, 0xc3, 0x14, 0xc6, 0x79, 0x30, 0xf6, 0xda, 0xdd, 0x11,
|
||||
0xf7, 0x07, 0xe4, 0x70, 0xa2, 0x2e, 0x2a, 0xba, 0x5b, 0x7c, 0x5c, 0x0b, 0xc9, 0x8a, 0xbd, 0xd3, 0x17, 0xd6, 0x39,
|
||||
0x1c, 0x16, 0x28, 0xa8, 0x4b, 0x20, 0xa5, 0x94, 0x2f, 0xb4, 0x0e, 0x87, 0x49, 0x5a, 0x8b, 0x1e, 0xa7, 0xb3, 0x18,
|
||||
0x7d, 0x40, 0x3f, 0x97, 0xf5, 0x9f, 0xa8, 0xd5, 0x59, 0x3c, 0xd4, 0xa4, 0x83, 0x44, 0xef, 0x2c, 0x1b, 0xc0, 0xb4,
|
||||
0x9e, 0x3b, 0x13, 0x78, 0x57, 0xfd, 0x96, 0x12, 0x06, 0x9e, 0x83, 0x99, 0xba, 0x5e, 0x9e, 0xf0, 0xf6, 0xdb, 0x1d,
|
||||
0x12, 0x4d, 0xc5, 0xf2, 0x9e, 0x4e, 0x93, 0x20, 0xef, 0x0c, 0x8f, 0x0f, 0xaf, 0x1b, 0xa9, 0xbd, 0x13, 0xd4, 0xa3,
|
||||
0x62, 0x4a, 0x7c, 0xef, 0x0b, 0x6f, 0x24, 0x2f, 0x8a, 0x60, 0x59, 0x9c, 0x91, 0x52, 0x65, 0xef, 0xc8, 0xfa, 0x53,
|
||||
0x11, 0x8c, 0x40, 0xc0, 0xe9, 0xdd, 0xc0, 0xfc, 0xc8, 0x74, 0x58, 0x1c, 0x2e, 0xa4, 0xe8, 0xa3, 0x3a, 0x5f, 0x77,
|
||||
0x95, 0x6d, 0x7d, 0xe1, 0xe8, 0x3e, 0x0b, 0x5f, 0xdd, 0x97, 0xa5, 0xc1, 0xe3, 0x65, 0x69, 0x80, 0x54, 0x23, 0xf3,
|
||||
0xb2, 0xd9, 0x25, 0x03, 0x5d, 0x20, 0x46, 0xf0, 0x3b, 0x78, 0x16, 0xbe, 0x06, 0xfe, 0xff, 0x4a, 0xbd, 0xf9, 0xc3,
|
||||
0xc5, 0xe6, 0x2b, 0x2a, 0xcd, 0x57, 0x56, 0x19, 0xe3, 0x9d, 0x72, 0x1e, 0x66, 0x50, 0x4e, 0x18, 0x16, 0x6c, 0xe5,
|
||||
0xff, 0x2f, 0xa0, 0xfd, 0x77, 0x1c, 0xc3, 0x17, 0xfe, 0x14, 0xcf, 0x90, 0x1e, 0x0c, 0x44, 0x38, 0x9c, 0xa2, 0xc9,
|
||||
0xab, 0x11, 0x1e, 0xf9, 0x48, 0xb5, 0x30, 0x63, 0x34, 0x81, 0x7e, 0x0f, 0xf9, 0x63, 0x1c, 0x4e, 0x60, 0x03, 0x05,
|
||||
0x3e, 0x8e, 0xde, 0x04, 0x21, 0x1e, 0x47, 0x40, 0x15, 0x78, 0x38, 0x0c, 0x90, 0xa1, 0x1d, 0xe3, 0x00, 0xc4, 0x29,
|
||||
0x92, 0xb0, 0x06, 0xa0, 0xb3, 0x10, 0x7b, 0x13, 0x10, 0x37, 0xc6, 0xde, 0x0c, 0x4f, 0xc7, 0x68, 0x8a, 0x27, 0x00,
|
||||
0x1d, 0x1e, 0x45, 0x95, 0x13, 0x61, 0x1f, 0xb6, 0xc3, 0x31, 0x99, 0xe2, 0x51, 0x88, 0xf4, 0x60, 0xe0, 0x98, 0x80,
|
||||
0x08, 0x07, 0x7b, 0xfe, 0x9b, 0x10, 0x07, 0x13, 0xd0, 0x3b, 0x1a, 0xbd, 0x00, 0xb1, 0xb3, 0x11, 0x32, 0xa3, 0x81,
|
||||
0x17, 0x14, 0x44, 0x8f, 0x81, 0x16, 0xfc, 0x75, 0x41, 0x03, 0x48, 0x7c, 0x14, 0xe2, 0x19, 0xc4, 0xae, 0xaf, 0xf8,
|
||||
0xcd, 0x68, 0x70, 0xf3, 0x7d, 0xe4, 0xfd, 0x61, 0xcc, 0xc2, 0xbf, 0x2e, 0x66, 0xbe, 0x42, 0x00, 0xa6, 0xa0, 0x1b,
|
||||
0xe4, 0x20, 0x3d, 0x18, 0xdd, 0xc0, 0x3c, 0x7d, 0x35, 0x43, 0x53, 0xe0, 0x1a, 0x4f, 0xd1, 0x0c, 0x45, 0x0a, 0x5d,
|
||||
0x60, 0x1f, 0x19, 0x26, 0x07, 0x98, 0xbe, 0x12, 0xc6, 0xd1, 0x9f, 0x18, 0xc6, 0xc7, 0x7c, 0xfa, 0x13, 0xbb, 0xf4,
|
||||
0xff, 0x48, 0x41, 0xd0, 0x8e, 0xe9, 0x36, 0x2c, 0x76, 0xcd, 0x95, 0x5e, 0x75, 0x51, 0x70, 0x43, 0x87, 0x6e, 0x04,
|
||||
0x2e, 0xf9, 0x3e, 0x62, 0x79, 0x52, 0xfa, 0xe9, 0x67, 0xdd, 0x39, 0x50, 0xfa, 0x69, 0xac, 0xcb, 0x79, 0x7a, 0x53,
|
||||
0x52, 0xf4, 0xfa, 0xfa, 0x1d, 0xdc, 0xca, 0xaa, 0x0a, 0xf1, 0x66, 0x0b, 0x97, 0xbf, 0x3d, 0x92, 0x8d, 0xba, 0xce,
|
||||
0x73, 0xe8, 0x15, 0xd5, 0x14, 0xee, 0x0d, 0xa8, 0x6f, 0x16, 0x30, 0xc6, 0xf1, 0xb2, 0x4b, 0xdf, 0x55, 0x94, 0x08,
|
||||
0x8a, 0x56, 0x6c, 0x43, 0x11, 0x93, 0xd0, 0x07, 0xd4, 0x14, 0x49, 0xa6, 0x86, 0x33, 0xa3, 0xa6, 0x83, 0x9e, 0x56,
|
||||
0x2b, 0x31, 0xdd, 0x30, 0x58, 0x02, 0x62, 0xd2, 0xbe, 0xed, 0x8d, 0xcb, 0xd0, 0x58, 0x75, 0x4d, 0xa5, 0x84, 0x8e,
|
||||
0x41, 0x59, 0x15, 0xa6, 0xb1, 0xba, 0x76, 0x22, 0xa2, 0x2f, 0x06, 0x89, 0xbb, 0x65, 0x05, 0x53, 0x97, 0xf9, 0x34,
|
||||
0xd6, 0xad, 0xa2, 0x92, 0xa0, 0xba, 0x15, 0xf3, 0xe5, 0x41, 0xcf, 0x2a, 0xca, 0x57, 0x70, 0x9b, 0x84, 0x77, 0x01,
|
||||
0xcd, 0x43, 0x46, 0xcb, 0xa6, 0x82, 0xe6, 0x24, 0xb9, 0xbe, 0xfe, 0xe9, 0xef, 0xea, 0x33, 0x85, 0x32, 0xe1, 0xcc,
|
||||
0x09, 0x7d, 0xbe, 0x61, 0x54, 0x93, 0x9e, 0x6f, 0x3c, 0x32, 0x1f, 0x1c, 0x5a, 0xe8, 0xd3, 0xc1, 0xbf, 0xfc, 0x33,
|
||||
0x29, 0xef, 0x4e, 0x9b, 0xbd, 0x24, 0xfd, 0x5f, 0x37, 0x9d, 0x86, 0x49, 0xac, 0x97, 0x35, 0x93, 0xe9, 0x35, 0x18,
|
||||
0x18, 0xbb, 0xe6, 0x01, 0x38, 0xa7, 0x1c, 0x30, 0xb4, 0x65, 0xcf, 0x03, 0x60, 0xff, 0x72, 0xf3, 0x02, 0xfd, 0xda,
|
||||
0xc2, 0x09, 0xa6, 0x06, 0x7b, 0xed, 0x65, 0x4d, 0x65, 0xd9, 0xe4, 0xc9, 0xbb, 0x5f, 0xae, 0x6f, 0xce, 0x1e, 0xaf,
|
||||
0x35, 0x11, 0xa2, 0x3c, 0x33, 0x1f, 0x42, 0xd6, 0x95, 0x64, 0x2d, 0xe9, 0xa4, 0x16, 0xeb, 0xa8, 0x10, 0x38, 0x79,
|
||||
0xa4, 0x9f, 0x17, 0xac, 0xa2, 0xc6, 0xa9, 0x9e, 0xd1, 0x4d, 0xd1, 0x97, 0x6c, 0x3c, 0xe9, 0x7e, 0x60, 0xa5, 0x6b,
|
||||
0x4e, 0x89, 0x6b, 0x8e, 0x8c, 0xab, 0x3f, 0x13, 0xfd, 0x0b, 0x65, 0x37, 0xa3, 0x8e, 0x36, 0x12, 0x00, 0x00};
|
||||
|
||||
} // namespace captive_portal
|
||||
} // namespace esphome
|
||||
|
|
|
@ -6,7 +6,7 @@ from esphome.const import (
|
|||
ICON_RADIATOR,
|
||||
ICON_RESTART,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PARTS_PER_BILLION,
|
||||
|
@ -43,7 +43,7 @@ CONFIG_SCHEMA = (
|
|||
unit_of_measurement=UNIT_PARTS_PER_BILLION,
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
|
||||
|
|
69
esphome/components/display/animation.cpp
Normal file
69
esphome/components/display/animation.cpp
Normal 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
|
67
esphome/components/display/animation.h
Normal file
67
esphome/components/display/animation.h
Normal 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
|
|
@ -12,115 +12,9 @@ namespace 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);
|
||||
|
||||
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) {
|
||||
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
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);
|
||||
}
|
||||
|
||||
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 width, height;
|
||||
this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height);
|
||||
|
||||
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;
|
||||
}
|
||||
font->print(x_start, y_start, this, color, text);
|
||||
}
|
||||
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];
|
||||
int ret = vsnprintf(buffer, sizeof(buffer), format, arg);
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -342,7 +227,7 @@ void DisplayBuffer::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_
|
|||
}
|
||||
#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 x_offset, baseline;
|
||||
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;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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_start(arg, format);
|
||||
this->vprintf_(x, y, font, color, align, format, 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_start(arg, format);
|
||||
this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, 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_start(arg, format);
|
||||
this->vprintf_(x, y, font, COLOR_ON, align, format, 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_start(arg, format);
|
||||
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))
|
||||
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];
|
||||
size_t ret = time.strftime(buffer, sizeof(buffer), format);
|
||||
if (ret > 0)
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -505,286 +391,6 @@ Rect DisplayBuffer::get_clipping() {
|
|||
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)) {}
|
||||
void DisplayPage::show() { this->parent_->show_page(this); }
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <cstdarg>
|
||||
#include <vector>
|
||||
#include "rect.h"
|
||||
#include "display_color_utils.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
@ -70,17 +71,52 @@ enum class TextAlign {
|
|||
BOTTOM_RIGHT = BOTTOM | RIGHT,
|
||||
};
|
||||
|
||||
/// Turn the pixel OFF.
|
||||
extern const Color COLOR_OFF;
|
||||
/// Turn the pixel ON.
|
||||
extern const Color COLOR_ON;
|
||||
/** ImageAlign is used to tell the display class how to position a image. By default
|
||||
* the coordinates you enter for the image() functions take the upper left corner of the image
|
||||
* as the "anchor" point. You can customize this behavior to, for example, make the coordinates
|
||||
* 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 {
|
||||
IMAGE_TYPE_BINARY = 0,
|
||||
IMAGE_TYPE_GRAYSCALE = 1,
|
||||
IMAGE_TYPE_RGB24 = 2,
|
||||
IMAGE_TYPE_RGB565 = 3,
|
||||
IMAGE_TYPE_RGBA = 4,
|
||||
LEFT = 0x00,
|
||||
CENTER_HORIZONTAL = 0x04,
|
||||
RIGHT = 0x08,
|
||||
|
||||
TOP_LEFT = TOP | LEFT,
|
||||
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 {
|
||||
|
@ -96,35 +132,6 @@ enum DisplayRotation {
|
|||
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 DisplayPage;
|
||||
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()); \
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
public:
|
||||
/// Fill the entire screen with the given color.
|
||||
|
@ -184,7 +209,7 @@ class DisplayBuffer {
|
|||
* @param align The alignment of the text.
|
||||
* @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`.
|
||||
*
|
||||
|
@ -194,7 +219,7 @@ class DisplayBuffer {
|
|||
* @param color The color to draw the text with.
|
||||
* @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`.
|
||||
*
|
||||
|
@ -204,7 +229,7 @@ class DisplayBuffer {
|
|||
* @param align The alignment of the text.
|
||||
* @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`.
|
||||
*
|
||||
|
@ -213,7 +238,7 @@ class DisplayBuffer {
|
|||
* @param font The font to draw the text with.
|
||||
* @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`.
|
||||
*
|
||||
|
@ -225,7 +250,7 @@ class DisplayBuffer {
|
|||
* @param format The format to use.
|
||||
* @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)));
|
||||
|
||||
/** 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 ... 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`.
|
||||
*
|
||||
|
@ -248,7 +273,8 @@ class DisplayBuffer {
|
|||
* @param format The format to use.
|
||||
* @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`.
|
||||
*
|
||||
|
@ -258,7 +284,7 @@ class DisplayBuffer {
|
|||
* @param format The format to use.
|
||||
* @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`.
|
||||
*
|
||||
|
@ -270,7 +296,7 @@ class DisplayBuffer {
|
|||
* @param format The strftime format to use.
|
||||
* @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)));
|
||||
|
||||
/** 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 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)));
|
||||
|
||||
/** 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 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)));
|
||||
|
||||
/** 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 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.
|
||||
*
|
||||
* @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 image The image to draw.
|
||||
* @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, 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
|
||||
/** 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 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);
|
||||
|
||||
/// Internal method to set the display writer lambda.
|
||||
|
@ -439,7 +476,7 @@ class DisplayBuffer {
|
|||
bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
|
||||
|
||||
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;
|
||||
|
||||
|
@ -475,123 +512,6 @@ class DisplayPage {
|
|||
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...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(DisplayPage *, page)
|
||||
|
|
147
esphome/components/display/font.cpp
Normal file
147
esphome/components/display/font.cpp
Normal 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
|
67
esphome/components/display/font.h
Normal file
67
esphome/components/display/font.h
Normal 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
|
134
esphome/components/display/image.cpp
Normal file
134
esphome/components/display/image.cpp
Normal 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
|
62
esphome/components/display/image.h
Normal file
62
esphome/components/display/image.h
Normal 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
|
98
esphome/components/display/rect.cpp
Normal file
98
esphome/components/display/rect.cpp
Normal 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
|
36
esphome/components/display/rect.h
Normal file
36
esphome/components/display/rect.h
Normal 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
|
|
@ -1,7 +1,7 @@
|
|||
#include "ethernet_info_text_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ethernet_info {
|
||||
|
@ -13,4 +13,4 @@ void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IP
|
|||
} // namespace ethernet_info
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#endif // USE_ESP32
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#include "esphome/components/ethernet/ethernet_component.h"
|
||||
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ethernet_info {
|
||||
|
@ -30,4 +30,4 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS
|
|||
} // namespace ethernet_info
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#endif // USE_ESP32
|
||||
|
|
|
@ -151,11 +151,13 @@ void FujitsuGeneralClimate::transmit_state() {
|
|||
case climate::CLIMATE_FAN_LOW:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW);
|
||||
break;
|
||||
case climate::CLIMATE_FAN_QUIET:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_SILENT);
|
||||
break;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO);
|
||||
break;
|
||||
// TODO Quiet / Silent
|
||||
}
|
||||
|
||||
// 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);
|
||||
ESP_LOGV(TAG, "Received fan mode %X", recv_fan_mode);
|
||||
switch (recv_fan_mode) {
|
||||
// TODO No Quiet / Silent in ESPH
|
||||
case FUJITSU_GENERAL_FAN_SILENT:
|
||||
this->fan_mode = climate::CLIMATE_FAN_QUIET;
|
||||
break;
|
||||
case FUJITSU_GENERAL_FAN_LOW:
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
break;
|
||||
|
|
|
@ -52,7 +52,7 @@ class FujitsuGeneralClimate : public climate_ir::ClimateIR {
|
|||
FujitsuGeneralClimate()
|
||||
: 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_HIGH},
|
||||
climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_QUIET},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_BOTH}) {}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include "graph.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/display/font.h"
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
|
|
@ -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_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() {
|
||||
// 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->last_send_ = millis();
|
||||
}
|
||||
|
||||
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 {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
|
|
|
@ -19,6 +19,7 @@ enum GrowattProtocolVersion {
|
|||
|
||||
class GrowattSolar : public PollingComponent, public modbus::ModbusDevice {
|
||||
public:
|
||||
void loop() override;
|
||||
void update() override;
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||
void dump_config() override;
|
||||
|
@ -55,6 +56,9 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice {
|
|||
}
|
||||
|
||||
protected:
|
||||
bool waiting_to_update_;
|
||||
uint32_t last_send_;
|
||||
|
||||
struct GrowattPhase {
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
CODEOWNERS = ["@Yarikx"]
|
130
esphome/components/haier/automation.h
Normal file
130
esphome/components/haier/automation.h
Normal 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
|
|
@ -1,43 +1,364 @@
|
|||
from esphome.components import climate
|
||||
import logging
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import uart
|
||||
from esphome.components.climate import ClimateSwingMode
|
||||
from esphome.const import CONF_ID, CONF_SUPPORTED_SWING_MODES
|
||||
import esphome.final_validate as fv
|
||||
from esphome.components import uart, sensor, climate, logger
|
||||
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")
|
||||
HaierClimate = haier_ns.class_(
|
||||
"HaierClimate", climate.Climate, cg.PollingComponent, uart.UARTDevice
|
||||
HaierClimateBase = haier_ns.class_(
|
||||
"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,
|
||||
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
|
||||
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
|
||||
|
||||
AirflowVerticalDirection = haier_ns.enum("AirflowVerticalDirection")
|
||||
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = {
|
||||
"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(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HaierClimate),
|
||||
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
|
||||
validate_swing_modes
|
||||
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
|
||||
cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True)
|
||||
),
|
||||
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):
|
||||
cg.add(haier_ns.init_haier_protocol_logging())
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await climate.register_climate(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:
|
||||
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")
|
||||
|
|
|
@ -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
|
|
@ -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
|
311
esphome/components/haier/haier_base.cpp
Normal file
311
esphome/components/haier/haier_base.cpp
Normal 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
|
142
esphome/components/haier/haier_base.h
Normal file
142
esphome/components/haier/haier_base.h
Normal 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
|
857
esphome/components/haier/hon_climate.cpp
Normal file
857
esphome/components/haier/hon_climate.cpp
Normal 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
|
95
esphome/components/haier/hon_climate.h
Normal file
95
esphome/components/haier/hon_climate.h
Normal 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
|
228
esphome/components/haier/hon_packet.h
Normal file
228
esphome/components/haier/hon_packet.h
Normal 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
|
33
esphome/components/haier/logger_handler.cpp
Normal file
33
esphome/components/haier/logger_handler.cpp
Normal 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
|
14
esphome/components/haier/logger_handler.h
Normal file
14
esphome/components/haier/logger_handler.h
Normal 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
|
457
esphome/components/haier/smartair2_climate.cpp
Normal file
457
esphome/components/haier/smartair2_climate.cpp
Normal 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
|
31
esphome/components/haier/smartair2_climate.h
Normal file
31
esphome/components/haier/smartair2_climate.h
Normal 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
|
97
esphome/components/haier/smartair2_packet.h
Normal file
97
esphome/components/haier/smartair2_packet.h
Normal 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
|
|
@ -14,6 +14,14 @@ ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t 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) {
|
||||
WriteBuffer buffers[2];
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (read_register(a_register, reinterpret_cast<uint8_t *>(data), len * 2) != ERROR_OK)
|
||||
return false;
|
||||
|
@ -60,5 +78,26 @@ uint8_t I2CRegister::get() const {
|
|||
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 esphome
|
||||
|
|
|
@ -31,6 +31,25 @@ class I2CRegister {
|
|||
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.
|
||||
// ("i2c" byte order is big-endian)
|
||||
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; }
|
||||
|
||||
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_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_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
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ MODELS = {
|
|||
"ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI),
|
||||
"ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI),
|
||||
"ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI),
|
||||
"S3BOX": ili9XXX_ns.class_("ILI9XXXS3Box", ili9XXXSPI),
|
||||
"S3BOX_LITE": ili9XXX_ns.class_("ILI9XXXS3BoxLite", ili9XXXSPI),
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
void ILI9XXXS3BoxLite::initialize() {
|
||||
this->init_lcd_(INITCMD_S3BOXLITE);
|
||||
|
|
|
@ -134,6 +134,11 @@ class ILI9XXXST7796 : public ILI9XXXDisplay {
|
|||
void initialize() override;
|
||||
};
|
||||
|
||||
class ILI9XXXS3Box : public ILI9XXXDisplay {
|
||||
protected:
|
||||
void initialize() override;
|
||||
};
|
||||
|
||||
class ILI9XXXS3BoxLite : public ILI9XXXDisplay {
|
||||
protected:
|
||||
void initialize() override;
|
||||
|
|
|
@ -169,6 +169,36 @@ static const uint8_t PROGMEM INITCMD_ST7796[] = {
|
|||
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[] = {
|
||||
0xEF, 3, 0x03, 0x80, 0x02,
|
||||
0xCF, 3, 0x00, 0xC1, 0x30,
|
||||
|
|
|
@ -11,6 +11,7 @@ namespace mqtt {
|
|||
static const char *const TAG = "mqtt.idf";
|
||||
|
||||
bool MQTTBackendIDF::initialize_() {
|
||||
#if ESP_IDF_VERSION_MAJOR < 5
|
||||
mqtt_cfg_.user_context = (void *) this;
|
||||
mqtt_cfg_.buffer_size = MQTT_BUFFER_SIZE;
|
||||
|
||||
|
@ -47,6 +48,41 @@ bool MQTTBackendIDF::initialize_() {
|
|||
} else {
|
||||
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_);
|
||||
if (mqtt_client) {
|
||||
handler_.reset(mqtt_client);
|
||||
|
@ -78,9 +114,8 @@ void MQTTBackendIDF::mqtt_event_handler_(const Event &event) {
|
|||
|
||||
case MQTT_EVENT_CONNECTED:
|
||||
ESP_LOGV(TAG, "MQTT_EVENT_CONNECTED");
|
||||
// TODO session present check
|
||||
this->is_connected_ = true;
|
||||
this->on_connect_.call(!mqtt_cfg_.disable_clean_session);
|
||||
this->on_connect_.call(event.session_present);
|
||||
break;
|
||||
case MQTT_EVENT_DISCONNECTED:
|
||||
ESP_LOGV(TAG, "MQTT_EVENT_DISCONNECTED");
|
||||
|
|
|
@ -22,6 +22,7 @@ struct Event {
|
|||
bool retain;
|
||||
int qos;
|
||||
bool dup;
|
||||
bool session_present;
|
||||
esp_mqtt_error_codes_t error_handle;
|
||||
|
||||
// Construct from esp_mqtt_event_t
|
||||
|
@ -36,6 +37,7 @@ struct Event {
|
|||
retain(event.retain),
|
||||
qos(event.qos),
|
||||
dup(event.dup),
|
||||
session_present(event.session_present),
|
||||
error_handle(*event.error_handle) {}
|
||||
};
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ bool MQTTComponent::send_discovery_() {
|
|||
} else {
|
||||
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
|
||||
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
|
||||
root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash;
|
||||
} else {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#include <cinttypes>
|
||||
#include "mqtt_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
|
@ -26,7 +27,7 @@ void MQTTSensorComponent::setup() {
|
|||
void MQTTSensorComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Sensor '%s':", this->sensor_->get_name().c_str());
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -29,13 +29,10 @@ void PCA9685Output::setup() {
|
|||
ESP_LOGCONFIG(TAG, "Setting up PCA9685OutputComponent...");
|
||||
|
||||
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)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->set_i2c_address(address_tmp);
|
||||
|
||||
if (!this->write_byte(PCA9685_REGISTER_MODE1, PCA9685_MODE1_RESTART | PCA9685_MODE1_AUTOINC)) {
|
||||
this->mark_failed();
|
||||
|
|
|
@ -16,8 +16,7 @@ from esphome.const import (
|
|||
KEY_TARGET_PLATFORM,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority, EsphomeError
|
||||
from esphome.helpers import mkdir_p, write_file
|
||||
import esphome.platformio_api as api
|
||||
from esphome.helpers import mkdir_p, write_file, copy_file_if_changed
|
||||
|
||||
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")
|
||||
mkdir_p(os.path.dirname(pio_path))
|
||||
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")
|
||||
if retval != 0:
|
||||
raise EsphomeError("PIO assembly failed")
|
||||
|
||||
write_file(
|
||||
CORE.relative_build_path("src/pio_includes.h"),
|
||||
"#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
|
||||
|
||||
|
||||
|
|
47
esphome/components/rp2040/build_pio.py.script
Normal file
47
esphome/components/rp2040/build_pio.py.script
Normal 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("==============================================")
|
40
esphome/components/rp2040_pio/__init__.py
Normal file
40
esphome/components/rp2040_pio/__init__.py
Normal 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"])
|
|
@ -127,6 +127,7 @@ def time_to_cycles(time_us):
|
|||
|
||||
CONF_PIO = "pio"
|
||||
|
||||
AUTO_LOAD = ["rp2040_pio"]
|
||||
CODEOWNERS = ["@Papa-DMan"]
|
||||
DEPENDENCIES = ["rp2040"]
|
||||
|
||||
|
@ -265,9 +266,3 @@ async def to_code(config):
|
|||
time_to_cycles(config[CONF_BIT1_LOW]),
|
||||
),
|
||||
)
|
||||
cg.add_platformio_option(
|
||||
"platform_packages",
|
||||
[
|
||||
"earlephilhower/tool-pioasm-rp2040-earlephilhower",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -11,7 +11,7 @@ from esphome.const import (
|
|||
CONF_TVOC,
|
||||
ICON_RADIATOR,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PARTS_PER_BILLION,
|
||||
|
@ -49,7 +49,7 @@ CONFIG_SCHEMA = (
|
|||
unit_of_measurement=UNIT_PARTS_PER_BILLION,
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema(
|
||||
|
|
|
@ -87,6 +87,30 @@ void SPIComponent::setup() {
|
|||
return;
|
||||
}
|
||||
#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
|
||||
|
||||
if (this->miso_ != nullptr) {
|
||||
|
|
|
@ -14,6 +14,7 @@ from esphome.const import (
|
|||
CONF_PASSWORD,
|
||||
CONF_INCLUDE_INTERNAL,
|
||||
CONF_OTA,
|
||||
CONF_LOG,
|
||||
CONF_VERSION,
|
||||
CONF_LOCAL,
|
||||
)
|
||||
|
@ -71,6 +72,7 @@ CONFIG_SCHEMA = cv.All(
|
|||
),
|
||||
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
|
||||
cv.Optional(CONF_OTA, default=True): cv.boolean,
|
||||
cv.Optional(CONF_LOG, default=True): cv.boolean,
|
||||
cv.Optional(CONF_LOCAL): cv.boolean,
|
||||
}
|
||||
).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)
|
||||
async def to_code(config):
|
||||
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)
|
||||
|
||||
cg.add_define("USE_WEBSERVER")
|
||||
version = config[CONF_VERSION]
|
||||
|
||||
cg.add(paren.set_port(config[CONF_PORT]))
|
||||
cg.add_define("USE_WEBSERVER")
|
||||
cg.add_define("USE_WEBSERVER_PORT", config[CONF_PORT])
|
||||
cg.add_define("USE_WEBSERVER_VERSION", config[CONF_VERSION])
|
||||
cg.add(var.set_css_url(config[CONF_CSS_URL]))
|
||||
cg.add(var.set_js_url(config[CONF_JS_URL]))
|
||||
cg.add_define("USE_WEBSERVER_VERSION", version)
|
||||
if version == 2:
|
||||
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_expose_log(config[CONF_LOG]))
|
||||
if CONF_AUTH in config:
|
||||
cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME]))
|
||||
cg.add(paren.set_auth_password(config[CONF_AUTH][CONF_PASSWORD]))
|
||||
if CONF_CSS_INCLUDE in config:
|
||||
cg.add_define("USE_WEBSERVER_CSS_INCLUDE")
|
||||
path = CORE.relative_config_path(config[CONF_CSS_INCLUDE])
|
||||
with open(file=path, encoding="utf-8") as myfile:
|
||||
cg.add(var.set_css_include(myfile.read()))
|
||||
with open(file=path, encoding="utf-8") as css_file:
|
||||
add_resource_as_progmem("CSS_INCLUDE", css_file.read())
|
||||
if CONF_JS_INCLUDE in config:
|
||||
cg.add_define("USE_WEBSERVER_JS_INCLUDE")
|
||||
path = CORE.relative_config_path(config[CONF_JS_INCLUDE])
|
||||
with open(file=path, encoding="utf-8") as myfile:
|
||||
cg.add(var.set_js_include(myfile.read()))
|
||||
with open(file=path, encoding="utf-8") as js_file:
|
||||
add_resource_as_progmem("JS_INCLUDE", js_file.read())
|
||||
cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL]))
|
||||
if CONF_LOCAL in config and config[CONF_LOCAL]:
|
||||
cg.add_define("USE_WEBSERVER_LOCAL")
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -90,10 +90,27 @@ WebServer::WebServer(web_server_base::WebServerBase *base)
|
|||
#endif
|
||||
}
|
||||
|
||||
#if USE_WEBSERVER_VERSION == 1
|
||||
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; }
|
||||
#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; }
|
||||
#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() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up web server...");
|
||||
|
@ -102,20 +119,13 @@ void WebServer::setup() {
|
|||
|
||||
this->events_.onConnect([this](AsyncEventSourceClient *client) {
|
||||
// Configure reconnect timeout and send config
|
||||
|
||||
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);
|
||||
client->send(this->get_config_json().c_str(), "ping", millis(), 30000);
|
||||
|
||||
this->entities_iterator_.begin(this->include_internal_);
|
||||
});
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
if (logger::global_logger != nullptr) {
|
||||
if (logger::global_logger != nullptr && this->expose_log_) {
|
||||
logger::global_logger->add_on_log_callback(
|
||||
[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");
|
||||
request->send(response);
|
||||
}
|
||||
#else
|
||||
#elif USE_WEBSERVER_VERSION == 1
|
||||
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
||||
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();
|
||||
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>"));
|
||||
stream->print(title.c_str());
|
||||
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
|
||||
stream->print(F("<link rel=\"stylesheet\" href=\"/0.css\">"));
|
||||
#endif
|
||||
|
@ -182,7 +186,6 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
|||
stream->print(F("\">"));
|
||||
}
|
||||
stream->print(F("</head><body>"));
|
||||
#if USE_WEBSERVER_VERSION == 1
|
||||
stream->print(F("<article class=\"markdown-body\"><h1>"));
|
||||
stream->print(title.c_str());
|
||||
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>"));
|
||||
}
|
||||
stream->print(F("<h2>Debug Log</h2><pre id=\"log\"></pre>"));
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER_JS_INCLUDE
|
||||
if (this->js_include_ != nullptr) {
|
||||
stream->print(F("<script type=\"module\" src=\"/0.js\"></script>"));
|
||||
}
|
||||
#endif
|
||||
#if USE_WEBSERVER_VERSION == 2
|
||||
stream->print(F("<esp-app></esp-app>"));
|
||||
#endif
|
||||
if (strlen(this->js_url_) > 0) {
|
||||
stream->print(F("<script src=\""));
|
||||
stream->print(this->js_url_);
|
||||
stream->print(F("\"></script>"));
|
||||
}
|
||||
#if USE_WEBSERVER_VERSION == 1
|
||||
stream->print(F("</article></body></html>"));
|
||||
#else
|
||||
stream->print(F("</body></html>"));
|
||||
#endif
|
||||
|
||||
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
|
||||
|
||||
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
||||
void WebServer::handle_css_request(AsyncWebServerRequest *request) {
|
||||
AsyncResponseStream *stream = request->beginResponseStream("text/css");
|
||||
if (this->css_include_ != nullptr) {
|
||||
stream->print(this->css_include_);
|
||||
}
|
||||
|
||||
request->send(stream);
|
||||
AsyncWebServerResponse *response =
|
||||
request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE);
|
||||
request->send(response);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_WEBSERVER_JS_INCLUDE
|
||||
void WebServer::handle_js_request(AsyncWebServerRequest *request) {
|
||||
AsyncResponseStream *stream = request->beginResponseStream("text/javascript");
|
||||
if (this->js_include_ != nullptr) {
|
||||
stream->addHeader("Access-Control-Allow-Origin", "*");
|
||||
stream->print(this->js_include_);
|
||||
}
|
||||
|
||||
request->send(stream);
|
||||
AsyncWebServerResponse *response =
|
||||
request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE);
|
||||
request->send(response);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
@ -14,6 +14,22 @@
|
|||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#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 web_server {
|
||||
|
||||
|
@ -40,6 +56,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
|
|||
public:
|
||||
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
|
||||
* 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);
|
||||
|
||||
/** 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
|
||||
* https://esphome.io/_static/webserver-v1.min.js
|
||||
*
|
||||
* @param js_url The url to the web server script.
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param js_include Local path to web server script.
|
||||
*/
|
||||
void set_js_include(const char *js_include);
|
||||
#endif
|
||||
|
||||
/** Determine whether internal components should be displayed on the web server.
|
||||
* Defaults to false.
|
||||
|
@ -77,6 +99,11 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
|
|||
* @param 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 ==========
|
||||
// (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 '/'.
|
||||
void handle_index_request(AsyncWebServerRequest *request);
|
||||
|
||||
/// Return the webserver configuration as JSON.
|
||||
std::string get_config_json();
|
||||
|
||||
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
||||
/// Handle included css request under '/0.css'.
|
||||
void handle_css_request(AsyncWebServerRequest *request);
|
||||
|
@ -240,12 +270,19 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
|
|||
web_server_base::WebServerBase *base_;
|
||||
AsyncEventSource events_{"/events"};
|
||||
ListEntitiesIterator entities_iterator_;
|
||||
#if USE_WEBSERVER_VERSION == 1
|
||||
const char *css_url_{nullptr};
|
||||
const char *css_include_{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};
|
||||
#endif
|
||||
bool include_internal_{false};
|
||||
bool allow_ota_{true};
|
||||
bool expose_log_{true};
|
||||
#ifdef USE_ESP32
|
||||
std::deque<std::function<void()>> to_schedule_;
|
||||
SemaphoreHandle_t to_schedule_lock_;
|
||||
|
|
|
@ -83,6 +83,7 @@ class WebServerBase : public Component {
|
|||
return;
|
||||
}
|
||||
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", "*");
|
||||
this->server_->begin();
|
||||
|
||||
|
|
|
@ -58,7 +58,9 @@ struct IDFWiFiEvent {
|
|||
wifi_event_ap_probe_req_rx_t ap_probe_req_rx;
|
||||
wifi_event_bss_rssi_low_t bss_rssi_low;
|
||||
ip_event_got_ip_t ip_got_ip;
|
||||
#if LWIP_IPV6
|
||||
ip_event_got_ip6_t ip_got_ip6;
|
||||
#endif
|
||||
ip_event_ap_staipassigned_t ip_ap_staipassigned;
|
||||
} 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));
|
||||
} 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));
|
||||
#if LWIP_IPV6
|
||||
} 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));
|
||||
#endif
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { // NOLINT(bugprone-branch-clone)
|
||||
// no data
|
||||
} 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)); }
|
||||
#if LWIP_IPV6
|
||||
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) {
|
||||
switch (reason) {
|
||||
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());
|
||||
s_sta_got_ip = true;
|
||||
|
||||
#if LWIP_IPV6
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_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());
|
||||
#endif
|
||||
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) {
|
||||
ESP_LOGV(TAG, "Event: Lost IP");
|
||||
|
|
73
esphome/components/xl9535/__init__.py
Normal file
73
esphome/components/xl9535/__init__.py
Normal 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
|
122
esphome/components/xl9535/xl9535.cpp
Normal file
122
esphome/components/xl9535/xl9535.cpp
Normal 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, ®ister_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, ®ister_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
|
54
esphome/components/xl9535/xl9535.h
Normal file
54
esphome/components/xl9535/xl9535.h
Normal 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
|
|
@ -368,6 +368,7 @@ CONF_LINE_TYPE = "line_type"
|
|||
CONF_LOADED_INTEGRATIONS = "loaded_integrations"
|
||||
CONF_LOCAL = "local"
|
||||
CONF_LOCK_ACTION = "lock_action"
|
||||
CONF_LOG = "log"
|
||||
CONF_LOG_TOPIC = "log_topic"
|
||||
CONF_LOGGER = "logger"
|
||||
CONF_LOGS = "logs"
|
||||
|
|
|
@ -99,7 +99,7 @@ void Application::loop() {
|
|||
|
||||
if (this->dump_config_at_ < this->components_.size()) {
|
||||
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
|
||||
ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION);
|
||||
#endif
|
||||
|
|
|
@ -56,7 +56,7 @@ namespace esphome {
|
|||
|
||||
class Application {
|
||||
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) {
|
||||
arch_init();
|
||||
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().
|
||||
const std::string &get_friendly_name() const { return this->friendly_name_; }
|
||||
/// 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_; }
|
||||
|
||||
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.
|
||||
* If the loop() method takes longer than the target interval, ESPHome won't
|
||||
|
@ -376,8 +376,8 @@ class Application {
|
|||
|
||||
std::string name_;
|
||||
std::string friendly_name_;
|
||||
std::string comment_;
|
||||
std::string compilation_time_;
|
||||
const char *comment_{nullptr};
|
||||
const char *compilation_time_{nullptr};
|
||||
bool name_add_mac_suffix_;
|
||||
uint32_t last_loop_{0};
|
||||
uint32_t loop_interval_{16};
|
||||
|
|
|
@ -2,6 +2,16 @@
|
|||
|
||||
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) {
|
||||
struct tm c_tm = this->to_c_tm();
|
||||
return ::strftime(buffer, buffer_len, format, &c_tm);
|
||||
|
@ -158,14 +168,4 @@ template<typename T> bool increment_time_value(T ¤t, uint16_t begin, uint1
|
|||
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
|
||||
|
|
|
@ -8,10 +8,6 @@ namespace esphome {
|
|||
|
||||
template<typename T> bool increment_time_value(T ¤t, 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
|
||||
struct ESPTime {
|
||||
/** seconds after the minute [0-60]
|
||||
|
|
|
@ -3,6 +3,7 @@ import binascii
|
|||
import codecs
|
||||
import collections
|
||||
import functools
|
||||
import gzip
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
|
@ -485,6 +486,7 @@ class DownloadBinaryRequestHandler(BaseHandler):
|
|||
@bind_config
|
||||
def get(self, configuration=None):
|
||||
type = self.get_argument("type", "firmware.bin")
|
||||
compressed = self.get_argument("compressed", "0") == "1"
|
||||
|
||||
storage_path = ext_storage_path(settings.config_dir, configuration)
|
||||
storage_json = StorageJSON.load(storage_path)
|
||||
|
@ -534,6 +536,8 @@ class DownloadBinaryRequestHandler(BaseHandler):
|
|||
self.send_error(404)
|
||||
return
|
||||
|
||||
filename = filename + ".gz" if compressed else filename
|
||||
|
||||
self.set_header("Content-Type", "application/octet-stream")
|
||||
self.set_header("Content-Disposition", f'attachment; filename="{filename}"')
|
||||
self.set_header("Cache-Control", "no-cache")
|
||||
|
@ -543,9 +547,20 @@ class DownloadBinaryRequestHandler(BaseHandler):
|
|||
|
||||
with open(path, "rb") as f:
|
||||
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:
|
||||
break
|
||||
|
||||
if compressed:
|
||||
data = gzip.compress(data, 9)
|
||||
|
||||
self.write(data)
|
||||
self.finish()
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ lib_deps =
|
|||
bblanchon/ArduinoJson@6.18.5 ; json
|
||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
|
||||
pavlodn/HaierProtocol@0.9.18 ; haier
|
||||
; This is using the repository until a new release is published to PlatformIO
|
||||
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
|
||||
build_flags =
|
||||
|
|
|
@ -9,9 +9,9 @@ pyserial==3.5
|
|||
platformio==6.1.7 # When updating platformio, also update Dockerfile
|
||||
esptool==4.6
|
||||
click==8.1.3
|
||||
esphome-dashboard==20230516.0
|
||||
aioesphomeapi==14.0.0
|
||||
zeroconf==0.63.0
|
||||
esphome-dashboard==20230621.0
|
||||
aioesphomeapi==14.1.0
|
||||
zeroconf==0.69.0
|
||||
|
||||
# esp-idf requires this, but doesn't bundle it by default
|
||||
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24
|
||||
|
|
|
@ -5,7 +5,7 @@ pyupgrade==3.4.0 # also change in .pre-commit-config.yaml when updating
|
|||
pre-commit
|
||||
|
||||
# Unit tests
|
||||
pytest==7.3.1
|
||||
pytest==7.3.2
|
||||
pytest-cov==4.1.0
|
||||
pytest-mock==3.10.0
|
||||
pytest-asyncio==0.21.0
|
||||
|
|
|
@ -5,12 +5,13 @@ set -e
|
|||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ ! -n "$DEVCONTAINER" ] && [ ! -n "$VIRTUAL_ENV" ]; then
|
||||
if [ ! -n "$DEVCONTAINER" ] && [ ! -n "$VIRTUAL_ENV" ] && [ ! "$ESPHOME_NO_VENV" ]; then
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
|
||||
pip3 install setuptools wheel
|
||||
pip3 install --no-use-pep517 -e .
|
||||
|
||||
pre-commit install
|
||||
|
|
|
@ -312,7 +312,8 @@ sensor:
|
|||
id: freezer_temp_source
|
||||
reference_voltage: 3.19
|
||||
number: 0
|
||||
- platform: airthings_wave_plus
|
||||
- id: airthingswp
|
||||
platform: airthings_wave_plus
|
||||
ble_client_id: airthings01
|
||||
update_interval: 5min
|
||||
temperature:
|
||||
|
@ -329,7 +330,8 @@ sensor:
|
|||
name: Wave Plus CO2
|
||||
tvoc:
|
||||
name: Wave Plus VOC
|
||||
- platform: airthings_wave_mini
|
||||
- id: airthingswm
|
||||
platform: airthings_wave_mini
|
||||
ble_client_id: airthingsmini01
|
||||
update_interval: 5min
|
||||
temperature:
|
||||
|
@ -695,6 +697,13 @@ image:
|
|||
file: mdi:alert-outline
|
||||
type: BINARY
|
||||
|
||||
graph:
|
||||
- id: my_graph
|
||||
sensor: ha_hello_world_temperature
|
||||
duration: 1h
|
||||
width: 100
|
||||
height: 100
|
||||
|
||||
cap1188:
|
||||
id: cap1188_component
|
||||
address: 0x29
|
||||
|
|
|
@ -944,13 +944,29 @@ climate:
|
|||
kd_multiplier: 0.0
|
||||
deadband_output_averaging_samples: 1
|
||||
- platform: haier
|
||||
protocol: hOn
|
||||
name: Haier AC
|
||||
supported_swing_modes:
|
||||
- vertical
|
||||
- horizontal
|
||||
- both
|
||||
update_interval: 10s
|
||||
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:
|
||||
- id: yard_sprinkler_ctrlr
|
||||
|
|
|
@ -384,6 +384,15 @@ binary_sensor:
|
|||
pullup: true
|
||||
inverted: false
|
||||
|
||||
- platform: gpio
|
||||
name: XL9535 Pin 0
|
||||
pin:
|
||||
xl9535: xl9535_hub
|
||||
number: 0
|
||||
mode:
|
||||
input: true
|
||||
inverted: false
|
||||
|
||||
climate:
|
||||
- platform: tuya
|
||||
id: tuya_climate
|
||||
|
@ -745,3 +754,7 @@ voice_assistant:
|
|||
max6956:
|
||||
- id: max6956_1
|
||||
address: 0x40
|
||||
|
||||
xl9535:
|
||||
- id: xl9535_hub
|
||||
address: 0x20
|
||||
|
|
Loading…
Reference in a new issue