mirror of
https://github.com/esphome/esphome.git
synced 2026-03-19 09:46:47 +01:00
commit
fb94778c04
13
.github/actions/restore-python/action.yml
vendored
13
.github/actions/restore-python/action.yml
vendored
@ -28,11 +28,20 @@ runs:
|
||||
# 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'
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
source venv/bin/activate
|
||||
python --version
|
||||
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
|
||||
pip install -e .
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
python -m venv venv
|
||||
./venv/Scripts/activate
|
||||
python --version
|
||||
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
|
||||
pip install -e .
|
||||
|
||||
105
.github/workflows/ci.yml
vendored
105
.github/workflows/ci.yml
vendored
@ -45,7 +45,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v3.3.2
|
||||
uses: actions/cache@v4.0.0
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@ -166,7 +166,35 @@ jobs:
|
||||
|
||||
pytest:
|
||||
name: Run pytest
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macOS-latest
|
||||
- windows-latest
|
||||
exclude:
|
||||
# Minimize CI resource usage
|
||||
# by only running the Python version
|
||||
# version used for docker images on Windows and macOS
|
||||
- python-version: "3.12"
|
||||
os: windows-latest
|
||||
- python-version: "3.10"
|
||||
os: windows-latest
|
||||
- python-version: "3.9"
|
||||
os: windows-latest
|
||||
- python-version: "3.12"
|
||||
os: macOS-latest
|
||||
- python-version: "3.10"
|
||||
os: macOS-latest
|
||||
- python-version: "3.9"
|
||||
os: macOS-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs:
|
||||
- common
|
||||
steps:
|
||||
@ -175,14 +203,24 @@ jobs:
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Register matcher
|
||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||
- name: Run pytest
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
./venv/Scripts/activate
|
||||
pytest -vv --cov-report=xml --tb=native tests
|
||||
- name: Run pytest
|
||||
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pytest -vv --tb=native tests
|
||||
pytest -vv --cov-report=xml --tb=native tests
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
clang-format:
|
||||
name: Check clang-format
|
||||
@ -327,7 +365,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache@v3.3.2
|
||||
uses: actions/cache@v4.0.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
# yamllint disable-line rule:line-length
|
||||
@ -354,6 +392,62 @@ jobs:
|
||||
# yamllint disable-line rule:line-length
|
||||
if: always()
|
||||
|
||||
list-components:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- common
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
# Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
|
||||
fetch-depth: 500
|
||||
- name: Fetch dev branch
|
||||
run: |
|
||||
git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/dev*:refs/remotes/origin/dev* +refs/tags/dev*:refs/tags/dev*
|
||||
git merge-base refs/remotes/origin/dev HEAD
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Find changed components
|
||||
id: set-matrix
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
echo "matrix=$(script/list-components.py --changed | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
|
||||
|
||||
test-build-components:
|
||||
name: Component test ${{ matrix.file }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- common
|
||||
- list-components
|
||||
if: ${{ needs.list-components.outputs.matrix != '[]' && needs.list-components.outputs.matrix != '' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix:
|
||||
file: ${{ fromJson(needs.list-components.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: test_build_components -e config -c ${{ matrix.file }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
./script/test_build_components -e config -c ${{ matrix.file }}
|
||||
- name: test_build_components -e compile -c ${{ matrix.file }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
./script/test_build_components -e compile -c ${{ matrix.file }}
|
||||
|
||||
ci-status:
|
||||
name: CI Status
|
||||
runs-on: ubuntu-latest
|
||||
@ -368,6 +462,7 @@ jobs:
|
||||
- pyupgrade
|
||||
- compile-tests
|
||||
- clang-tidy
|
||||
- test-build-components
|
||||
if: always()
|
||||
steps:
|
||||
- name: Success
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 23.12.0
|
||||
rev: 23.12.1
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
|
||||
21
CODEOWNERS
21
CODEOWNERS
@ -25,7 +25,7 @@ esphome/components/airthings_ble/* @jeromelaban
|
||||
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
|
||||
esphome/components/airthings_wave_mini/* @ncareau
|
||||
esphome/components/airthings_wave_plus/* @jeromelaban
|
||||
esphome/components/alarm_control_panel/* @grahambrown11
|
||||
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
|
||||
esphome/components/alpha3/* @jan-hofmeier
|
||||
esphome/components/am43/* @buxtronix
|
||||
esphome/components/am43/cover/* @buxtronix
|
||||
@ -34,6 +34,8 @@ esphome/components/analog_threshold/* @ianchi
|
||||
esphome/components/animation/* @syndlex
|
||||
esphome/components/anova/* @buxtronix
|
||||
esphome/components/api/* @OttoWinter
|
||||
esphome/components/as5600/* @ammmze
|
||||
esphome/components/as5600/sensor/* @ammmze
|
||||
esphome/components/as7341/* @mrgnr
|
||||
esphome/components/async_tcp/* @OttoWinter
|
||||
esphome/components/atc_mithermometer/* @ahpohl
|
||||
@ -50,8 +52,10 @@ esphome/components/bk72xx/* @kuba2k2
|
||||
esphome/components/bl0939/* @ziceva
|
||||
esphome/components/bl0940/* @tobias-
|
||||
esphome/components/bl0942/* @dbuezas
|
||||
esphome/components/ble_client/* @buxtronix
|
||||
esphome/components/ble_client/* @buxtronix @clydebarrow
|
||||
esphome/components/bluetooth_proxy/* @jesserockz
|
||||
esphome/components/bme280_base/* @esphome/core
|
||||
esphome/components/bme280_spi/* @apbodrov
|
||||
esphome/components/bme680_bsec/* @trvrnrth
|
||||
esphome/components/bmi160/* @flaviut
|
||||
esphome/components/bmp3xx/* @martgras
|
||||
@ -67,6 +71,7 @@ esphome/components/cd74hc4067/* @asoehlke
|
||||
esphome/components/climate/* @esphome/core
|
||||
esphome/components/climate_ir/* @glmnet
|
||||
esphome/components/color_temperature/* @jesserockz
|
||||
esphome/components/combination/* @Cat-Ion @kahrendt
|
||||
esphome/components/coolix/* @glmnet
|
||||
esphome/components/copy/* @OttoWinter
|
||||
esphome/components/cover/* @esphome/core
|
||||
@ -133,6 +138,7 @@ esphome/components/heatpumpir/* @rob-deutsch
|
||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
esphome/components/hm3301/* @freekode
|
||||
esphome/components/homeassistant/* @OttoWinter
|
||||
esphome/components/honeywell_hih_i2c/* @Benichou34
|
||||
esphome/components/honeywellabp/* @RubyBailey
|
||||
esphome/components/honeywellabp2_i2c/* @jpfaff
|
||||
esphome/components/host/* @esphome/core
|
||||
@ -156,7 +162,6 @@ esphome/components/integration/* @OttoWinter
|
||||
esphome/components/internal_temperature/* @Mat931
|
||||
esphome/components/interval/* @esphome/core
|
||||
esphome/components/json/* @OttoWinter
|
||||
esphome/components/kalman_combinator/* @Cat-Ion
|
||||
esphome/components/key_collector/* @ssieb
|
||||
esphome/components/key_provider/* @ssieb
|
||||
esphome/components/kuntze/* @ssieb
|
||||
@ -194,6 +199,7 @@ esphome/components/mcp9808/* @k7hpn
|
||||
esphome/components/md5/* @esphome/core
|
||||
esphome/components/mdns/* @esphome/core
|
||||
esphome/components/media_player/* @jesserockz
|
||||
esphome/components/micro_wake_word/* @jesserockz @kahrendt
|
||||
esphome/components/micronova/* @jorre05
|
||||
esphome/components/microphone/* @jesserockz
|
||||
esphome/components/mics_4514/* @jesserockz
|
||||
@ -223,7 +229,7 @@ esphome/components/nextion/binary_sensor/* @senexcrenshaw
|
||||
esphome/components/nextion/sensor/* @senexcrenshaw
|
||||
esphome/components/nextion/switch/* @senexcrenshaw
|
||||
esphome/components/nextion/text_sensor/* @senexcrenshaw
|
||||
esphome/components/nfc/* @jesserockz
|
||||
esphome/components/nfc/* @jesserockz @kbx81
|
||||
esphome/components/noblex/* @AGalfra
|
||||
esphome/components/number/* @esphome/core
|
||||
esphome/components/ota/* @esphome/core
|
||||
@ -314,6 +320,9 @@ esphome/components/ssd1331_base/* @kbx81
|
||||
esphome/components/ssd1331_spi/* @kbx81
|
||||
esphome/components/ssd1351_base/* @kbx81
|
||||
esphome/components/ssd1351_spi/* @kbx81
|
||||
esphome/components/st7567_base/* @latonita
|
||||
esphome/components/st7567_i2c/* @latonita
|
||||
esphome/components/st7567_spi/* @latonita
|
||||
esphome/components/st7735/* @SenexCrenshaw
|
||||
esphome/components/st7789v/* @kbx81
|
||||
esphome/components/st7920/* @marsjan155
|
||||
@ -325,7 +334,7 @@ esphome/components/tca9548a/* @andreashergert1984
|
||||
esphome/components/tcl112/* @glmnet
|
||||
esphome/components/tee501/* @Stock-M
|
||||
esphome/components/teleinfo/* @0hax
|
||||
esphome/components/template/alarm_control_panel/* @grahambrown11
|
||||
esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar
|
||||
esphome/components/text/* @mauritskorse
|
||||
esphome/components/thermostat/* @kbx81
|
||||
esphome/components/time/* @OttoWinter
|
||||
@ -355,9 +364,11 @@ esphome/components/ufire_ec/* @pvizeli
|
||||
esphome/components/ufire_ise/* @pvizeli
|
||||
esphome/components/ultrasonic/* @OttoWinter
|
||||
esphome/components/vbus/* @ssieb
|
||||
esphome/components/veml3235/* @kbx81
|
||||
esphome/components/version/* @esphome/core
|
||||
esphome/components/voice_assistant/* @jesserockz
|
||||
esphome/components/wake_on_lan/* @willwill2will54
|
||||
esphome/components/waveshare_epaper/* @clydebarrow
|
||||
esphome/components/web_server_base/* @OttoWinter
|
||||
esphome/components/web_server_idf/* @dentra
|
||||
esphome/components/whirlpool/* @glmnet
|
||||
|
||||
@ -35,7 +35,7 @@ RUN \
|
||||
iputils-ping=3:20221126-1 \
|
||||
git=1:2.39.2-1.1 \
|
||||
curl=7.88.1-10+deb12u5 \
|
||||
openssh-client=1:9.2p1-2+deb12u1 \
|
||||
openssh-client=1:9.2p1-2+deb12u2 \
|
||||
python3-cffi=1.15.1-5 \
|
||||
libcairo2=1.16.0-7 \
|
||||
libmagic1=1:5.44-3 \
|
||||
@ -81,7 +81,7 @@ RUN \
|
||||
fi; \
|
||||
pip3 install \
|
||||
--break-system-packages --no-cache-dir \
|
||||
platformio==6.1.11 \
|
||||
platformio==6.1.13 \
|
||||
# Change some platformio settings
|
||||
&& platformio settings set enable_telemetry No \
|
||||
&& platformio settings set check_platformio_interval 1000000 \
|
||||
|
||||
@ -139,6 +139,9 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||
VARIANT_ESP32C3: {
|
||||
5: adc2_channel_t.ADC2_CHANNEL_0,
|
||||
},
|
||||
VARIANT_ESP32C2: {},
|
||||
VARIANT_ESP32C6: {},
|
||||
VARIANT_ESP32H2: {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
|
||||
CODEOWNERS = ["@grahambrown11"]
|
||||
CODEOWNERS = ["@grahambrown11", "@hwstar"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
CONF_ON_TRIGGERED = "on_triggered"
|
||||
@ -22,6 +22,8 @@ CONF_ON_ARMED_HOME = "on_armed_home"
|
||||
CONF_ON_ARMED_NIGHT = "on_armed_night"
|
||||
CONF_ON_ARMED_AWAY = "on_armed_away"
|
||||
CONF_ON_DISARMED = "on_disarmed"
|
||||
CONF_ON_CHIME = "on_chime"
|
||||
CONF_ON_READY = "on_ready"
|
||||
|
||||
alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel")
|
||||
AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase)
|
||||
@ -53,12 +55,22 @@ ArmedAwayTrigger = alarm_control_panel_ns.class_(
|
||||
DisarmedTrigger = alarm_control_panel_ns.class_(
|
||||
"DisarmedTrigger", automation.Trigger.template()
|
||||
)
|
||||
ChimeTrigger = alarm_control_panel_ns.class_(
|
||||
"ChimeTrigger", automation.Trigger.template()
|
||||
)
|
||||
ReadyTrigger = alarm_control_panel_ns.class_(
|
||||
"ReadyTrigger", automation.Trigger.template()
|
||||
)
|
||||
|
||||
ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action)
|
||||
ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action)
|
||||
ArmNightAction = alarm_control_panel_ns.class_("ArmNightAction", automation.Action)
|
||||
DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action)
|
||||
PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action)
|
||||
TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action)
|
||||
ChimeAction = alarm_control_panel_ns.class_("ChimeAction", automation.Action)
|
||||
ReadyAction = alarm_control_panel_ns.class_("ReadyAction", automation.Action)
|
||||
|
||||
AlarmControlPanelCondition = alarm_control_panel_ns.class_(
|
||||
"AlarmControlPanelCondition", automation.Condition
|
||||
)
|
||||
@ -111,6 +123,16 @@ ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_CHIME): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_READY): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@ -157,6 +179,12 @@ async def setup_alarm_control_panel_core_(var, config):
|
||||
for conf in config.get(CONF_ON_CLEARED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_CHIME, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_READY, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
|
||||
async def register_alarm_control_panel(var, config):
|
||||
@ -232,6 +260,29 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
|
||||
)
|
||||
async def alarm_action_chime_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(
|
||||
"alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_condition(
|
||||
"alarm_control_panel.ready",
|
||||
AlarmControlPanelCondition,
|
||||
ALARM_CONTROL_PANEL_CONDITION_SCHEMA,
|
||||
)
|
||||
async def alarm_action_ready_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_condition(
|
||||
"alarm_control_panel.is_armed",
|
||||
AlarmControlPanelCondition,
|
||||
|
||||
@ -96,6 +96,14 @@ void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback
|
||||
this->cleared_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_chime_callback(std::function<void()> &&callback) {
|
||||
this->chime_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback) {
|
||||
this->ready_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_away(optional<std::string> code) {
|
||||
auto call = this->make_call();
|
||||
call.arm_away();
|
||||
|
||||
@ -89,6 +89,18 @@ class AlarmControlPanel : public EntityBase {
|
||||
*/
|
||||
void add_on_cleared_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when a chime zone goes from closed to open
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_chime_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when a ready state changes
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_ready_callback(std::function<void()> &&callback);
|
||||
|
||||
/** A numeric representation of the supported features as per HomeAssistant
|
||||
*
|
||||
*/
|
||||
@ -178,6 +190,10 @@ class AlarmControlPanel : public EntityBase {
|
||||
CallbackManager<void()> disarmed_callback_{};
|
||||
// clear callback
|
||||
CallbackManager<void()> cleared_callback_{};
|
||||
// chime callback
|
||||
CallbackManager<void()> chime_callback_{};
|
||||
// ready callback
|
||||
CallbackManager<void()> ready_callback_{};
|
||||
};
|
||||
|
||||
} // namespace alarm_control_panel
|
||||
|
||||
@ -69,6 +69,20 @@ class ClearedTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
class ChimeTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class ReadyTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
|
||||
|
||||
228
esphome/components/as5600/__init__.py
Normal file
228
esphome/components/as5600/__init__.py
Normal file
@ -0,0 +1,228 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_DIR_PIN,
|
||||
CONF_DIRECTION,
|
||||
CONF_HYSTERESIS,
|
||||
CONF_RANGE,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@ammmze"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
MULTI_CONF = True
|
||||
|
||||
as5600_ns = cg.esphome_ns.namespace("as5600")
|
||||
AS5600Component = as5600_ns.class_("AS5600Component", cg.Component, i2c.I2CDevice)
|
||||
|
||||
DIRECTION = {
|
||||
"CLOCKWISE": 0,
|
||||
"COUNTERCLOCKWISE": 1,
|
||||
}
|
||||
|
||||
POWER_MODE = {
|
||||
"NOMINAL": 0,
|
||||
"LOW1": 1,
|
||||
"LOW2": 2,
|
||||
"LOW3": 3,
|
||||
}
|
||||
|
||||
HYSTERESIS = {
|
||||
"NONE": 0,
|
||||
"LSB1": 1,
|
||||
"LSB2": 2,
|
||||
"LSB3": 3,
|
||||
}
|
||||
|
||||
SLOW_FILTER = {
|
||||
"16X": 0,
|
||||
"8X": 1,
|
||||
"4X": 2,
|
||||
"2X": 3,
|
||||
}
|
||||
|
||||
FAST_FILTER = {
|
||||
"NONE": 0,
|
||||
"LSB6": 1,
|
||||
"LSB7": 2,
|
||||
"LSB9": 3,
|
||||
"LSB18": 4,
|
||||
"LSB21": 5,
|
||||
"LSB24": 6,
|
||||
"LSB10": 7,
|
||||
}
|
||||
|
||||
CONF_ANGLE = "angle"
|
||||
CONF_RAW_ANGLE = "raw_angle"
|
||||
CONF_RAW_POSITION = "raw_position"
|
||||
CONF_WATCHDOG = "watchdog"
|
||||
CONF_POWER_MODE = "power_mode"
|
||||
CONF_SLOW_FILTER = "slow_filter"
|
||||
CONF_FAST_FILTER = "fast_filter"
|
||||
CONF_START_POSITION = "start_position"
|
||||
CONF_END_POSITION = "end_position"
|
||||
|
||||
|
||||
RESOLUTION = 4096
|
||||
MAX_POSITION = RESOLUTION - 1
|
||||
ANGLE_TO_POSITION = RESOLUTION / 360
|
||||
POSITION_TO_ANGLE = 360 / RESOLUTION
|
||||
# validate min range of 18deg (per datasheet) ... though i seem to get valid values down to a range of 192steps (16.875deg)
|
||||
MIN_RANGE = round(18 * ANGLE_TO_POSITION)
|
||||
|
||||
|
||||
def angle(min=-360, max=360):
|
||||
return cv.All(
|
||||
cv.float_with_unit("angle", "(°|deg)"), cv.float_range(min=min, max=max)
|
||||
)
|
||||
|
||||
|
||||
def angle_to_position(value, min=-360, max=360):
|
||||
try:
|
||||
value = angle(min=min, max=max)(value)
|
||||
return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION
|
||||
except cv.Invalid as e:
|
||||
raise cv.Invalid(f"When using angle, {e.error_message}")
|
||||
|
||||
|
||||
def percent_to_position(value):
|
||||
value = cv.possibly_negative_percentage(value)
|
||||
return (RESOLUTION + round(value * RESOLUTION)) % RESOLUTION
|
||||
|
||||
|
||||
def position(min=-MAX_POSITION, max=MAX_POSITION):
|
||||
"""Validate that the config option is a position.
|
||||
Accepts integers, degrees, or percentage (of 360 degrees).
|
||||
"""
|
||||
|
||||
def validator(value):
|
||||
if isinstance(value, str) and value.endswith("%"):
|
||||
value = percent_to_position(value)
|
||||
|
||||
if isinstance(value, str) and (value.endswith("°") or value.endswith("deg")):
|
||||
return angle_to_position(
|
||||
value,
|
||||
min=round(min * POSITION_TO_ANGLE),
|
||||
max=round(max * POSITION_TO_ANGLE),
|
||||
)
|
||||
|
||||
return cv.int_range(min=min, max=max)(value)
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def position_range():
|
||||
"""Validate that value given is a valid range for the device.
|
||||
A valid range is one of the following:
|
||||
- a value of 0 (meaning full range)
|
||||
- 18 thru 360 degrees
|
||||
- negative 360 thru negative 18 degrees (notes: these are normalized to their positive values, accepting negatives is for convenience)
|
||||
"""
|
||||
zero_validator = position(min=0, max=0)
|
||||
negative_validator = cv.Any(
|
||||
position(min=-MAX_POSITION, max=-MIN_RANGE),
|
||||
zero_validator,
|
||||
)
|
||||
positive_validator = cv.Any(
|
||||
position(min=MIN_RANGE, max=MAX_POSITION),
|
||||
zero_validator,
|
||||
)
|
||||
|
||||
def validator(value):
|
||||
is_negative_str = isinstance(value, str) and value.startswith("-")
|
||||
is_negative_num = isinstance(value, (float, int)) and value < 0
|
||||
if is_negative_str or is_negative_num:
|
||||
return negative_validator(value)
|
||||
return positive_validator(value)
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def has_valid_range_config():
|
||||
"""Validate that that the config start + end position results in a valid
|
||||
positional range, which must be >= 18degrees
|
||||
"""
|
||||
range_validator = position_range()
|
||||
|
||||
def validator(config):
|
||||
# if we don't have an end position, then there is nothing to do
|
||||
if CONF_END_POSITION not in config:
|
||||
return config
|
||||
|
||||
# determine the range by taking the difference from the end and start
|
||||
range = config[CONF_END_POSITION] - config[CONF_START_POSITION]
|
||||
|
||||
# but need to account for start position being greater than end position
|
||||
# where the range rolls back around the 0 position
|
||||
if config[CONF_END_POSITION] < config[CONF_START_POSITION]:
|
||||
range = RESOLUTION + config[CONF_END_POSITION] - config[CONF_START_POSITION]
|
||||
|
||||
try:
|
||||
range_validator(range)
|
||||
return config
|
||||
except cv.Invalid as e:
|
||||
raise cv.Invalid(
|
||||
f"The range between start and end position is invalid. It was was {range} but {e.error_message}"
|
||||
)
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AS5600Component),
|
||||
cv.Optional(CONF_DIR_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Optional(CONF_DIRECTION, default="CLOCKWISE"): cv.enum(
|
||||
DIRECTION, upper=True
|
||||
),
|
||||
cv.Optional(CONF_WATCHDOG, default=False): cv.boolean,
|
||||
cv.Optional(CONF_POWER_MODE, default="NOMINAL"): cv.enum(
|
||||
POWER_MODE, upper=True, space=""
|
||||
),
|
||||
cv.Optional(CONF_HYSTERESIS, default="NONE"): cv.enum(
|
||||
HYSTERESIS, upper=True, space=""
|
||||
),
|
||||
cv.Optional(CONF_SLOW_FILTER, default="16X"): cv.enum(
|
||||
SLOW_FILTER, upper=True, space=""
|
||||
),
|
||||
cv.Optional(CONF_FAST_FILTER, default="NONE"): cv.enum(
|
||||
FAST_FILTER, upper=True, space=""
|
||||
),
|
||||
cv.Optional(CONF_START_POSITION, default=0): position(),
|
||||
cv.Optional(CONF_END_POSITION): position(),
|
||||
cv.Optional(CONF_RANGE): position_range(),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x36)),
|
||||
# ensure end_position and range are mutually exclusive
|
||||
cv.has_at_most_one_key(CONF_END_POSITION, CONF_RANGE),
|
||||
has_valid_range_config(),
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
cg.add(var.set_direction(config[CONF_DIRECTION]))
|
||||
cg.add(var.set_watchdog(config[CONF_WATCHDOG]))
|
||||
cg.add(var.set_power_mode(config[CONF_POWER_MODE]))
|
||||
cg.add(var.set_hysteresis(config[CONF_HYSTERESIS]))
|
||||
cg.add(var.set_slow_filter(config[CONF_SLOW_FILTER]))
|
||||
cg.add(var.set_fast_filter(config[CONF_FAST_FILTER]))
|
||||
cg.add(var.set_start_position(config[CONF_START_POSITION]))
|
||||
|
||||
if dir_pin_config := config.get(CONF_DIR_PIN):
|
||||
pin = await cg.gpio_pin_expression(dir_pin_config)
|
||||
cg.add(var.set_dir_pin(pin))
|
||||
|
||||
if (end_position_config := config.get(CONF_END_POSITION, None)) is not None:
|
||||
cg.add(var.set_end_position(end_position_config))
|
||||
|
||||
if (range_config := config.get(CONF_RANGE, None)) is not None:
|
||||
cg.add(var.set_range(range_config))
|
||||
138
esphome/components/as5600/as5600.cpp
Normal file
138
esphome/components/as5600/as5600.cpp
Normal file
@ -0,0 +1,138 @@
|
||||
#include "as5600.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace as5600 {
|
||||
|
||||
static const char *const TAG = "as5600";
|
||||
|
||||
// Configuration registers
|
||||
static const uint8_t REGISTER_ZMCO = 0x00; // 8 bytes / R
|
||||
static const uint8_t REGISTER_ZPOS = 0x01; // 16 bytes / RW
|
||||
static const uint8_t REGISTER_MPOS = 0x03; // 16 bytes / RW
|
||||
static const uint8_t REGISTER_MANG = 0x05; // 16 bytes / RW
|
||||
static const uint8_t REGISTER_CONF = 0x07; // 16 bytes / RW
|
||||
|
||||
// Output registers
|
||||
static const uint8_t REGISTER_ANGLE_RAW = 0x0C; // 16 bytes / R
|
||||
static const uint8_t REGISTER_ANGLE = 0x0E; // 16 bytes / R
|
||||
|
||||
// Status registers
|
||||
static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R
|
||||
static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R
|
||||
static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
|
||||
|
||||
void AS5600Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up AS5600...");
|
||||
|
||||
if (!this->read_byte(REGISTER_STATUS).has_value()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// configuration direction pin, if given
|
||||
// the dir pin on the chip should be low for clockwise
|
||||
// and high for counterclockwise. If the pin is left floating
|
||||
// the reported positions will be erratic.
|
||||
if (this->dir_pin_ != nullptr) {
|
||||
this->dir_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
this->dir_pin_->digital_write(this->direction_ == 1);
|
||||
}
|
||||
|
||||
// build config register
|
||||
// take the value, shift it left, and add mask to it to ensure we
|
||||
// are only changing the bits appropriate for that setting in the
|
||||
// off chance we somehow have bad value in there and it makes for
|
||||
// a nice visual for the bit positions.
|
||||
uint16_t config = 0;
|
||||
// clang-format off
|
||||
config |= (this->watchdog_ << 13) & 0b0010000000000000;
|
||||
config |= (this->fast_filter_ << 10) & 0b0001110000000000;
|
||||
config |= (this->slow_filter_ << 8) & 0b0000001100000000;
|
||||
config |= (this->pwm_frequency_ << 6) & 0b0000000011000000;
|
||||
config |= (this->output_mode_ << 4) & 0b0000000000110000;
|
||||
config |= (this->hysteresis_ << 2) & 0b0000000000001100;
|
||||
config |= (this->power_mode_ << 0) & 0b0000000000000011;
|
||||
// clang-format on
|
||||
|
||||
// write config to config register
|
||||
if (!this->write_byte_16(REGISTER_CONF, config)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// configure the start position
|
||||
this->write_byte_16(REGISTER_ZPOS, this->start_position_);
|
||||
|
||||
// configure either end position or max angle
|
||||
if (this->end_mode_ == END_MODE_POSITION) {
|
||||
this->write_byte_16(REGISTER_MPOS, this->end_position_);
|
||||
} else {
|
||||
this->write_byte_16(REGISTER_MANG, this->end_position_);
|
||||
}
|
||||
|
||||
// calculate the raw max from end position or start + range
|
||||
this->raw_max_ = this->end_mode_ == END_MODE_POSITION ? this->end_position_ & 4095
|
||||
: (this->start_position_ + this->end_position_) & 4095;
|
||||
|
||||
// calculate allowed range of motion by taking the start from the end
|
||||
// but only if the end is greater than the start. If the start is greater
|
||||
// than the end position, then that means we take the start all the way to
|
||||
// reset point (i.e. 0 deg raw) and then we that with the end position
|
||||
uint16_t range = this->raw_max_ > this->start_position_ ? this->raw_max_ - this->start_position_
|
||||
: (4095 - this->start_position_) + this->raw_max_;
|
||||
|
||||
// range scale is ratio of actual allowed range to the full range
|
||||
this->range_scale_ = range / 4095.0f;
|
||||
}
|
||||
|
||||
void AS5600Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AS5600:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication with AS5600 failed!");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Watchdog: %d", this->watchdog_);
|
||||
ESP_LOGCONFIG(TAG, " Fast Filter: %d", this->fast_filter_);
|
||||
ESP_LOGCONFIG(TAG, " Slow Filter: %d", this->slow_filter_);
|
||||
ESP_LOGCONFIG(TAG, " Hysteresis: %d", this->hysteresis_);
|
||||
ESP_LOGCONFIG(TAG, " Start Position: %d", this->start_position_);
|
||||
if (this->end_mode_ == END_MODE_POSITION) {
|
||||
ESP_LOGCONFIG(TAG, " End Position: %d", this->end_position_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Range: %d", this->end_position_);
|
||||
}
|
||||
}
|
||||
|
||||
bool AS5600Component::in_range(uint16_t raw_position) {
|
||||
return this->raw_max_ > this->start_position_
|
||||
? raw_position >= this->start_position_ && raw_position <= this->raw_max_
|
||||
: raw_position >= this->start_position_ || raw_position <= this->raw_max_;
|
||||
}
|
||||
|
||||
AS5600MagnetStatus AS5600Component::read_magnet_status() {
|
||||
uint8_t status = this->reg(REGISTER_STATUS).get() >> 3 & 0b000111;
|
||||
return static_cast<AS5600MagnetStatus>(status);
|
||||
}
|
||||
|
||||
optional<uint16_t> AS5600Component::read_position() {
|
||||
uint16_t pos = 0;
|
||||
if (!this->read_byte_16(REGISTER_ANGLE, &pos)) {
|
||||
return {};
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
optional<uint16_t> AS5600Component::read_raw_position() {
|
||||
uint16_t pos = 0;
|
||||
if (!this->read_byte_16(REGISTER_ANGLE_RAW, &pos)) {
|
||||
return {};
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
} // namespace as5600
|
||||
} // namespace esphome
|
||||
105
esphome/components/as5600/as5600.h
Normal file
105
esphome/components/as5600/as5600.h
Normal file
@ -0,0 +1,105 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace as5600 {
|
||||
|
||||
static const uint16_t POSITION_COUNT = 4096;
|
||||
static const float RAW_TO_DEGREES = 360.0 / POSITION_COUNT;
|
||||
static const float DEGREES_TO_RAW = POSITION_COUNT / 360.0;
|
||||
|
||||
enum EndPositionMode : uint8_t {
|
||||
// In this mode, the end position is calculated by taking the start position
|
||||
// and adding the range/positions. For example, you could say start at 90deg,
|
||||
// and have a range of 180deg and effectively the sensor will report values
|
||||
// from the physical 90deg thru 270deg.
|
||||
END_MODE_RANGE,
|
||||
// In this mode, the end position is explicitly set, and changing the start
|
||||
// position will NOT change the end position.
|
||||
END_MODE_POSITION,
|
||||
};
|
||||
|
||||
enum OutRangeMode : uint8_t {
|
||||
// In this mode, the AS5600 chip itself actually reports these values, but
|
||||
// effectively it splits the out-of-range values in half, and when positioned
|
||||
// over the half closest to the min/start position, it will report 0 and when
|
||||
// positioned over the half closes to the max/end position, it will report the
|
||||
// max/end value.
|
||||
OUT_RANGE_MODE_MIN_MAX,
|
||||
// In this mode, when the magnet is positioned outside the configured
|
||||
// range, the sensor will report NAN, which translates to "Unknown"
|
||||
// in Home Assistant.
|
||||
OUT_RANGE_MODE_NAN,
|
||||
};
|
||||
|
||||
enum AS5600MagnetStatus : uint8_t {
|
||||
MAGNET_GONE = 2, // 0b010 / magnet not detected
|
||||
MAGNET_OK = 4, // 0b100 / magnet just right
|
||||
MAGNET_STRONG = 5, // 0b101 / magnet too strong
|
||||
MAGNET_WEAK = 6, // 0b110 / magnet too weak
|
||||
};
|
||||
|
||||
class AS5600Component : public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
/// Set up the internal sensor array.
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
/// HARDWARE_LATE setup priority
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
// configuration setters
|
||||
void set_dir_pin(InternalGPIOPin *pin) { this->dir_pin_ = pin; }
|
||||
void set_direction(uint8_t direction) { this->direction_ = direction; }
|
||||
void set_fast_filter(uint8_t fast_filter) { this->fast_filter_ = fast_filter; }
|
||||
void set_hysteresis(uint8_t hysteresis) { this->hysteresis_ = hysteresis; }
|
||||
void set_power_mode(uint8_t power_mode) { this->power_mode_ = power_mode; }
|
||||
void set_slow_filter(uint8_t slow_filter) { this->slow_filter_ = slow_filter; }
|
||||
void set_watchdog(bool watchdog) { this->watchdog_ = watchdog; }
|
||||
bool get_watchdog() { return this->watchdog_; }
|
||||
void set_start_position(uint16_t start_position) { this->start_position_ = start_position % POSITION_COUNT; }
|
||||
void set_end_position(uint16_t end_position) {
|
||||
this->end_position_ = end_position % POSITION_COUNT;
|
||||
this->end_mode_ = END_MODE_POSITION;
|
||||
}
|
||||
void set_range(uint16_t range) {
|
||||
this->end_position_ = range % POSITION_COUNT;
|
||||
this->end_mode_ = END_MODE_RANGE;
|
||||
}
|
||||
|
||||
// Gets the scale value for the configured range.
|
||||
// For example, if configured to start at 0deg and end at 180deg, the
|
||||
// range is 50% of the native/raw range, so the range scale would be 0.5.
|
||||
// If configured to use the full 360deg, the range scale would be 1.0.
|
||||
float get_range_scale() { return this->range_scale_; }
|
||||
|
||||
// Indicates whether the given *raw* position is within the configured range
|
||||
bool in_range(uint16_t raw_position);
|
||||
|
||||
AS5600MagnetStatus read_magnet_status();
|
||||
optional<uint16_t> read_position();
|
||||
optional<uint16_t> read_raw_position();
|
||||
|
||||
protected:
|
||||
InternalGPIOPin *dir_pin_{nullptr};
|
||||
uint8_t direction_;
|
||||
uint8_t fast_filter_;
|
||||
uint8_t hysteresis_;
|
||||
uint8_t power_mode_;
|
||||
uint8_t slow_filter_;
|
||||
uint8_t pwm_frequency_{0};
|
||||
uint8_t output_mode_{0};
|
||||
bool watchdog_;
|
||||
uint16_t start_position_;
|
||||
uint16_t end_position_{0};
|
||||
uint16_t raw_max_;
|
||||
EndPositionMode end_mode_{END_MODE_RANGE};
|
||||
float range_scale_{1.0};
|
||||
};
|
||||
|
||||
} // namespace as5600
|
||||
} // namespace esphome
|
||||
119
esphome/components/as5600/sensor/__init__.py
Normal file
119
esphome/components/as5600/sensor/__init__.py
Normal file
@ -0,0 +1,119 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
ICON_MAGNET,
|
||||
ICON_ROTATE_RIGHT,
|
||||
CONF_GAIN,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
CONF_MAGNITUDE,
|
||||
CONF_STATUS,
|
||||
CONF_POSITION,
|
||||
)
|
||||
from .. import as5600_ns, AS5600Component
|
||||
|
||||
CODEOWNERS = ["@ammmze"]
|
||||
DEPENDENCIES = ["as5600"]
|
||||
|
||||
AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent)
|
||||
|
||||
CONF_ANGLE = "angle"
|
||||
CONF_RAW_ANGLE = "raw_angle"
|
||||
CONF_RAW_POSITION = "raw_position"
|
||||
CONF_WATCHDOG = "watchdog"
|
||||
CONF_POWER_MODE = "power_mode"
|
||||
CONF_SLOW_FILTER = "slow_filter"
|
||||
CONF_FAST_FILTER = "fast_filter"
|
||||
CONF_PWM_FREQUENCY = "pwm_frequency"
|
||||
CONF_BURN_COUNT = "burn_count"
|
||||
CONF_START_POSITION = "start_position"
|
||||
CONF_END_POSITION = "end_position"
|
||||
CONF_OUT_OF_RANGE_MODE = "out_of_range_mode"
|
||||
|
||||
OutOfRangeMode = as5600_ns.enum("OutRangeMode")
|
||||
OUT_OF_RANGE_MODES = {
|
||||
"MIN_MAX": OutOfRangeMode.OUT_RANGE_MODE_MIN_MAX,
|
||||
"NAN": OutOfRangeMode.OUT_RANGE_MODE_NAN,
|
||||
}
|
||||
|
||||
|
||||
CONF_AS5600_ID = "as5600_id"
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
AS5600Sensor,
|
||||
accuracy_decimals=0,
|
||||
icon=ICON_ROTATE_RIGHT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_AS5600_ID): cv.use_id(AS5600Component),
|
||||
cv.Optional(CONF_OUT_OF_RANGE_MODE): cv.enum(
|
||||
OUT_OF_RANGE_MODES, upper=True, space="_"
|
||||
),
|
||||
cv.Optional(CONF_RAW_POSITION): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
icon=ICON_ROTATE_RIGHT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_GAIN): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_MAGNITUDE): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
icon=ICON_MAGNET,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_STATUS): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
icon=ICON_MAGNET,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_parented(var, config[CONF_AS5600_ID])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
|
||||
if out_of_range_mode_config := config.get(CONF_OUT_OF_RANGE_MODE):
|
||||
cg.add(var.set_out_of_range_mode(out_of_range_mode_config))
|
||||
|
||||
if angle_config := config.get(CONF_ANGLE):
|
||||
sens = await sensor.new_sensor(angle_config)
|
||||
cg.add(var.set_angle_sensor(sens))
|
||||
|
||||
if raw_angle_config := config.get(CONF_RAW_ANGLE):
|
||||
sens = await sensor.new_sensor(raw_angle_config)
|
||||
cg.add(var.set_raw_angle_sensor(sens))
|
||||
|
||||
if position_config := config.get(CONF_POSITION):
|
||||
sens = await sensor.new_sensor(position_config)
|
||||
cg.add(var.set_position_sensor(sens))
|
||||
|
||||
if raw_position_config := config.get(CONF_RAW_POSITION):
|
||||
sens = await sensor.new_sensor(raw_position_config)
|
||||
cg.add(var.set_raw_position_sensor(sens))
|
||||
|
||||
if gain_config := config.get(CONF_GAIN):
|
||||
sens = await sensor.new_sensor(gain_config)
|
||||
cg.add(var.set_gain_sensor(sens))
|
||||
|
||||
if magnitude_config := config.get(CONF_MAGNITUDE):
|
||||
sens = await sensor.new_sensor(magnitude_config)
|
||||
cg.add(var.set_magnitude_sensor(sens))
|
||||
|
||||
if status_config := config.get(CONF_STATUS):
|
||||
sens = await sensor.new_sensor(status_config)
|
||||
cg.add(var.set_status_sensor(sens))
|
||||
98
esphome/components/as5600/sensor/as5600_sensor.cpp
Normal file
98
esphome/components/as5600/sensor/as5600_sensor.cpp
Normal file
@ -0,0 +1,98 @@
|
||||
#include "as5600_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace as5600 {
|
||||
|
||||
static const char *const TAG = "as5600.sensor";
|
||||
|
||||
// Configuration registers
|
||||
static const uint8_t REGISTER_ZMCO = 0x00; // 8 bytes / R
|
||||
static const uint8_t REGISTER_ZPOS = 0x01; // 16 bytes / RW
|
||||
static const uint8_t REGISTER_MPOS = 0x03; // 16 bytes / RW
|
||||
static const uint8_t REGISTER_MANG = 0x05; // 16 bytes / RW
|
||||
static const uint8_t REGISTER_CONF = 0x07; // 16 bytes / RW
|
||||
|
||||
// Output registers
|
||||
static const uint8_t REGISTER_ANGLE_RAW = 0x0C; // 16 bytes / R
|
||||
static const uint8_t REGISTER_ANGLE = 0x0E; // 16 bytes / R
|
||||
|
||||
// Status registers
|
||||
static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R
|
||||
static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R
|
||||
static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
|
||||
|
||||
float AS5600Sensor::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void AS5600Sensor::dump_config() {
|
||||
LOG_SENSOR("", "AS5600 Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_);
|
||||
if (this->angle_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Angle Sensor", this->angle_sensor_);
|
||||
}
|
||||
if (this->raw_angle_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Raw Angle Sensor", this->raw_angle_sensor_);
|
||||
}
|
||||
if (this->position_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Position Sensor", this->position_sensor_);
|
||||
}
|
||||
if (this->raw_position_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_);
|
||||
}
|
||||
if (this->gain_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_);
|
||||
}
|
||||
if (this->magnitude_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_);
|
||||
}
|
||||
if (this->status_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Status Sensor", this->status_sensor_);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
void AS5600Sensor::update() {
|
||||
if (this->gain_sensor_ != nullptr) {
|
||||
this->gain_sensor_->publish_state(this->parent_->reg(REGISTER_AGC).get());
|
||||
}
|
||||
|
||||
if (this->magnitude_sensor_ != nullptr) {
|
||||
uint16_t value = 0;
|
||||
this->parent_->read_byte_16(REGISTER_MAGNITUDE, &value);
|
||||
this->magnitude_sensor_->publish_state(value);
|
||||
}
|
||||
|
||||
// 2 = magnet not detected
|
||||
// 4 = magnet just right
|
||||
// 5 = magnet too strong
|
||||
// 6 = magnet too weak
|
||||
if (this->status_sensor_ != nullptr) {
|
||||
this->status_sensor_->publish_state(this->parent_->read_magnet_status());
|
||||
}
|
||||
|
||||
auto pos = this->parent_->read_position();
|
||||
if (!pos.has_value()) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
auto raw = this->parent_->read_raw_position();
|
||||
if (!raw.has_value()) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->out_of_range_mode_ == OUT_RANGE_MODE_NAN) {
|
||||
this->publish_state(this->parent_->in_range(raw.value()) ? pos.value() : NAN);
|
||||
} else {
|
||||
this->publish_state(pos.value());
|
||||
}
|
||||
|
||||
if (this->raw_position_sensor_ != nullptr) {
|
||||
this->raw_position_sensor_->publish_state(raw.value());
|
||||
}
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
} // namespace as5600
|
||||
} // namespace esphome
|
||||
43
esphome/components/as5600/sensor/as5600_sensor.h
Normal file
43
esphome/components/as5600/sensor/as5600_sensor.h
Normal file
@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/as5600/as5600.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace as5600 {
|
||||
|
||||
class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>, public sensor::Sensor {
|
||||
public:
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; }
|
||||
void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; }
|
||||
void set_position_sensor(sensor::Sensor *position_sensor) { this->position_sensor_ = position_sensor; }
|
||||
void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) {
|
||||
this->raw_position_sensor_ = raw_position_sensor;
|
||||
}
|
||||
void set_gain_sensor(sensor::Sensor *gain_sensor) { this->gain_sensor_ = gain_sensor; }
|
||||
void set_magnitude_sensor(sensor::Sensor *magnitude_sensor) { this->magnitude_sensor_ = magnitude_sensor; }
|
||||
void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; }
|
||||
void set_out_of_range_mode(OutRangeMode oor_mode) { this->out_of_range_mode_ = oor_mode; }
|
||||
OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; }
|
||||
|
||||
protected:
|
||||
sensor::Sensor *angle_sensor_{nullptr};
|
||||
sensor::Sensor *raw_angle_sensor_{nullptr};
|
||||
sensor::Sensor *position_sensor_{nullptr};
|
||||
sensor::Sensor *raw_position_sensor_{nullptr};
|
||||
sensor::Sensor *gain_sensor_{nullptr};
|
||||
sensor::Sensor *magnitude_sensor_{nullptr};
|
||||
sensor::Sensor *status_sensor_{nullptr};
|
||||
OutRangeMode out_of_range_mode_{OUT_RANGE_MODE_MIN_MAX};
|
||||
};
|
||||
|
||||
} // namespace as5600
|
||||
} // namespace esphome
|
||||
@ -242,7 +242,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
this->set_notify_(true);
|
||||
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_.has_value()) {
|
||||
if (this->time_id_ != nullptr) {
|
||||
this->send_local_time();
|
||||
}
|
||||
#endif
|
||||
@ -441,9 +441,8 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) {
|
||||
|
||||
#ifdef USE_TIME
|
||||
void BedJetHub::send_local_time() {
|
||||
if (this->time_id_.has_value()) {
|
||||
auto *time_id = *this->time_id_;
|
||||
ESPTime now = time_id->now();
|
||||
if (this->time_id_ != nullptr) {
|
||||
ESPTime now = this->time_id_->now();
|
||||
if (now.is_valid()) {
|
||||
this->set_clock(now.hour, now.minute);
|
||||
ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute);
|
||||
@ -454,10 +453,9 @@ void BedJetHub::send_local_time() {
|
||||
}
|
||||
|
||||
void BedJetHub::setup_time_() {
|
||||
if (this->time_id_.has_value()) {
|
||||
if (this->time_id_ != nullptr) {
|
||||
this->send_local_time();
|
||||
auto *time_id = *this->time_id_;
|
||||
time_id->add_on_time_sync_callback([this] { this->send_local_time(); });
|
||||
this->time_id_->add_on_time_sync_callback([this] { this->send_local_time(); });
|
||||
} else {
|
||||
ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
|
||||
}
|
||||
|
||||
@ -141,7 +141,7 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo
|
||||
#ifdef USE_TIME
|
||||
/** Initializes time sync callbacks to support syncing current time to the BedJet. */
|
||||
void setup_time_();
|
||||
optional<time::RealTimeClock *> time_id_{};
|
||||
time::RealTimeClock *time_id_{nullptr};
|
||||
#endif
|
||||
|
||||
uint32_t timeout_{DEFAULT_STATUS_TIMEOUT};
|
||||
|
||||
@ -141,6 +141,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon
|
||||
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
|
||||
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
|
||||
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
|
||||
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
|
||||
|
||||
FILTER_REGISTRY = Registry()
|
||||
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
|
||||
@ -259,6 +260,19 @@ async def lambda_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(filter_id, lambda_)
|
||||
|
||||
|
||||
@register_filter(
|
||||
"settle",
|
||||
SettleFilter,
|
||||
cv.templatable(cv.positive_time_period_milliseconds),
|
||||
)
|
||||
async def settle_filter_to_code(config, filter_id):
|
||||
var = cg.new_Pvariable(filter_id)
|
||||
await cg.register_component(var, {})
|
||||
template_ = await cg.templatable(config, [], cg.uint32)
|
||||
cg.add(var.set_delay(template_))
|
||||
return var
|
||||
|
||||
|
||||
MULTI_CLICK_TIMING_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_STATE): cv.boolean,
|
||||
|
||||
@ -111,6 +111,23 @@ LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move
|
||||
|
||||
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
|
||||
|
||||
optional<bool> SettleFilter::new_value(bool value, bool is_initial) {
|
||||
if (!this->steady_) {
|
||||
this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() {
|
||||
this->steady_ = true;
|
||||
this->output(value, is_initial);
|
||||
});
|
||||
return {};
|
||||
} else {
|
||||
this->steady_ = false;
|
||||
this->output(value, is_initial);
|
||||
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@ -108,6 +108,19 @@ class LambdaFilter : public Filter {
|
||||
std::function<optional<bool>(bool)> f_;
|
||||
};
|
||||
|
||||
class SettleFilter : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value, bool is_initial) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
|
||||
protected:
|
||||
TemplatableValue<uint32_t> delay_{};
|
||||
bool steady_{true};
|
||||
};
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.automation import maybe_simple_id
|
||||
from esphome.components import esp32_ble_tracker, esp32_ble_client
|
||||
from esphome.const import (
|
||||
CONF_CHARACTERISTIC_UUID,
|
||||
@ -15,7 +16,7 @@ from esphome.const import (
|
||||
from esphome import automation
|
||||
|
||||
AUTO_LOAD = ["esp32_ble_client"]
|
||||
CODEOWNERS = ["@buxtronix"]
|
||||
CODEOWNERS = ["@buxtronix", "@clydebarrow"]
|
||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||
|
||||
ble_client_ns = cg.esphome_ns.namespace("ble_client")
|
||||
@ -43,6 +44,10 @@ BLEClientNumericComparisonRequestTrigger = ble_client_ns.class_(
|
||||
|
||||
# Actions
|
||||
BLEWriteAction = ble_client_ns.class_("BLEClientWriteAction", automation.Action)
|
||||
BLEConnectAction = ble_client_ns.class_("BLEClientConnectAction", automation.Action)
|
||||
BLEDisconnectAction = ble_client_ns.class_(
|
||||
"BLEClientDisconnectAction", automation.Action
|
||||
)
|
||||
BLEPasskeyReplyAction = ble_client_ns.class_(
|
||||
"BLEClientPasskeyReplyAction", automation.Action
|
||||
)
|
||||
@ -58,6 +63,7 @@ CONF_ACCEPT = "accept"
|
||||
CONF_ON_PASSKEY_REQUEST = "on_passkey_request"
|
||||
CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification"
|
||||
CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request"
|
||||
CONF_AUTO_CONNECT = "auto_connect"
|
||||
|
||||
# Espressif platformio framework is built with MAX_BLE_CONN to 3, so
|
||||
# enforce this in yaml checks.
|
||||
@ -69,6 +75,7 @@ CONFIG_SCHEMA = (
|
||||
cv.GenerateID(): cv.declare_id(BLEClient),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_NAME): cv.string,
|
||||
cv.Optional(CONF_AUTO_CONNECT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
@ -135,6 +142,12 @@ BLE_WRITE_ACTION_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
BLE_CONNECT_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
|
||||
}
|
||||
)
|
||||
|
||||
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
|
||||
@ -157,6 +170,24 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"ble_client.disconnect", BLEDisconnectAction, BLE_CONNECT_ACTION_SCHEMA
|
||||
)
|
||||
async def ble_disconnect_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"ble_client.connect", BLEConnectAction, BLE_CONNECT_ACTION_SCHEMA
|
||||
)
|
||||
async def ble_connect_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA
|
||||
)
|
||||
@ -261,6 +292,7 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
await esp32_ble_tracker.register_client(var, config)
|
||||
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||
cg.add(var.set_auto_connect(config[CONF_AUTO_CONNECT]))
|
||||
for conf in config.get(CONF_ON_CONNECT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
@ -2,76 +2,10 @@
|
||||
|
||||
#include "automation.h"
|
||||
|
||||
#include <esp_bt_defs.h>
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
static const char *const TAG = "ble_client.automation";
|
||||
|
||||
void BLEWriterClientNode::write(const std::vector<uint8_t> &value) {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "Cannot write to BLE characteristic - not connected");
|
||||
return;
|
||||
} else if (this->ble_char_handle_ == 0) {
|
||||
ESP_LOGW(TAG, "Cannot write to BLE characteristic - characteristic not found");
|
||||
return;
|
||||
}
|
||||
esp_gatt_write_type_t write_type;
|
||||
if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) {
|
||||
write_type = ESP_GATT_WRITE_TYPE_RSP;
|
||||
ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP");
|
||||
} else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) {
|
||||
write_type = ESP_GATT_WRITE_TYPE_NO_RSP;
|
||||
ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str());
|
||||
return;
|
||||
}
|
||||
ESP_LOGVV(TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->ble_char_handle_,
|
||||
value.size(), const_cast<uint8_t *>(value.data()), write_type, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void BLEWriterClientNode::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_REG_EVT:
|
||||
break;
|
||||
case ESP_GATTC_OPEN_EVT:
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
ESP_LOGD(TAG, "Connection established with %s", ble_client_->address_str().c_str());
|
||||
break;
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW("ble_write_action", "Characteristic %s was not found in service %s",
|
||||
this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str());
|
||||
break;
|
||||
}
|
||||
this->ble_char_handle_ = chr->handle;
|
||||
this->char_props_ = chr->properties;
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
|
||||
ble_client_->address_str().c_str());
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_DISCONNECT_EVT:
|
||||
this->node_state = espbt::ClientState::IDLE;
|
||||
this->ble_char_handle_ = 0;
|
||||
ESP_LOGD(TAG, "Disconnected from %s", ble_client_->address_str().c_str());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
const char *const Automation::TAG = "ble_client.automation";
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
|
||||
@ -7,9 +7,19 @@
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
|
||||
// placeholder class for static TAG .
|
||||
class Automation {
|
||||
public:
|
||||
// could be made inline with C++17
|
||||
static const char *const TAG;
|
||||
};
|
||||
|
||||
// implement on_connect automation.
|
||||
class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode {
|
||||
public:
|
||||
explicit BLEClientConnectTrigger(BLEClient *parent) { parent->register_ble_node(this); }
|
||||
@ -23,17 +33,28 @@ class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode {
|
||||
}
|
||||
};
|
||||
|
||||
// on_disconnect automation
|
||||
class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode {
|
||||
public:
|
||||
explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); }
|
||||
void loop() 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 {
|
||||
if (event == ESP_GATTC_DISCONNECT_EVT &&
|
||||
memcmp(param->disconnect.remote_bda, this->parent_->get_remote_bda(), 6) == 0)
|
||||
this->trigger();
|
||||
if (event == ESP_GATTC_SEARCH_CMPL_EVT)
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
// test for CLOSE and not DISCONNECT - DISCONNECT can occur even if no virtual connection (OPEN event) occurred.
|
||||
// So this will not trigger unless a complete open has previously succeeded.
|
||||
switch (event) {
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
this->trigger();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -42,10 +63,8 @@ class BLEClientPasskeyRequestTrigger : public Trigger<>, public BLEClientNode {
|
||||
explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); }
|
||||
void loop() override {}
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override {
|
||||
if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT &&
|
||||
memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) {
|
||||
if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr))
|
||||
this->trigger();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -54,10 +73,8 @@ class BLEClientPasskeyNotificationTrigger : public Trigger<uint32_t>, public BLE
|
||||
explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); }
|
||||
void loop() override {}
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override {
|
||||
if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT &&
|
||||
memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) {
|
||||
uint32_t passkey = param->ble_security.key_notif.passkey;
|
||||
this->trigger(passkey);
|
||||
if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) {
|
||||
this->trigger(param->ble_security.key_notif.passkey);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -67,24 +84,20 @@ class BLEClientNumericComparisonRequestTrigger : public Trigger<uint32_t>, publi
|
||||
explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); }
|
||||
void loop() override {}
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override {
|
||||
if (event == ESP_GAP_BLE_NC_REQ_EVT &&
|
||||
memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) {
|
||||
uint32_t passkey = param->ble_security.key_notif.passkey;
|
||||
this->trigger(passkey);
|
||||
if (event == ESP_GAP_BLE_NC_REQ_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) {
|
||||
this->trigger(param->ble_security.key_notif.passkey);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class BLEWriterClientNode : public BLEClientNode {
|
||||
// implement the ble_client.ble_write action.
|
||||
template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, public BLEClientNode {
|
||||
public:
|
||||
BLEWriterClientNode(BLEClient *ble_client) {
|
||||
BLEClientWriteAction(BLEClient *ble_client) {
|
||||
ble_client->register_ble_node(this);
|
||||
ble_client_ = ble_client;
|
||||
}
|
||||
|
||||
// Attempts to write the contents of value to char_uuid_.
|
||||
void write(const std::vector<uint8_t> &value);
|
||||
|
||||
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
||||
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
@ -93,29 +106,6 @@ class BLEWriterClientNode : public BLEClientNode {
|
||||
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
|
||||
private:
|
||||
BLEClient *ble_client_;
|
||||
int ble_char_handle_ = 0;
|
||||
esp_gatt_char_prop_t char_props_;
|
||||
espbt::ESPBTUUID service_uuid_;
|
||||
espbt::ESPBTUUID char_uuid_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, public BLEWriterClientNode {
|
||||
public:
|
||||
BLEClientWriteAction(BLEClient *ble_client) : BLEWriterClientNode(ble_client) {}
|
||||
|
||||
void play(Ts... x) override {
|
||||
if (has_simple_value_) {
|
||||
return write(this->value_simple_);
|
||||
} else {
|
||||
return write(this->value_template_(x...));
|
||||
}
|
||||
}
|
||||
|
||||
void set_value_template(std::function<std::vector<uint8_t>(Ts...)> func) {
|
||||
this->value_template_ = std::move(func);
|
||||
has_simple_value_ = false;
|
||||
@ -126,10 +116,94 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
has_simple_value_ = true;
|
||||
}
|
||||
|
||||
void play(Ts... x) override {}
|
||||
|
||||
void play_complex(Ts... x) override {
|
||||
this->num_running_++;
|
||||
this->var_ = std::make_tuple(x...);
|
||||
auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...);
|
||||
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
||||
if (!write(value))
|
||||
this->play_next_(x...);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note about logging: the esph_log_X macros are used here because the CI checks complain about use of the ESP LOG
|
||||
* macros in header files (Can't even write it in a comment!)
|
||||
* Not sure why, because they seem to work just fine.
|
||||
* The problem is that the implementation of a templated class can't be placed in a .cpp file when using C++ less than
|
||||
* 17, so the methods have to be here. The esph_log_X macros are equivalent in function, but don't trigger the CI
|
||||
* errors.
|
||||
*/
|
||||
// initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
|
||||
bool write(const std::vector<uint8_t> &value) {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
|
||||
return false;
|
||||
}
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
|
||||
esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(),
|
||||
this->char_handle_, value.size(), const_cast<uint8_t *>(value.data()),
|
||||
this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ESP_OK) {
|
||||
esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override {
|
||||
switch (event) {
|
||||
case ESP_GATTC_WRITE_CHAR_EVT:
|
||||
// upstream code checked the MAC address, verify the characteristic.
|
||||
if (param->write.handle == this->char_handle_)
|
||||
this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); });
|
||||
break;
|
||||
case ESP_GATTC_DISCONNECT_EVT:
|
||||
if (this->num_running_ != 0)
|
||||
this->stop_complex();
|
||||
break;
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
|
||||
if (chr == nullptr) {
|
||||
esph_log_w("ble_write_action", "Characteristic %s was not found in service %s",
|
||||
this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str());
|
||||
break;
|
||||
}
|
||||
this->char_handle_ = chr->handle;
|
||||
this->char_props_ = chr->properties;
|
||||
if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) {
|
||||
this->write_type_ = ESP_GATT_WRITE_TYPE_RSP;
|
||||
esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP");
|
||||
} else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) {
|
||||
this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP;
|
||||
esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
|
||||
} else {
|
||||
esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str());
|
||||
break;
|
||||
}
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
|
||||
ble_client_->address_str().c_str());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
BLEClient *ble_client_;
|
||||
bool has_simple_value_ = true;
|
||||
std::vector<uint8_t> value_simple_;
|
||||
std::function<std::vector<uint8_t>(Ts...)> value_template_{};
|
||||
espbt::ESPBTUUID service_uuid_;
|
||||
espbt::ESPBTUUID char_uuid_;
|
||||
std::tuple<Ts...> var_{};
|
||||
uint16_t char_handle_{};
|
||||
esp_gatt_char_prop_t char_props_{};
|
||||
esp_gatt_write_type_t write_type_{};
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...> {
|
||||
@ -212,6 +286,92 @@ template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...>
|
||||
BLEClient *parent_{nullptr};
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEClientConnectAction : public Action<Ts...>, public BLEClientNode {
|
||||
public:
|
||||
BLEClientConnectAction(BLEClient *ble_client) {
|
||||
ble_client->register_ble_node(this);
|
||||
ble_client_ = ble_client;
|
||||
}
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override {
|
||||
if (this->num_running_ == 0)
|
||||
return;
|
||||
switch (event) {
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT:
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); });
|
||||
break;
|
||||
// if the connection is closed, terminate the automation chain.
|
||||
case ESP_GATTC_DISCONNECT_EVT:
|
||||
this->stop_complex();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// not used since we override play_complex_
|
||||
void play(Ts... x) override {}
|
||||
|
||||
void play_complex(Ts... x) override {
|
||||
// it makes no sense to have multiple instances of this running at the same time.
|
||||
// this would occur only if the same automation was re-triggered while still
|
||||
// running. So just cancel the second chain if this is detected.
|
||||
if (this->num_running_ != 0) {
|
||||
this->stop_complex();
|
||||
return;
|
||||
}
|
||||
this->num_running_++;
|
||||
if (this->node_state == espbt::ClientState::ESTABLISHED) {
|
||||
this->play_next_(x...);
|
||||
} else {
|
||||
this->var_ = std::make_tuple(x...);
|
||||
this->ble_client_->connect();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
BLEClient *ble_client_;
|
||||
std::tuple<Ts...> var_{};
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEClientDisconnectAction : public Action<Ts...>, public BLEClientNode {
|
||||
public:
|
||||
BLEClientDisconnectAction(BLEClient *ble_client) {
|
||||
ble_client->register_ble_node(this);
|
||||
ble_client_ = ble_client;
|
||||
}
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override {
|
||||
if (this->num_running_ == 0)
|
||||
return;
|
||||
switch (event) {
|
||||
case ESP_GATTC_CLOSE_EVT:
|
||||
case ESP_GATTC_DISCONNECT_EVT:
|
||||
this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// not used since we override play_complex_
|
||||
void play(Ts... x) override {}
|
||||
|
||||
void play_complex(Ts... x) override {
|
||||
this->num_running_++;
|
||||
if (this->node_state == espbt::ClientState::IDLE) {
|
||||
this->play_next_(x...);
|
||||
} else {
|
||||
this->var_ = std::make_tuple(x...);
|
||||
this->ble_client_->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
BLEClient *ble_client_;
|
||||
std::tuple<Ts...> var_{};
|
||||
};
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ void BLEClient::loop() {
|
||||
void BLEClient::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BLE Client:");
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Auto-Connect: %s", TRUEFALSE(this->auto_connect_));
|
||||
}
|
||||
|
||||
bool BLEClient::parse_device(const espbt::ESPBTDevice &device) {
|
||||
@ -37,31 +38,24 @@ bool BLEClient::parse_device(const espbt::ESPBTDevice &device) {
|
||||
void BLEClient::set_enabled(bool enabled) {
|
||||
if (enabled == this->enabled)
|
||||
return;
|
||||
if (!enabled && this->state() != espbt::ClientState::IDLE) {
|
||||
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str());
|
||||
auto ret = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
|
||||
if (ret) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s status=%d", this->address_str().c_str(), ret);
|
||||
}
|
||||
}
|
||||
this->enabled = enabled;
|
||||
if (!enabled) {
|
||||
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str());
|
||||
this->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
bool BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
bool all_established = this->all_nodes_established_();
|
||||
|
||||
if (!BLEClientBase::gattc_event_handler(event, esp_gattc_if, param))
|
||||
return false;
|
||||
|
||||
for (auto *node : this->nodes_)
|
||||
node->gattc_event_handler(event, esp_gattc_if, param);
|
||||
|
||||
// Delete characteristics after clients have used them to save RAM.
|
||||
if (!all_established && this->all_nodes_established_()) {
|
||||
for (auto &svc : this->services_)
|
||||
delete svc; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
this->services_.clear();
|
||||
if (!this->services_.empty() && this->all_nodes_established_()) {
|
||||
this->release_services();
|
||||
ESP_LOGD(TAG, "All clients established, services released");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -19,26 +19,36 @@ void BLEBinaryOutput::dump_config() {
|
||||
void BLEBinaryOutput::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:
|
||||
this->client_state_ = espbt::ClientState::ESTABLISHED;
|
||||
ESP_LOGW(TAG, "[%s] Connected successfully!", this->char_uuid_.to_string().c_str());
|
||||
break;
|
||||
case ESP_GATTC_DISCONNECT_EVT:
|
||||
ESP_LOGW(TAG, "[%s] Disconnected", this->char_uuid_.to_string().c_str());
|
||||
this->client_state_ = espbt::ClientState::IDLE;
|
||||
break;
|
||||
case ESP_GATTC_WRITE_CHAR_EVT: {
|
||||
if (param->write.status == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG, "[%s] Characteristic not found.", this->char_uuid_.to_string().c_str());
|
||||
ESP_LOGW(TAG, "Characteristic %s was not found in service %s", this->char_uuid_.to_string().c_str(),
|
||||
this->service_uuid_.to_string().c_str());
|
||||
break;
|
||||
}
|
||||
if (param->write.handle == chr->handle) {
|
||||
ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_string().c_str(), param->write.status);
|
||||
this->char_handle_ = chr->handle;
|
||||
this->char_props_ = chr->properties;
|
||||
if (this->require_response_ && this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) {
|
||||
this->write_type_ = ESP_GATT_WRITE_TYPE_RSP;
|
||||
ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP");
|
||||
} else if (!this->require_response_ && this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) {
|
||||
this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP;
|
||||
ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Characteristic %s does not allow writing with%s response", this->char_uuid_.to_string().c_str(),
|
||||
this->require_response_ ? "" : "out");
|
||||
break;
|
||||
}
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
|
||||
this->parent()->address_str().c_str());
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_WRITE_CHAR_EVT: {
|
||||
if (param->write.handle == this->char_handle_) {
|
||||
if (param->write.status != 0)
|
||||
ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_string().c_str(), param->write.status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -48,26 +58,18 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
|
||||
}
|
||||
|
||||
void BLEBinaryOutput::write_state(bool state) {
|
||||
if (this->client_state_ != espbt::ClientState::ESTABLISHED) {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.",
|
||||
this->char_uuid_.to_string().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG, "[%s] Characteristic not found. State update can not be written.",
|
||||
this->char_uuid_.to_string().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t state_as_uint = (uint8_t) state;
|
||||
ESP_LOGV(TAG, "[%s] Write State: %d", this->char_uuid_.to_string().c_str(), state_as_uint);
|
||||
if (this->require_response_) {
|
||||
chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_RSP);
|
||||
} else {
|
||||
chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_NO_RSP);
|
||||
}
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_,
|
||||
sizeof(state_as_uint), &state_as_uint, this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ESP_GATT_OK)
|
||||
ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_string().c_str(), err);
|
||||
}
|
||||
|
||||
} // namespace ble_client
|
||||
|
||||
@ -32,7 +32,9 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi
|
||||
bool require_response_;
|
||||
espbt::ESPBTUUID service_uuid_;
|
||||
espbt::ESPBTUUID char_uuid_;
|
||||
espbt::ClientState client_state_;
|
||||
uint16_t char_handle_{};
|
||||
esp_gatt_char_prop_t char_props_{};
|
||||
esp_gatt_write_type_t write_type_{};
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
|
||||
@ -14,15 +14,17 @@ class BLESensorNotifyTrigger : public Trigger<float>, public BLESensor {
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override {
|
||||
switch (event) {
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
this->sensor_->node_state = espbt::ClientState::ESTABLISHED;
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.handle == this->sensor_->handle)
|
||||
this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len));
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.conn_id != this->sensor_->parent()->get_conn_id() ||
|
||||
param->notify.handle != this->sensor_->handle)
|
||||
break;
|
||||
this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len));
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
// confirms notifications are being listened for. While enabling of notifications may still be in
|
||||
// progress by the parent, we assume it will happen.
|
||||
if (param->reg_for_notify.status == ESP_GATT_OK && param->reg_for_notify.handle == this->sensor_->handle)
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
|
||||
@ -22,26 +22,19 @@ void BLEClientRSSISensor::dump_config() {
|
||||
void BLEClientRSSISensor::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, "[%s] Connected successfully!", this->get_name().c_str());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str());
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT:
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
if (this->should_update_) {
|
||||
this->should_update_ = false;
|
||||
this->get_rssi_();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str());
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
@ -74,8 +74,6 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
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;
|
||||
@ -87,15 +85,23 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle)
|
||||
break;
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
|
||||
ESP_LOGD(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
|
||||
param->notify.handle, param->notify.value[0]);
|
||||
if (param->notify.handle != this->handle)
|
||||
break;
|
||||
this->publish_state(this->parse_data_(param->notify.value, param->notify.value_len));
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
if (param->reg_for_notify.handle == this->handle) {
|
||||
if (param->reg_for_notify.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "Error registering for notifications at handle %d, status=%d", param->reg_for_notify.handle,
|
||||
param->reg_for_notify.status);
|
||||
break;
|
||||
}
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
ESP_LOGD(TAG, "Register for notify on %s complete", this->char_uuid_.to_string().c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
@ -17,14 +17,11 @@ void BLEClientSwitch::write_state(bool state) {
|
||||
void BLEClientSwitch::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_REG_EVT:
|
||||
case ESP_GATTC_CLOSE_EVT:
|
||||
this->publish_state(this->parent_->enabled);
|
||||
break;
|
||||
case ESP_GATTC_OPEN_EVT:
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT:
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
case ESP_GATTC_DISCONNECT_EVT:
|
||||
this->node_state = espbt::ClientState::IDLE;
|
||||
this->publish_state(this->parent_->enabled);
|
||||
break;
|
||||
default:
|
||||
|
||||
@ -36,8 +36,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str());
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
this->status_set_warning();
|
||||
this->publish_state(EMPTY);
|
||||
break;
|
||||
@ -77,20 +76,18 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
this->publish_state(this->parse_data(param->read.value, param->read.value_len));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle)
|
||||
if (param->notify.handle != this->handle)
|
||||
break;
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
|
||||
param->notify.handle, param->notify.value[0]);
|
||||
@ -98,7 +95,8 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
if (param->reg_for_notify.status == ESP_GATT_OK && param->reg_for_notify.handle == this->handle)
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
@ -1,116 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_IIR_FILTER,
|
||||
CONF_OVERSAMPLING,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_HECTOPASCAL,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
bme280_ns = cg.esphome_ns.namespace("bme280")
|
||||
BME280Oversampling = bme280_ns.enum("BME280Oversampling")
|
||||
OVERSAMPLING_OPTIONS = {
|
||||
"NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE,
|
||||
"1X": BME280Oversampling.BME280_OVERSAMPLING_1X,
|
||||
"2X": BME280Oversampling.BME280_OVERSAMPLING_2X,
|
||||
"4X": BME280Oversampling.BME280_OVERSAMPLING_4X,
|
||||
"8X": BME280Oversampling.BME280_OVERSAMPLING_8X,
|
||||
"16X": BME280Oversampling.BME280_OVERSAMPLING_16X,
|
||||
}
|
||||
|
||||
BME280IIRFilter = bme280_ns.enum("BME280IIRFilter")
|
||||
IIR_FILTER_OPTIONS = {
|
||||
"OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF,
|
||||
"2X": BME280IIRFilter.BME280_IIR_FILTER_2X,
|
||||
"4X": BME280IIRFilter.BME280_IIR_FILTER_4X,
|
||||
"8X": BME280IIRFilter.BME280_IIR_FILTER_8X,
|
||||
"16X": BME280IIRFilter.BME280_IIR_FILTER_16X,
|
||||
}
|
||||
|
||||
BME280Component = bme280_ns.class_(
|
||||
"BME280Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BME280Component),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
|
||||
OVERSAMPLING_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
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,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
|
||||
OVERSAMPLING_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
|
||||
OVERSAMPLING_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
|
||||
IIR_FILTER_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x77))
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temperature_config)
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING]))
|
||||
|
||||
if pressure_config := config.get(CONF_PRESSURE):
|
||||
sens = await sensor.new_sensor(pressure_config)
|
||||
cg.add(var.set_pressure_sensor(sens))
|
||||
cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
|
||||
|
||||
if humidity_config := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(humidity_config)
|
||||
cg.add(var.set_humidity_sensor(sens))
|
||||
cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING]))
|
||||
|
||||
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))
|
||||
1
esphome/components/bme280_base/__init__.py
Normal file
1
esphome/components/bme280_base/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@ -1,9 +1,14 @@
|
||||
#include "bme280.h"
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
#include "bme280_base.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <esphome/components/sensor/sensor.h>
|
||||
#include <esphome/core/component.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace bme280 {
|
||||
namespace bme280_base {
|
||||
|
||||
static const char *const TAG = "bme280.sensor";
|
||||
|
||||
@ -46,7 +51,24 @@ static const uint8_t BME280_STATUS_IM_UPDATE = 0b01;
|
||||
|
||||
inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); }
|
||||
|
||||
static const char *oversampling_to_str(BME280Oversampling oversampling) {
|
||||
const char *iir_filter_to_str(BME280IIRFilter filter) { // NOLINT
|
||||
switch (filter) {
|
||||
case BME280_IIR_FILTER_OFF:
|
||||
return "OFF";
|
||||
case BME280_IIR_FILTER_2X:
|
||||
return "2x";
|
||||
case BME280_IIR_FILTER_4X:
|
||||
return "4x";
|
||||
case BME280_IIR_FILTER_8X:
|
||||
return "8x";
|
||||
case BME280_IIR_FILTER_16X:
|
||||
return "16x";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
const char *oversampling_to_str(BME280Oversampling oversampling) { // NOLINT
|
||||
switch (oversampling) {
|
||||
case BME280_OVERSAMPLING_NONE:
|
||||
return "None";
|
||||
@ -65,23 +87,6 @@ static const char *oversampling_to_str(BME280Oversampling oversampling) {
|
||||
}
|
||||
}
|
||||
|
||||
static const char *iir_filter_to_str(BME280IIRFilter filter) {
|
||||
switch (filter) {
|
||||
case BME280_IIR_FILTER_OFF:
|
||||
return "OFF";
|
||||
case BME280_IIR_FILTER_2X:
|
||||
return "2x";
|
||||
case BME280_IIR_FILTER_4X:
|
||||
return "4x";
|
||||
case BME280_IIR_FILTER_8X:
|
||||
return "8x";
|
||||
case BME280_IIR_FILTER_16X:
|
||||
return "16x";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
void BME280Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up BME280...");
|
||||
uint8_t chip_id = 0;
|
||||
@ -112,7 +117,7 @@ void BME280Component::setup() {
|
||||
// Wait until the NVM data has finished loading.
|
||||
uint8_t status;
|
||||
uint8_t retry = 5;
|
||||
do {
|
||||
do { // NOLINT
|
||||
delay(2);
|
||||
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
|
||||
ESP_LOGW(TAG, "Error reading status register.");
|
||||
@ -175,7 +180,6 @@ void BME280Component::setup() {
|
||||
}
|
||||
void BME280Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BME280:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
switch (this->error_code_) {
|
||||
case COMMUNICATION_FAILED:
|
||||
ESP_LOGE(TAG, "Communication with BME280 failed!");
|
||||
@ -226,14 +230,14 @@ void BME280Component::update() {
|
||||
return;
|
||||
}
|
||||
int32_t t_fine = 0;
|
||||
float temperature = this->read_temperature_(data, &t_fine);
|
||||
float const temperature = this->read_temperature_(data, &t_fine);
|
||||
if (std::isnan(temperature)) {
|
||||
ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values.");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
float pressure = this->read_pressure_(data, t_fine);
|
||||
float humidity = this->read_humidity_(data, t_fine);
|
||||
float const pressure = this->read_pressure_(data, t_fine);
|
||||
float const humidity = this->read_humidity_(data, t_fine);
|
||||
|
||||
ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
@ -257,12 +261,12 @@ float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) {
|
||||
const int32_t t2 = this->calibration_.t2;
|
||||
const int32_t t3 = this->calibration_.t3;
|
||||
|
||||
int32_t var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11;
|
||||
int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14;
|
||||
int32_t const var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11;
|
||||
int32_t const var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14;
|
||||
*t_fine = var1 + var2;
|
||||
|
||||
float temperature = (*t_fine * 5 + 128) >> 8;
|
||||
return temperature / 100.0f;
|
||||
float const temperature = (*t_fine * 5 + 128);
|
||||
return temperature / 25600.0f;
|
||||
}
|
||||
|
||||
float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) {
|
||||
@ -303,11 +307,11 @@ float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) {
|
||||
}
|
||||
|
||||
float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) {
|
||||
uint16_t raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF);
|
||||
uint16_t const raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF);
|
||||
if (raw_adc == 0x8000)
|
||||
return NAN;
|
||||
|
||||
int32_t adc = raw_adc;
|
||||
int32_t const adc = raw_adc;
|
||||
|
||||
const int32_t h1 = this->calibration_.h1;
|
||||
const int32_t h2 = this->calibration_.h2;
|
||||
@ -325,7 +329,7 @@ float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) {
|
||||
|
||||
v_x1_u32r = v_x1_u32r < 0 ? 0 : v_x1_u32r;
|
||||
v_x1_u32r = v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r;
|
||||
float h = v_x1_u32r >> 12;
|
||||
float const h = v_x1_u32r >> 12;
|
||||
|
||||
return h / 1024.0f;
|
||||
}
|
||||
@ -351,5 +355,5 @@ uint16_t BME280Component::read_u16_le_(uint8_t a_register) {
|
||||
}
|
||||
int16_t BME280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); }
|
||||
|
||||
} // namespace bme280
|
||||
} // namespace bme280_base
|
||||
} // namespace esphome
|
||||
@ -2,10 +2,9 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bme280 {
|
||||
namespace bme280_base {
|
||||
|
||||
/// Internal struct storing the calibration values of an BME280.
|
||||
struct BME280CalibrationData {
|
||||
@ -57,8 +56,8 @@ enum BME280IIRFilter {
|
||||
BME280_IIR_FILTER_16X = 0b100,
|
||||
};
|
||||
|
||||
/// This class implements support for the BME280 Temperature+Pressure+Humidity i2c sensor.
|
||||
class BME280Component : public PollingComponent, public i2c::I2CDevice {
|
||||
/// This class implements support for the BME280 Temperature+Pressure+Humidity sensor.
|
||||
class BME280Component : public PollingComponent {
|
||||
public:
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
|
||||
@ -91,6 +90,11 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice {
|
||||
uint16_t read_u16_le_(uint8_t a_register);
|
||||
int16_t read_s16_le_(uint8_t a_register);
|
||||
|
||||
virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0;
|
||||
virtual bool write_byte(uint8_t a_register, uint8_t data) = 0;
|
||||
virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
|
||||
virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0;
|
||||
|
||||
BME280CalibrationData calibration_;
|
||||
BME280Oversampling temperature_oversampling_{BME280_OVERSAMPLING_16X};
|
||||
BME280Oversampling pressure_oversampling_{BME280_OVERSAMPLING_16X};
|
||||
@ -106,5 +110,5 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice {
|
||||
} error_code_{NONE};
|
||||
};
|
||||
|
||||
} // namespace bme280
|
||||
} // namespace bme280_base
|
||||
} // namespace esphome
|
||||
106
esphome/components/bme280_base/sensor.py
Normal file
106
esphome/components/bme280_base/sensor.py
Normal file
@ -0,0 +1,106 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_IIR_FILTER,
|
||||
CONF_OVERSAMPLING,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_HECTOPASCAL,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
bme280_ns = cg.esphome_ns.namespace("bme280_base")
|
||||
BME280Oversampling = bme280_ns.enum("BME280Oversampling")
|
||||
OVERSAMPLING_OPTIONS = {
|
||||
"NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE,
|
||||
"1X": BME280Oversampling.BME280_OVERSAMPLING_1X,
|
||||
"2X": BME280Oversampling.BME280_OVERSAMPLING_2X,
|
||||
"4X": BME280Oversampling.BME280_OVERSAMPLING_4X,
|
||||
"8X": BME280Oversampling.BME280_OVERSAMPLING_8X,
|
||||
"16X": BME280Oversampling.BME280_OVERSAMPLING_16X,
|
||||
}
|
||||
|
||||
BME280IIRFilter = bme280_ns.enum("BME280IIRFilter")
|
||||
IIR_FILTER_OPTIONS = {
|
||||
"OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF,
|
||||
"2X": BME280IIRFilter.BME280_IIR_FILTER_2X,
|
||||
"4X": BME280IIRFilter.BME280_IIR_FILTER_4X,
|
||||
"8X": BME280IIRFilter.BME280_IIR_FILTER_8X,
|
||||
"16X": BME280IIRFilter.BME280_IIR_FILTER_16X,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA_BASE = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
|
||||
OVERSAMPLING_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
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,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
|
||||
OVERSAMPLING_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
|
||||
OVERSAMPLING_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
|
||||
IIR_FILTER_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
).extend(cv.polling_component_schema("60s"))
|
||||
|
||||
|
||||
async def to_code(config, func=None):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
if func is not None:
|
||||
await func(var, config)
|
||||
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temperature_config)
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING]))
|
||||
|
||||
if pressure_config := config.get(CONF_PRESSURE):
|
||||
sens = await sensor.new_sensor(pressure_config)
|
||||
cg.add(var.set_pressure_sensor(sens))
|
||||
cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
|
||||
|
||||
if humidity_config := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(humidity_config)
|
||||
cg.add(var.set_humidity_sensor(sens))
|
||||
cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING]))
|
||||
|
||||
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))
|
||||
30
esphome/components/bme280_i2c/bme280_i2c.cpp
Normal file
30
esphome/components/bme280_i2c/bme280_i2c.cpp
Normal file
@ -0,0 +1,30 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "bme280_i2c.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "../bme280_base/bme280_base.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bme280_i2c {
|
||||
|
||||
bool BME280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) {
|
||||
return I2CDevice::read_byte(a_register, data);
|
||||
};
|
||||
bool BME280I2CComponent::write_byte(uint8_t a_register, uint8_t data) {
|
||||
return I2CDevice::write_byte(a_register, data);
|
||||
};
|
||||
bool BME280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
|
||||
return I2CDevice::read_bytes(a_register, data, len);
|
||||
};
|
||||
bool BME280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
|
||||
return I2CDevice::read_byte_16(a_register, data);
|
||||
};
|
||||
|
||||
void BME280I2CComponent::dump_config() {
|
||||
LOG_I2C_DEVICE(this);
|
||||
BME280Component::dump_config();
|
||||
}
|
||||
|
||||
} // namespace bme280_i2c
|
||||
} // namespace esphome
|
||||
20
esphome/components/bme280_i2c/bme280_i2c.h
Normal file
20
esphome/components/bme280_i2c/bme280_i2c.h
Normal file
@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/bme280_base/bme280_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bme280_i2c {
|
||||
|
||||
static const char *const TAG = "bme280_i2c.sensor";
|
||||
|
||||
class BME280I2CComponent : public esphome::bme280_base::BME280Component, public i2c::I2CDevice {
|
||||
bool read_byte(uint8_t a_register, uint8_t *data) override;
|
||||
bool write_byte(uint8_t a_register, uint8_t data) override;
|
||||
bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
|
||||
bool read_byte_16(uint8_t a_register, uint16_t *data) override;
|
||||
void dump_config() override;
|
||||
};
|
||||
|
||||
} // namespace bme280_i2c
|
||||
} // namespace esphome
|
||||
19
esphome/components/bme280_i2c/sensor.py
Normal file
19
esphome/components/bme280_i2c/sensor.py
Normal file
@ -0,0 +1,19 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from ..bme280_base.sensor import to_code as to_code_base, cv, CONFIG_SCHEMA_BASE
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["bme280_base"]
|
||||
|
||||
bme280_ns = cg.esphome_ns.namespace("bme280_i2c")
|
||||
BME280I2CComponent = bme280_ns.class_(
|
||||
"BME280I2CComponent", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
|
||||
i2c.i2c_device_schema(default_address=0x77)
|
||||
).extend({cv.GenerateID(): cv.declare_id(BME280I2CComponent)})
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
await to_code_base(config, func=i2c.register_i2c_device)
|
||||
1
esphome/components/bme280_spi/__init__.py
Normal file
1
esphome/components/bme280_spi/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@apbodrov"]
|
||||
66
esphome/components/bme280_spi/bme280_spi.cpp
Normal file
66
esphome/components/bme280_spi/bme280_spi.cpp
Normal file
@ -0,0 +1,66 @@
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
#include "bme280_spi.h"
|
||||
#include <esphome/components/bme280_base/bme280_base.h>
|
||||
|
||||
int set_bit(uint8_t num, int position) {
|
||||
int mask = 1 << position;
|
||||
return num | mask;
|
||||
}
|
||||
|
||||
int clear_bit(uint8_t num, int position) {
|
||||
int mask = 1 << position;
|
||||
return num & ~mask;
|
||||
}
|
||||
|
||||
namespace esphome {
|
||||
namespace bme280_spi {
|
||||
|
||||
void BME280SPIComponent::setup() {
|
||||
this->spi_setup();
|
||||
BME280Component::setup();
|
||||
};
|
||||
|
||||
// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not used
|
||||
// and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read).
|
||||
// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the byte
|
||||
// 0x77 is transferred, for read access, the byte 0xF7 is transferred.
|
||||
// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf
|
||||
|
||||
bool BME280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) {
|
||||
this->enable();
|
||||
// cause: *data = this->delegate_->transfer(tmp) doesnt work
|
||||
this->delegate_->transfer(set_bit(a_register, 7));
|
||||
*data = this->delegate_->transfer(0);
|
||||
this->disable();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BME280SPIComponent::write_byte(uint8_t a_register, uint8_t data) {
|
||||
this->enable();
|
||||
this->delegate_->transfer(clear_bit(a_register, 7));
|
||||
this->delegate_->transfer(data);
|
||||
this->disable();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BME280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
|
||||
this->enable();
|
||||
this->delegate_->transfer(set_bit(a_register, 7));
|
||||
this->delegate_->read_array(data, len);
|
||||
this->disable();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BME280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
|
||||
this->enable();
|
||||
this->delegate_->transfer(set_bit(a_register, 7));
|
||||
((uint8_t *) data)[1] = this->delegate_->transfer(0);
|
||||
((uint8_t *) data)[0] = this->delegate_->transfer(0);
|
||||
this->disable();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace bme280_spi
|
||||
} // namespace esphome
|
||||
20
esphome/components/bme280_spi/bme280_spi.h
Normal file
20
esphome/components/bme280_spi/bme280_spi.h
Normal file
@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/bme280_base/bme280_base.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bme280_spi {
|
||||
|
||||
class BME280SPIComponent : public esphome::bme280_base::BME280Component,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_200KHZ> {
|
||||
void setup() override;
|
||||
bool read_byte(uint8_t a_register, uint8_t *data) override;
|
||||
bool write_byte(uint8_t a_register, uint8_t data) override;
|
||||
bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
|
||||
bool read_byte_16(uint8_t a_register, uint16_t *data) override;
|
||||
};
|
||||
|
||||
} // namespace bme280_spi
|
||||
} // namespace esphome
|
||||
24
esphome/components/bme280_spi/sensor.py
Normal file
24
esphome/components/bme280_spi/sensor.py
Normal file
@ -0,0 +1,24 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
from esphome.components.bme280_base.sensor import (
|
||||
to_code as to_code_base,
|
||||
cv,
|
||||
CONFIG_SCHEMA_BASE,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
AUTO_LOAD = ["bme280_base"]
|
||||
|
||||
|
||||
bme280_spi_ns = cg.esphome_ns.namespace("bme280_spi")
|
||||
BME280SPIComponent = bme280_spi_ns.class_(
|
||||
"BME280SPIComponent", cg.PollingComponent, spi.SPIDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema()).extend(
|
||||
{cv.GenerateID(): cv.declare_id(BME280SPIComponent)}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
await to_code_base(config, func=spi.register_spi_device)
|
||||
@ -200,8 +200,8 @@ float BMP280Component::read_temperature_(int32_t *t_fine) {
|
||||
int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14;
|
||||
*t_fine = var1 + var2;
|
||||
|
||||
float temperature = (*t_fine * 5 + 128) >> 8;
|
||||
return temperature / 100.0f;
|
||||
float temperature = (*t_fine * 5 + 128);
|
||||
return temperature / 25600.0f;
|
||||
}
|
||||
|
||||
float BMP280Component::read_pressure_(int32_t t_fine) {
|
||||
|
||||
0
esphome/components/combination/__init__.py
Normal file
0
esphome/components/combination/__init__.py
Normal file
262
esphome/components/combination/combination.cpp
Normal file
262
esphome/components/combination/combination.cpp
Normal file
@ -0,0 +1,262 @@
|
||||
#include "combination.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace combination {
|
||||
|
||||
static const char *const TAG = "combination";
|
||||
|
||||
void CombinationComponent::log_config_(const LogString *combo_type) {
|
||||
LOG_SENSOR("", "Combination Sensor:", this);
|
||||
ESP_LOGCONFIG(TAG, " Combination Type: %s", LOG_STR_ARG(combo_type));
|
||||
this->log_source_sensors();
|
||||
}
|
||||
|
||||
void CombinationNoParameterComponent::add_source(Sensor *sensor) { this->sensors_.emplace_back(sensor); }
|
||||
|
||||
void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function<float(float)> const &stddev) {
|
||||
this->sensor_pairs_.emplace_back(sensor, stddev);
|
||||
}
|
||||
|
||||
void CombinationOneParameterComponent::add_source(Sensor *sensor, float stddev) {
|
||||
this->add_source(sensor, std::function<float(float)>{[stddev](float x) -> float { return stddev; }});
|
||||
}
|
||||
|
||||
void CombinationNoParameterComponent::log_source_sensors() {
|
||||
ESP_LOGCONFIG(TAG, " Source Sensors:");
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
ESP_LOGCONFIG(TAG, " - %s", sensor->get_name().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void CombinationOneParameterComponent::log_source_sensors() {
|
||||
ESP_LOGCONFIG(TAG, " Source Sensors:");
|
||||
for (const auto &sensor : this->sensor_pairs_) {
|
||||
auto &entity = *sensor.first;
|
||||
ESP_LOGCONFIG(TAG, " - %s", entity.get_name().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void CombinationNoParameterComponent::setup() {
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
// All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result
|
||||
// repeatedly in the same loop if multiple source senors update.
|
||||
sensor->add_on_state_callback(
|
||||
[this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); });
|
||||
}
|
||||
}
|
||||
|
||||
void KalmanCombinationComponent::dump_config() {
|
||||
this->log_config_(LOG_STR("kalman"));
|
||||
ESP_LOGCONFIG(TAG, " Update variance: %f per ms", this->update_variance_value_);
|
||||
|
||||
if (this->std_dev_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Standard Deviation Sensor:", this->std_dev_sensor_);
|
||||
}
|
||||
}
|
||||
|
||||
void KalmanCombinationComponent::setup() {
|
||||
for (const auto &sensor : this->sensor_pairs_) {
|
||||
const auto stddev = sensor.second;
|
||||
sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); });
|
||||
}
|
||||
}
|
||||
|
||||
void KalmanCombinationComponent::update_variance_() {
|
||||
uint32_t now = millis();
|
||||
|
||||
// Variance increases by update_variance_ each millisecond
|
||||
auto dt = now - this->last_update_;
|
||||
auto dv = this->update_variance_value_ * dt;
|
||||
this->variance_ += dv;
|
||||
this->last_update_ = now;
|
||||
}
|
||||
|
||||
void KalmanCombinationComponent::correct_(float value, float stddev) {
|
||||
if (std::isnan(value) || std::isinf(stddev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::isnan(this->state_) || std::isinf(this->variance_)) {
|
||||
this->state_ = value;
|
||||
this->variance_ = stddev * stddev;
|
||||
if (this->std_dev_sensor_ != nullptr) {
|
||||
this->std_dev_sensor_->publish_state(stddev);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this->update_variance_();
|
||||
|
||||
// Combine two gaussian distributions mu1+-var1, mu2+-var2 to a new one around mu
|
||||
// Use the value with the smaller variance as mu1 to prevent precision errors
|
||||
const bool this_first = this->variance_ < (stddev * stddev);
|
||||
const float mu1 = this_first ? this->state_ : value;
|
||||
const float mu2 = this_first ? value : this->state_;
|
||||
|
||||
const float var1 = this_first ? this->variance_ : stddev * stddev;
|
||||
const float var2 = this_first ? stddev * stddev : this->variance_;
|
||||
|
||||
const float mu = mu1 + var1 * (mu2 - mu1) / (var1 + var2);
|
||||
const float var = var1 - (var1 * var1) / (var1 + var2);
|
||||
|
||||
// Update and publish state
|
||||
this->state_ = mu;
|
||||
this->variance_ = var;
|
||||
|
||||
this->publish_state(mu);
|
||||
if (this->std_dev_sensor_ != nullptr) {
|
||||
this->std_dev_sensor_->publish_state(std::sqrt(var));
|
||||
}
|
||||
}
|
||||
|
||||
void LinearCombinationComponent::setup() {
|
||||
for (const auto &sensor : this->sensor_pairs_) {
|
||||
// All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result
|
||||
// repeatedly in the same loop if multiple source senors update.
|
||||
sensor.first->add_on_state_callback(
|
||||
[this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); });
|
||||
}
|
||||
}
|
||||
|
||||
void LinearCombinationComponent::handle_new_value(float value) {
|
||||
// Multiplies each sensor state by a configured coeffecient and then sums
|
||||
|
||||
if (!std::isfinite(value))
|
||||
return;
|
||||
|
||||
float sum = 0.0;
|
||||
|
||||
for (const auto &sensor : this->sensor_pairs_) {
|
||||
const float sensor_state = sensor.first->state;
|
||||
if (std::isfinite(sensor_state)) {
|
||||
sum += sensor_state * sensor.second(sensor_state);
|
||||
}
|
||||
}
|
||||
|
||||
this->publish_state(sum);
|
||||
};
|
||||
|
||||
void MaximumCombinationComponent::handle_new_value(float value) {
|
||||
if (!std::isfinite(value))
|
||||
return;
|
||||
|
||||
float max_value = (-1) * std::numeric_limits<float>::infinity(); // note x = max(x, -infinity)
|
||||
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
if (std::isfinite(sensor->state)) {
|
||||
max_value = std::max(max_value, sensor->state);
|
||||
}
|
||||
}
|
||||
|
||||
this->publish_state(max_value);
|
||||
}
|
||||
|
||||
void MeanCombinationComponent::handle_new_value(float value) {
|
||||
if (!std::isfinite(value))
|
||||
return;
|
||||
|
||||
float sum = 0.0;
|
||||
size_t count = 0.0;
|
||||
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
if (std::isfinite(sensor->state)) {
|
||||
++count;
|
||||
sum += sensor->state;
|
||||
}
|
||||
}
|
||||
|
||||
float mean = sum / count;
|
||||
|
||||
this->publish_state(mean);
|
||||
}
|
||||
|
||||
void MedianCombinationComponent::handle_new_value(float value) {
|
||||
// Sorts sensor states in ascending order and determines the middle value
|
||||
|
||||
if (!std::isfinite(value))
|
||||
return;
|
||||
|
||||
std::vector<float> sensor_states;
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
if (std::isfinite(sensor->state)) {
|
||||
sensor_states.push_back(sensor->state);
|
||||
}
|
||||
}
|
||||
|
||||
sort(sensor_states.begin(), sensor_states.end());
|
||||
size_t sensor_states_size = sensor_states.size();
|
||||
|
||||
float median = NAN;
|
||||
|
||||
if (sensor_states_size) {
|
||||
if (sensor_states_size % 2) {
|
||||
// Odd number of measurements, use middle measurement
|
||||
median = sensor_states[sensor_states_size / 2];
|
||||
} else {
|
||||
// Even number of measurements, use the average of the two middle measurements
|
||||
median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
this->publish_state(median);
|
||||
}
|
||||
|
||||
void MinimumCombinationComponent::handle_new_value(float value) {
|
||||
if (!std::isfinite(value))
|
||||
return;
|
||||
|
||||
float min_value = std::numeric_limits<float>::infinity(); // note x = min(x, infinity)
|
||||
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
if (std::isfinite(sensor->state)) {
|
||||
min_value = std::min(min_value, sensor->state);
|
||||
}
|
||||
}
|
||||
|
||||
this->publish_state(min_value);
|
||||
}
|
||||
|
||||
void MostRecentCombinationComponent::handle_new_value(float value) { this->publish_state(value); }
|
||||
|
||||
void RangeCombinationComponent::handle_new_value(float value) {
|
||||
// Sorts sensor states then takes difference between largest and smallest states
|
||||
|
||||
if (!std::isfinite(value))
|
||||
return;
|
||||
|
||||
std::vector<float> sensor_states;
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
if (std::isfinite(sensor->state)) {
|
||||
sensor_states.push_back(sensor->state);
|
||||
}
|
||||
}
|
||||
|
||||
sort(sensor_states.begin(), sensor_states.end());
|
||||
|
||||
float range = sensor_states.back() - sensor_states.front();
|
||||
this->publish_state(range);
|
||||
}
|
||||
|
||||
void SumCombinationComponent::handle_new_value(float value) {
|
||||
if (!std::isfinite(value))
|
||||
return;
|
||||
|
||||
float sum = 0.0;
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
if (std::isfinite(sensor->state)) {
|
||||
sum += sensor->state;
|
||||
}
|
||||
}
|
||||
|
||||
this->publish_state(sum);
|
||||
}
|
||||
|
||||
} // namespace combination
|
||||
} // namespace esphome
|
||||
141
esphome/components/combination/combination.h
Normal file
141
esphome/components/combination/combination.h
Normal file
@ -0,0 +1,141 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace combination {
|
||||
|
||||
class CombinationComponent : public Component, public sensor::Sensor {
|
||||
public:
|
||||
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
|
||||
|
||||
/// @brief Logs all source sensor's names
|
||||
virtual void log_source_sensors() = 0;
|
||||
|
||||
protected:
|
||||
/// @brief Logs the sensor for use in dump_config
|
||||
/// @param combo_type Name of the combination operation
|
||||
void log_config_(const LogString *combo_type);
|
||||
};
|
||||
|
||||
/// @brief Base class for operations that do not require an extra parameter to compute the combination
|
||||
class CombinationNoParameterComponent : public CombinationComponent {
|
||||
public:
|
||||
/// @brief Adds a callback to each source sensor
|
||||
void setup() override;
|
||||
|
||||
void add_source(Sensor *sensor);
|
||||
|
||||
/// @brief Computes the combination
|
||||
/// @param value Newest sensor measurement
|
||||
virtual void handle_new_value(float value) = 0;
|
||||
|
||||
/// @brief Logs all source sensor's names in sensors_
|
||||
void log_source_sensors() override;
|
||||
|
||||
protected:
|
||||
std::vector<Sensor *> sensors_;
|
||||
};
|
||||
|
||||
// Base class for opertions that require one parameter to compute the combination
|
||||
class CombinationOneParameterComponent : public CombinationComponent {
|
||||
public:
|
||||
void add_source(Sensor *sensor, std::function<float(float)> const &stddev);
|
||||
void add_source(Sensor *sensor, float stddev);
|
||||
|
||||
/// @brief Logs all source sensor's names in sensor_pairs_
|
||||
void log_source_sensors() override;
|
||||
|
||||
protected:
|
||||
std::vector<std::pair<Sensor *, std::function<float(float)>>> sensor_pairs_;
|
||||
};
|
||||
|
||||
class KalmanCombinationComponent : public CombinationOneParameterComponent {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
|
||||
void set_process_std_dev(float process_std_dev) {
|
||||
this->update_variance_value_ = process_std_dev * process_std_dev * 0.001f;
|
||||
}
|
||||
void set_std_dev_sensor(Sensor *sensor) { this->std_dev_sensor_ = sensor; }
|
||||
|
||||
protected:
|
||||
void update_variance_();
|
||||
void correct_(float value, float stddev);
|
||||
|
||||
// Optional sensor for publishing the current error
|
||||
sensor::Sensor *std_dev_sensor_{nullptr};
|
||||
|
||||
// Tick of the last update
|
||||
uint32_t last_update_{0};
|
||||
// Change of the variance, per ms
|
||||
float update_variance_value_{0.f};
|
||||
|
||||
// Best guess for the state and its variance
|
||||
float state_{NAN};
|
||||
float variance_{INFINITY};
|
||||
};
|
||||
|
||||
class LinearCombinationComponent : public CombinationOneParameterComponent {
|
||||
public:
|
||||
void dump_config() override { this->log_config_(LOG_STR("linear")); }
|
||||
void setup() override;
|
||||
|
||||
void handle_new_value(float value);
|
||||
};
|
||||
|
||||
class MaximumCombinationComponent : public CombinationNoParameterComponent {
|
||||
public:
|
||||
void dump_config() override { this->log_config_(LOG_STR("max")); }
|
||||
|
||||
void handle_new_value(float value) override;
|
||||
};
|
||||
|
||||
class MeanCombinationComponent : public CombinationNoParameterComponent {
|
||||
public:
|
||||
void dump_config() override { this->log_config_(LOG_STR("mean")); }
|
||||
|
||||
void handle_new_value(float value) override;
|
||||
};
|
||||
|
||||
class MedianCombinationComponent : public CombinationNoParameterComponent {
|
||||
public:
|
||||
void dump_config() override { this->log_config_(LOG_STR("median")); }
|
||||
|
||||
void handle_new_value(float value) override;
|
||||
};
|
||||
|
||||
class MinimumCombinationComponent : public CombinationNoParameterComponent {
|
||||
public:
|
||||
void dump_config() override { this->log_config_(LOG_STR("min")); }
|
||||
|
||||
void handle_new_value(float value) override;
|
||||
};
|
||||
|
||||
class MostRecentCombinationComponent : public CombinationNoParameterComponent {
|
||||
public:
|
||||
void dump_config() override { this->log_config_(LOG_STR("most_recently_updated")); }
|
||||
|
||||
void handle_new_value(float value) override;
|
||||
};
|
||||
|
||||
class RangeCombinationComponent : public CombinationNoParameterComponent {
|
||||
public:
|
||||
void dump_config() override { this->log_config_(LOG_STR("range")); }
|
||||
|
||||
void handle_new_value(float value) override;
|
||||
};
|
||||
|
||||
class SumCombinationComponent : public CombinationNoParameterComponent {
|
||||
public:
|
||||
void dump_config() override { this->log_config_(LOG_STR("sum")); }
|
||||
|
||||
void handle_new_value(float value) override;
|
||||
};
|
||||
|
||||
} // namespace combination
|
||||
} // namespace esphome
|
||||
176
esphome/components/combination/sensor.py
Normal file
176
esphome/components/combination/sensor.py
Normal file
@ -0,0 +1,176 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor
|
||||
from esphome.const import (
|
||||
CONF_ACCURACY_DECIMALS,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_RANGE,
|
||||
CONF_SOURCE,
|
||||
CONF_SUM,
|
||||
CONF_TYPE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
)
|
||||
from esphome.core.entity_helpers import inherit_property_from
|
||||
|
||||
CODEOWNERS = ["@Cat-Ion", "@kahrendt"]
|
||||
|
||||
combination_ns = cg.esphome_ns.namespace("combination")
|
||||
|
||||
KalmanCombinationComponent = combination_ns.class_(
|
||||
"KalmanCombinationComponent", cg.Component, sensor.Sensor
|
||||
)
|
||||
LinearCombinationComponent = combination_ns.class_(
|
||||
"LinearCombinationComponent", cg.Component, sensor.Sensor
|
||||
)
|
||||
MaximumCombinationComponent = combination_ns.class_(
|
||||
"MaximumCombinationComponent", cg.Component, sensor.Sensor
|
||||
)
|
||||
MeanCombinationComponent = combination_ns.class_(
|
||||
"MeanCombinationComponent", cg.Component, sensor.Sensor
|
||||
)
|
||||
MedianCombinationComponent = combination_ns.class_(
|
||||
"MedianCombinationComponent", cg.Component, sensor.Sensor
|
||||
)
|
||||
MinimumCombinationComponent = combination_ns.class_(
|
||||
"MinimumCombinationComponent", cg.Component, sensor.Sensor
|
||||
)
|
||||
MostRecentCombinationComponent = combination_ns.class_(
|
||||
"MostRecentCombinationComponent", cg.Component, sensor.Sensor
|
||||
)
|
||||
RangeCombinationComponent = combination_ns.class_(
|
||||
"RangeCombinationComponent", cg.Component, sensor.Sensor
|
||||
)
|
||||
SumCombinationComponent = combination_ns.class_(
|
||||
"SumCombinationComponent", cg.Component, sensor.Sensor
|
||||
)
|
||||
|
||||
CONF_COEFFECIENT = "coeffecient"
|
||||
CONF_ERROR = "error"
|
||||
CONF_KALMAN = "kalman"
|
||||
CONF_LINEAR = "linear"
|
||||
CONF_MAX = "max"
|
||||
CONF_MEAN = "mean"
|
||||
CONF_MEDIAN = "median"
|
||||
CONF_MIN = "min"
|
||||
CONF_MOST_RECENTLY_UPDATED = "most_recently_updated"
|
||||
CONF_PROCESS_STD_DEV = "process_std_dev"
|
||||
CONF_SOURCES = "sources"
|
||||
CONF_STD_DEV = "std_dev"
|
||||
|
||||
|
||||
KALMAN_SOURCE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
|
||||
cv.Required(CONF_ERROR): cv.templatable(cv.positive_float),
|
||||
}
|
||||
)
|
||||
|
||||
LINEAR_SOURCE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
|
||||
cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_),
|
||||
}
|
||||
)
|
||||
|
||||
SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
CONF_KALMAN: sensor.sensor_schema(KalmanCombinationComponent)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float,
|
||||
cv.Required(CONF_SOURCES): cv.ensure_list(KALMAN_SOURCE_SCHEMA),
|
||||
cv.Optional(CONF_STD_DEV): sensor.sensor_schema(),
|
||||
}
|
||||
),
|
||||
CONF_LINEAR: sensor.sensor_schema(LinearCombinationComponent)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(LINEAR_SOURCE_SCHEMA)}),
|
||||
CONF_MAX: sensor.sensor_schema(MaximumCombinationComponent)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
|
||||
CONF_MEAN: sensor.sensor_schema(MeanCombinationComponent)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
|
||||
CONF_MEDIAN: sensor.sensor_schema(MedianCombinationComponent)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
|
||||
CONF_MIN: sensor.sensor_schema(MinimumCombinationComponent)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
|
||||
CONF_MOST_RECENTLY_UPDATED: sensor.sensor_schema(MostRecentCombinationComponent)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
|
||||
CONF_RANGE: sensor.sensor_schema(RangeCombinationComponent)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
|
||||
CONF_SUM: sensor.sensor_schema(SumCombinationComponent)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Inherit some sensor values from the first source, for both the state and the error value
|
||||
# CONF_STATE_CLASS could also be inherited, but might lead to unexpected behaviour with "total_increasing"
|
||||
properties_to_inherit = [
|
||||
CONF_ACCURACY_DECIMALS,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
]
|
||||
inherit_schema_for_state = [
|
||||
inherit_property_from(property, [CONF_SOURCES, 0, CONF_SOURCE])
|
||||
for property in properties_to_inherit
|
||||
]
|
||||
inherit_schema_for_std_dev = [
|
||||
inherit_property_from([CONF_STD_DEV, property], [CONF_SOURCES, 0, CONF_SOURCE])
|
||||
for property in properties_to_inherit
|
||||
]
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
*inherit_schema_for_state,
|
||||
*inherit_schema_for_std_dev,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
|
||||
if proces_std_dev := config.get(CONF_PROCESS_STD_DEV):
|
||||
cg.add(var.set_process_std_dev(proces_std_dev))
|
||||
|
||||
for source_conf in config[CONF_SOURCES]:
|
||||
source = await cg.get_variable(source_conf[CONF_SOURCE])
|
||||
if config[CONF_TYPE] == CONF_KALMAN:
|
||||
error = await cg.templatable(
|
||||
source_conf[CONF_ERROR],
|
||||
[(float, "x")],
|
||||
cg.float_,
|
||||
)
|
||||
cg.add(var.add_source(source, error))
|
||||
elif config[CONF_TYPE] == CONF_LINEAR:
|
||||
coeffecient = await cg.templatable(
|
||||
source_conf[CONF_COEFFECIENT],
|
||||
[(float, "x")],
|
||||
cg.float_,
|
||||
)
|
||||
cg.add(var.add_source(source, coeffecient))
|
||||
else:
|
||||
cg.add(var.add_source(source))
|
||||
|
||||
if CONF_STD_DEV in config:
|
||||
sens = await sensor.new_sensor(config[CONF_STD_DEV])
|
||||
cg.add(var.set_std_dev_sensor(sens))
|
||||
@ -1,6 +1,8 @@
|
||||
#include "cse7766.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
namespace esphome {
|
||||
namespace cse7766 {
|
||||
@ -68,20 +70,26 @@ bool CSE7766Component::check_byte_() {
|
||||
return true;
|
||||
}
|
||||
void CSE7766Component::parse_data_() {
|
||||
ESP_LOGVV(TAG, "CSE7766 Data: ");
|
||||
for (uint8_t i = 0; i < 23; i++) {
|
||||
ESP_LOGVV(TAG, " %u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i + 1, BYTE_TO_BINARY(this->raw_data_[i]),
|
||||
this->raw_data_[i]);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Raw data:" << std::hex << std::uppercase << std::setfill('0');
|
||||
for (uint8_t i = 0; i < 23; i++) {
|
||||
ss << ' ' << std::setw(2) << static_cast<unsigned>(this->raw_data_[i]);
|
||||
}
|
||||
ESP_LOGVV(TAG, "%s", ss.str().c_str());
|
||||
}
|
||||
#endif
|
||||
|
||||
// Parse header
|
||||
uint8_t header1 = this->raw_data_[0];
|
||||
|
||||
if (header1 == 0xAA) {
|
||||
ESP_LOGE(TAG, "CSE7766 not calibrated!");
|
||||
return;
|
||||
}
|
||||
|
||||
bool power_cycle_exceeds_range = false;
|
||||
|
||||
if ((header1 & 0xF0) == 0xF0) {
|
||||
if (header1 & 0xD) {
|
||||
ESP_LOGE(TAG, "CSE7766 reports abnormal external circuit or chip damage: (0x%02X)", header1);
|
||||
@ -94,99 +102,106 @@ void CSE7766Component::parse_data_() {
|
||||
if (header1 & (1 << 0)) {
|
||||
ESP_LOGE(TAG, " Coefficient storage area is abnormal.");
|
||||
}
|
||||
|
||||
// Datasheet: voltage or current cycle exceeding range means invalid values
|
||||
return;
|
||||
}
|
||||
|
||||
power_cycle_exceeds_range = header1 & (1 << 1);
|
||||
}
|
||||
|
||||
uint32_t voltage_calib = this->get_24_bit_uint_(2);
|
||||
// Parse data frame
|
||||
uint32_t voltage_coeff = this->get_24_bit_uint_(2);
|
||||
uint32_t voltage_cycle = this->get_24_bit_uint_(5);
|
||||
uint32_t current_calib = this->get_24_bit_uint_(8);
|
||||
uint32_t current_coeff = this->get_24_bit_uint_(8);
|
||||
uint32_t current_cycle = this->get_24_bit_uint_(11);
|
||||
uint32_t power_calib = this->get_24_bit_uint_(14);
|
||||
uint32_t power_coeff = this->get_24_bit_uint_(14);
|
||||
uint32_t power_cycle = this->get_24_bit_uint_(17);
|
||||
|
||||
uint8_t adj = this->raw_data_[20];
|
||||
uint32_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22];
|
||||
|
||||
bool have_power = adj & 0x10;
|
||||
bool have_current = adj & 0x20;
|
||||
bool have_voltage = adj & 0x40;
|
||||
|
||||
float voltage = 0.0f;
|
||||
if (have_voltage) {
|
||||
// voltage cycle of serial port outputted is a complete cycle;
|
||||
this->voltage_acc_ += voltage_calib / float(voltage_cycle);
|
||||
this->voltage_counts_ += 1;
|
||||
voltage = voltage_coeff / float(voltage_cycle);
|
||||
if (this->voltage_sensor_ != nullptr) {
|
||||
this->voltage_sensor_->publish_state(voltage);
|
||||
}
|
||||
}
|
||||
|
||||
bool have_power = adj & 0x10;
|
||||
float power = 0.0f;
|
||||
|
||||
if (have_power) {
|
||||
// power cycle of serial port outputted is a complete cycle;
|
||||
// According to the user manual, power cycle exceeding range means the measured power is 0
|
||||
if (!power_cycle_exceeds_range) {
|
||||
power = power_calib / float(power_cycle);
|
||||
float energy = 0.0f;
|
||||
if (power_cycle_exceeds_range) {
|
||||
// Datasheet: power cycle exceeding range means active power is 0
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(0.0f);
|
||||
}
|
||||
} else if (have_power) {
|
||||
power = power_coeff / float(power_cycle);
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(power);
|
||||
}
|
||||
this->power_acc_ += power;
|
||||
this->power_counts_ += 1;
|
||||
|
||||
uint32_t difference;
|
||||
// Add CF pulses to the total energy only if we have Power coefficient to multiply by
|
||||
|
||||
if (this->cf_pulses_last_ == 0) {
|
||||
this->cf_pulses_last_ = cf_pulses;
|
||||
}
|
||||
|
||||
uint32_t cf_diff;
|
||||
if (cf_pulses < this->cf_pulses_last_) {
|
||||
difference = cf_pulses + (0x10000 - this->cf_pulses_last_);
|
||||
cf_diff = cf_pulses + (0x10000 - this->cf_pulses_last_);
|
||||
} else {
|
||||
difference = cf_pulses - this->cf_pulses_last_;
|
||||
cf_diff = cf_pulses - this->cf_pulses_last_;
|
||||
}
|
||||
this->cf_pulses_last_ = cf_pulses;
|
||||
this->energy_total_ += difference * float(power_calib) / 1000000.0f / 3600.0f;
|
||||
this->energy_total_counts_ += 1;
|
||||
}
|
||||
|
||||
if (adj & 0x20) {
|
||||
// indicates current cycle of serial port outputted is a complete cycle;
|
||||
float current = 0.0f;
|
||||
if (have_voltage && !have_power) {
|
||||
// Testing has shown that when we have voltage and current but not power, that means the power is 0.
|
||||
// We report a power of 0, which in turn means we should report a current of 0.
|
||||
this->power_counts_ += 1;
|
||||
} else if (power != 0.0f) {
|
||||
current = current_calib / float(current_cycle);
|
||||
}
|
||||
this->current_acc_ += current;
|
||||
this->current_counts_ += 1;
|
||||
}
|
||||
}
|
||||
void CSE7766Component::update() {
|
||||
const auto publish_state = [](const char *name, sensor::Sensor *sensor, float &acc, uint32_t &counts) {
|
||||
if (counts != 0) {
|
||||
const auto avg = acc / counts;
|
||||
|
||||
ESP_LOGV(TAG, "Got %s_acc=%.2f %s_counts=%" PRIu32 " %s=%.1f", name, acc, name, counts, name, avg);
|
||||
|
||||
if (sensor != nullptr) {
|
||||
sensor->publish_state(avg);
|
||||
}
|
||||
|
||||
acc = 0.0f;
|
||||
counts = 0;
|
||||
}
|
||||
};
|
||||
|
||||
publish_state("voltage", this->voltage_sensor_, this->voltage_acc_, this->voltage_counts_);
|
||||
publish_state("current", this->current_sensor_, this->current_acc_, this->current_counts_);
|
||||
publish_state("power", this->power_sensor_, this->power_acc_, this->power_counts_);
|
||||
|
||||
if (this->energy_total_counts_ != 0) {
|
||||
ESP_LOGV(TAG, "Got energy_total=%.2f energy_total_counts=%" PRIu32, this->energy_total_,
|
||||
this->energy_total_counts_);
|
||||
|
||||
if (this->energy_sensor_ != nullptr) {
|
||||
energy = cf_diff * float(power_coeff) / 1000000.0f / 3600.0f;
|
||||
this->energy_total_ += energy;
|
||||
if (this->energy_sensor_ != nullptr)
|
||||
this->energy_sensor_->publish_state(this->energy_total_);
|
||||
}
|
||||
this->energy_total_counts_ = 0;
|
||||
} else if ((this->energy_sensor_ != nullptr) && !this->energy_sensor_->has_state()) {
|
||||
this->energy_sensor_->publish_state(0);
|
||||
}
|
||||
|
||||
float current = 0.0f;
|
||||
float calculated_current = 0.0f;
|
||||
if (have_current) {
|
||||
// Assumption: if we don't have power measurement, then current is likely below 50mA
|
||||
if (have_power && voltage > 1.0f) {
|
||||
calculated_current = power / voltage;
|
||||
}
|
||||
// Datasheet: minimum measured current is 50mA
|
||||
if (calculated_current > 0.05f) {
|
||||
current = current_coeff / float(current_cycle);
|
||||
}
|
||||
if (this->current_sensor_ != nullptr) {
|
||||
this->current_sensor_->publish_state(current);
|
||||
}
|
||||
}
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Parsed:";
|
||||
if (have_voltage) {
|
||||
ss << " V=" << voltage << "V";
|
||||
}
|
||||
if (have_current) {
|
||||
ss << " I=" << current * 1000.0f << "mA (~" << calculated_current * 1000.0f << "mA)";
|
||||
}
|
||||
if (have_power) {
|
||||
ss << " P=" << power << "W";
|
||||
}
|
||||
if (energy != 0.0f) {
|
||||
ss << " E=" << energy << "kWh (" << cf_pulses << ")";
|
||||
}
|
||||
ESP_LOGVV(TAG, "%s", ss.str().c_str());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) {
|
||||
@ -196,7 +211,6 @@ uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) {
|
||||
|
||||
void CSE7766Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "CSE7766:");
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current", this->current_sensor_);
|
||||
LOG_SENSOR(" ", "Power", this->power_sensor_);
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
namespace esphome {
|
||||
namespace cse7766 {
|
||||
|
||||
class CSE7766Component : public PollingComponent, public uart::UARTDevice {
|
||||
class CSE7766Component : public Component, public uart::UARTDevice {
|
||||
public:
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
||||
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
|
||||
@ -16,7 +16,6 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice {
|
||||
|
||||
void loop() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
@ -31,16 +30,8 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice {
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
sensor::Sensor *power_sensor_{nullptr};
|
||||
sensor::Sensor *energy_sensor_{nullptr};
|
||||
float voltage_acc_{0.0f};
|
||||
float current_acc_{0.0f};
|
||||
float power_acc_{0.0f};
|
||||
float energy_total_{0.0f};
|
||||
uint32_t cf_pulses_last_{0};
|
||||
uint32_t voltage_counts_{0};
|
||||
uint32_t current_counts_{0};
|
||||
uint32_t power_counts_{0};
|
||||
// Setting this to 1 means it will always publish 0 once at startup
|
||||
uint32_t energy_total_counts_{1};
|
||||
};
|
||||
|
||||
} // namespace cse7766
|
||||
|
||||
@ -22,43 +22,37 @@ from esphome.const import (
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
cse7766_ns = cg.esphome_ns.namespace("cse7766")
|
||||
CSE7766Component = cse7766_ns.class_(
|
||||
"CSE7766Component", cg.PollingComponent, uart.UARTDevice
|
||||
)
|
||||
CSE7766Component = cse7766_ns.class_("CSE7766Component", cg.Component, uart.UARTDevice)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CSE7766Component),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_ENERGY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CSE7766Component),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_ENERGY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
}
|
||||
).extend(uart.UART_DEVICE_SCHEMA)
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"cse7766", baud_rate=4800, require_rx=True
|
||||
)
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome import core
|
||||
from esphome.automation import maybe_simple_id
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.components import uart
|
||||
@ -101,7 +100,7 @@ def range_segment_list(input):
|
||||
|
||||
largest_distance = -1
|
||||
for distance in input:
|
||||
if isinstance(distance, core.Lambda):
|
||||
if isinstance(distance, cv.Lambda):
|
||||
continue
|
||||
m = cv.distance(distance)
|
||||
if m > 9:
|
||||
@ -128,14 +127,14 @@ MMWAVE_SETTINGS_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_OUTPUT_LATENCY): {
|
||||
cv.Required(CONF_DELAY_AFTER_DETECT): cv.templatable(
|
||||
cv.All(
|
||||
cv.positive_time_period,
|
||||
cv.Range(max=core.TimePeriod(seconds=1638.375)),
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=cv.TimePeriod(seconds=1638.375)),
|
||||
)
|
||||
),
|
||||
cv.Required(CONF_DELAY_AFTER_DISAPPEAR): cv.templatable(
|
||||
cv.All(
|
||||
cv.positive_time_period,
|
||||
cv.Range(max=core.TimePeriod(seconds=1638.375)),
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=cv.TimePeriod(seconds=1638.375)),
|
||||
)
|
||||
),
|
||||
},
|
||||
|
||||
@ -50,7 +50,7 @@ class DfrobotSen0395SettingsAction : public Action<Ts...>, public Parented<Dfrob
|
||||
float detect = this->delay_after_detect_.value(x...);
|
||||
float disappear = this->delay_after_disappear_.value(x...);
|
||||
if (detect >= 0 && disappear >= 0) {
|
||||
this->parent_->enqueue(make_unique<OutputLatencyCommand>(detect, disappear));
|
||||
this->parent_->enqueue(make_unique<SetLatencyCommand>(detect, disappear));
|
||||
}
|
||||
}
|
||||
if (this->start_after_power_on_.has_value()) {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
#include "commands.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "dfrobot_sen0395.h"
|
||||
@ -194,32 +196,22 @@ uint8_t DetRangeCfgCommand::on_message(std::string &message) {
|
||||
return 0; // Command not done yet.
|
||||
}
|
||||
|
||||
OutputLatencyCommand::OutputLatencyCommand(float delay_after_detection, float delay_after_disappear) {
|
||||
delay_after_detection = round(delay_after_detection / 0.025) * 0.025;
|
||||
delay_after_disappear = round(delay_after_disappear / 0.025) * 0.025;
|
||||
if (delay_after_detection < 0)
|
||||
delay_after_detection = 0;
|
||||
if (delay_after_detection > 1638.375)
|
||||
delay_after_detection = 1638.375;
|
||||
if (delay_after_disappear < 0)
|
||||
delay_after_disappear = 0;
|
||||
if (delay_after_disappear > 1638.375)
|
||||
delay_after_disappear = 1638.375;
|
||||
|
||||
this->delay_after_detection_ = delay_after_detection;
|
||||
this->delay_after_disappear_ = delay_after_disappear;
|
||||
|
||||
this->cmd_ = str_sprintf("outputLatency -1 %.0f %.0f", delay_after_detection / 0.025, delay_after_disappear / 0.025);
|
||||
SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_after_disappear) {
|
||||
delay_after_detection = std::round(delay_after_detection / 0.025f) * 0.025f;
|
||||
delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f;
|
||||
this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f);
|
||||
this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f);
|
||||
this->cmd_ = str_sprintf("setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
|
||||
};
|
||||
|
||||
uint8_t OutputLatencyCommand::on_message(std::string &message) {
|
||||
uint8_t SetLatencyCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot configure output latency. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG, "Updated output latency config:");
|
||||
ESP_LOGI(TAG, "Signal that someone was detected is delayed by %.02fs.", this->delay_after_detection_);
|
||||
ESP_LOGI(TAG, "Signal that nobody is detected anymore is delayed by %.02fs.", this->delay_after_disappear_);
|
||||
ESP_LOGI(TAG, "Signal that someone was detected is delayed by %.03f s.", this->delay_after_detection_);
|
||||
ESP_LOGI(TAG, "Signal that nobody is detected anymore is delayed by %.03f s.", this->delay_after_disappear_);
|
||||
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
|
||||
return 1; // Command done
|
||||
}
|
||||
|
||||
@ -62,9 +62,9 @@ class DetRangeCfgCommand : public Command {
|
||||
// TODO: Set min max values in component, so they can be published as sensor.
|
||||
};
|
||||
|
||||
class OutputLatencyCommand : public Command {
|
||||
class SetLatencyCommand : public Command {
|
||||
public:
|
||||
OutputLatencyCommand(float delay_after_detection, float delay_after_disappear);
|
||||
SetLatencyCommand(float delay_after_detection, float delay_after_disappear);
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
|
||||
@ -91,7 +91,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
delayMicroseconds(40);
|
||||
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
|
||||
delayMicroseconds(2000);
|
||||
} else if (this->model_ == DHT_MODEL_AM2302) {
|
||||
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
|
||||
delayMicroseconds(1000);
|
||||
} else {
|
||||
delayMicroseconds(800);
|
||||
|
||||
@ -11,6 +11,7 @@ enum DHTModel {
|
||||
DHT_MODEL_AUTO_DETECT = 0,
|
||||
DHT_MODEL_DHT11,
|
||||
DHT_MODEL_DHT22,
|
||||
DHT_MODEL_AM2120,
|
||||
DHT_MODEL_AM2302,
|
||||
DHT_MODEL_RHT03,
|
||||
DHT_MODEL_SI7021,
|
||||
@ -27,6 +28,7 @@ class DHT : public PollingComponent {
|
||||
* - DHT_MODEL_AUTO_DETECT (default)
|
||||
* - DHT_MODEL_DHT11
|
||||
* - DHT_MODEL_DHT22
|
||||
* - DHT_MODEL_AM2120
|
||||
* - DHT_MODEL_AM2302
|
||||
* - DHT_MODEL_RHT03
|
||||
* - DHT_MODEL_SI7021
|
||||
|
||||
@ -23,6 +23,7 @@ DHT_MODELS = {
|
||||
"AUTO_DETECT": DHTModel.DHT_MODEL_AUTO_DETECT,
|
||||
"DHT11": DHTModel.DHT_MODEL_DHT11,
|
||||
"DHT22": DHTModel.DHT_MODEL_DHT22,
|
||||
"AM2120": DHTModel.DHT_MODEL_AM2120,
|
||||
"AM2302": DHTModel.DHT_MODEL_AM2302,
|
||||
"RHT03": DHTModel.DHT_MODEL_RHT03,
|
||||
"SI7021": DHTModel.DHT_MODEL_SI7021,
|
||||
|
||||
@ -35,6 +35,41 @@ void HOT Display::line(int x1, int y1, int x2, int y2, Color color) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
uint32_t color_value;
|
||||
for (int y = 0; y != h; y++) {
|
||||
size_t source_idx = (y_offset + y) * line_stride + x_offset;
|
||||
size_t source_idx_mod;
|
||||
for (int x = 0; x != w; x++, source_idx++) {
|
||||
switch (bitness) {
|
||||
default:
|
||||
color_value = ptr[source_idx];
|
||||
break;
|
||||
case COLOR_BITNESS_565:
|
||||
source_idx_mod = source_idx * 2;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
|
||||
}
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
source_idx_mod = source_idx * 3;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
|
||||
}
|
||||
break;
|
||||
}
|
||||
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
|
||||
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
|
||||
for (int i = x; i < x + width; i++)
|
||||
@ -106,6 +141,122 @@ void Display::filled_circle(int center_x, int center_y, int radius, Color color)
|
||||
}
|
||||
} while (dx <= 0);
|
||||
}
|
||||
void HOT Display::triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
|
||||
this->line(x1, y1, x2, y2, color);
|
||||
this->line(x1, y1, x3, y3, color);
|
||||
this->line(x2, y2, x3, y3, color);
|
||||
}
|
||||
void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3) {
|
||||
if (*y1 > *y2) {
|
||||
int x_temp = *x1, y_temp = *y1;
|
||||
*x1 = *x2, *y1 = *y2;
|
||||
*x2 = x_temp, *y2 = y_temp;
|
||||
}
|
||||
if (*y1 > *y3) {
|
||||
int x_temp = *x1, y_temp = *y1;
|
||||
*x1 = *x3, *y1 = *y3;
|
||||
*x3 = x_temp, *y3 = y_temp;
|
||||
}
|
||||
if (*y2 > *y3) {
|
||||
int x_temp = *x2, y_temp = *y2;
|
||||
*x2 = *x3, *y2 = *y3;
|
||||
*x3 = x_temp, *y3 = y_temp;
|
||||
}
|
||||
}
|
||||
void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
|
||||
// y2 must be equal to y3 (same horizontal line)
|
||||
|
||||
// Initialize Bresenham's algorithm for side 1
|
||||
int s1_current_x = x1;
|
||||
int s1_current_y = y1;
|
||||
bool s1_axis_swap = false;
|
||||
int s1_dx = abs(x2 - x1);
|
||||
int s1_dy = abs(y2 - y1);
|
||||
int s1_sign_x = ((x2 - x1) >= 0) ? 1 : -1;
|
||||
int s1_sign_y = ((y2 - y1) >= 0) ? 1 : -1;
|
||||
if (s1_dy > s1_dx) { // swap values
|
||||
int tmp = s1_dx;
|
||||
s1_dx = s1_dy;
|
||||
s1_dy = tmp;
|
||||
s1_axis_swap = true;
|
||||
}
|
||||
int s1_error = 2 * s1_dy - s1_dx;
|
||||
|
||||
// Initialize Bresenham's algorithm for side 2
|
||||
int s2_current_x = x1;
|
||||
int s2_current_y = y1;
|
||||
bool s2_axis_swap = false;
|
||||
int s2_dx = abs(x3 - x1);
|
||||
int s2_dy = abs(y3 - y1);
|
||||
int s2_sign_x = ((x3 - x1) >= 0) ? 1 : -1;
|
||||
int s2_sign_y = ((y3 - y1) >= 0) ? 1 : -1;
|
||||
if (s2_dy > s2_dx) { // swap values
|
||||
int tmp = s2_dx;
|
||||
s2_dx = s2_dy;
|
||||
s2_dy = tmp;
|
||||
s2_axis_swap = true;
|
||||
}
|
||||
int s2_error = 2 * s2_dy - s2_dx;
|
||||
|
||||
// Iterate on side 1 and allow side 2 to be processed to match the advance of the y-axis.
|
||||
for (int i = 0; i <= s1_dx; i++) {
|
||||
if (s1_current_x <= s2_current_x) {
|
||||
this->horizontal_line(s1_current_x, s1_current_y, s2_current_x - s1_current_x + 1, color);
|
||||
} else {
|
||||
this->horizontal_line(s2_current_x, s2_current_y, s1_current_x - s2_current_x + 1, color);
|
||||
}
|
||||
|
||||
// Bresenham's #1
|
||||
// Side 1 s1_current_x and s1_current_y calculation
|
||||
while (s1_error >= 0) {
|
||||
if (s1_axis_swap) {
|
||||
s1_current_x += s1_sign_x;
|
||||
} else {
|
||||
s1_current_y += s1_sign_y;
|
||||
}
|
||||
s1_error = s1_error - 2 * s1_dx;
|
||||
}
|
||||
if (s1_axis_swap) {
|
||||
s1_current_y += s1_sign_y;
|
||||
} else {
|
||||
s1_current_x += s1_sign_x;
|
||||
}
|
||||
s1_error = s1_error + 2 * s1_dy;
|
||||
|
||||
// Bresenham's #2
|
||||
// Side 2 s2_current_x and s2_current_y calculation
|
||||
while (s2_current_y != s1_current_y) {
|
||||
while (s2_error >= 0) {
|
||||
if (s2_axis_swap) {
|
||||
s2_current_x += s2_sign_x;
|
||||
} else {
|
||||
s2_current_y += s2_sign_y;
|
||||
}
|
||||
s2_error = s2_error - 2 * s2_dx;
|
||||
}
|
||||
if (s2_axis_swap) {
|
||||
s2_current_y += s2_sign_y;
|
||||
} else {
|
||||
s2_current_x += s2_sign_x;
|
||||
}
|
||||
s2_error = s2_error + 2 * s2_dy;
|
||||
}
|
||||
}
|
||||
}
|
||||
void Display::filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
|
||||
// Sort the three points by y-coordinate ascending, so [x1,y1] is the topmost point
|
||||
this->sort_triangle_points_by_y_(&x1, &y1, &x2, &y2, &x3, &y3);
|
||||
|
||||
if (y2 == y3) { // Check for special case of a bottom-flat triangle
|
||||
this->filled_flat_side_triangle_(x1, y1, x2, y2, x3, y3, color);
|
||||
} else if (y1 == y2) { // Check for special case of a top-flat triangle
|
||||
this->filled_flat_side_triangle_(x3, y3, x1, y1, x2, y2, color);
|
||||
} else { // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle
|
||||
int x_temp = (int) (x1 + ((float) (y2 - y1) / (float) (y3 - y1)) * (x3 - x1)), y_temp = y2;
|
||||
this->filled_flat_side_triangle_(x1, y1, x2, y2, x_temp, y_temp, color);
|
||||
this->filled_flat_side_triangle_(x3, y3, x2, y2, x_temp, y_temp, color);
|
||||
}
|
||||
}
|
||||
|
||||
void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) {
|
||||
int x_start, y_start;
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/time.h"
|
||||
#include "display_color_utils.h"
|
||||
|
||||
#ifdef USE_GRAPH
|
||||
#include "esphome/components/graph/graph.h"
|
||||
@ -185,6 +186,34 @@ class Display : public PollingComponent {
|
||||
/// Set a single pixel at the specified coordinates to the given color.
|
||||
virtual void draw_pixel_at(int x, int y, Color color) = 0;
|
||||
|
||||
/** Given an array of pixels encoded in the nominated format, draw these into the display's buffer.
|
||||
* The naive implementation here will work in all cases, but can be overridden by sub-classes
|
||||
* in order to optimise the procedure.
|
||||
* The parameters describe a rectangular block of pixels, potentially within a larger buffer.
|
||||
*
|
||||
* \param x_start The starting destination x position
|
||||
* \param y_start The starting destination y position
|
||||
* \param w the width of the pixel block
|
||||
* \param h the height of the pixel block
|
||||
* \param ptr A pointer to the start of the data to be copied
|
||||
* \param order The ordering of the colors
|
||||
* \param bitness Defines the number of bits and their format for each pixel
|
||||
* \param big_endian True if 16 bit values are stored big-endian
|
||||
* \param x_offset The initial x-offset into the source buffer.
|
||||
* \param y_offset The initial y-offset into the source buffer.
|
||||
* \param x_pad How many pixels are in each line after the end of the pixels to be copied.
|
||||
*
|
||||
* The length of each source buffer line (stride) will be x_offset + w + x_pad.
|
||||
*/
|
||||
virtual void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad);
|
||||
|
||||
/// Convenience overload for base case where the pixels are packed into the buffer with no gaps (e.g. suits LVGL.)
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian) {
|
||||
this->draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, 0, 0, 0);
|
||||
}
|
||||
|
||||
/// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color.
|
||||
void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON);
|
||||
|
||||
@ -207,6 +236,12 @@ class Display : public PollingComponent {
|
||||
/// Fill a circle centered around [center_x,center_y] with the radius radius with the given color.
|
||||
void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON);
|
||||
|
||||
/// Draw the outline of a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color.
|
||||
void triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON);
|
||||
|
||||
/// Fill a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color.
|
||||
void filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON);
|
||||
|
||||
/** Print `text` with the anchor point at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the text alignment anchor point.
|
||||
@ -503,6 +538,15 @@ class Display : public PollingComponent {
|
||||
void do_update_();
|
||||
void clear_clipping_();
|
||||
|
||||
/**
|
||||
* This method fills a triangle using only integer variables by using a
|
||||
* modified bresenham algorithm.
|
||||
* It is mandatory that [x2,y2] and [x3,y3] lie on the same horizontal line,
|
||||
* so y2 must be equal to y3.
|
||||
*/
|
||||
void filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color);
|
||||
void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3);
|
||||
|
||||
DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
|
||||
optional<display_writer_t> writer_{};
|
||||
DisplayPage *page_{nullptr};
|
||||
|
||||
@ -226,7 +226,7 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
|
||||
# The default/recommended esp-idf framework version
|
||||
# - https://github.com/espressif/esp-idf/releases
|
||||
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
|
||||
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 5)
|
||||
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 6)
|
||||
# The platformio/espressif32 version to use for esp-idf frameworks
|
||||
# - https://github.com/platformio/platform-espressif32/releases
|
||||
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
|
||||
@ -271,8 +271,8 @@ def _arduino_check_versions(value):
|
||||
def _esp_idf_check_versions(value):
|
||||
value = value.copy()
|
||||
lookups = {
|
||||
"dev": (cv.Version(5, 1, 0), "https://github.com/espressif/esp-idf.git"),
|
||||
"latest": (cv.Version(5, 1, 0), None),
|
||||
"dev": (cv.Version(5, 1, 2), "https://github.com/espressif/esp-idf.git"),
|
||||
"latest": (cv.Version(5, 1, 2), None),
|
||||
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
|
||||
}
|
||||
|
||||
|
||||
@ -42,6 +42,34 @@ ESP32_BASE_PINS = {
|
||||
}
|
||||
|
||||
ESP32_BOARD_PINS = {
|
||||
"adafruit_feather_esp32_v2": {
|
||||
"A0": 26,
|
||||
"A1": 25,
|
||||
"A2": 34,
|
||||
"A3": 39,
|
||||
"A4": 36,
|
||||
"A5": 4,
|
||||
"SCK": 5,
|
||||
"MOSI": 19,
|
||||
"MISO": 21,
|
||||
"RX": 7,
|
||||
"TX": 8,
|
||||
"D37": 37,
|
||||
"LED": 13,
|
||||
"LED_BUILTIN": 13,
|
||||
"D12": 12,
|
||||
"D27": 27,
|
||||
"D33": 33,
|
||||
"D15": 15,
|
||||
"D32": 32,
|
||||
"D14": 14,
|
||||
"SCL": 20,
|
||||
"SDA": 22,
|
||||
"BUTTON": 38,
|
||||
"NEOPIXEL": 0,
|
||||
"PIN_NEOPIXEL": 0,
|
||||
"NEOPIXEL_POWER": 2,
|
||||
},
|
||||
"adafruit_feather_esp32s2_tft": {
|
||||
"BUTTON": 0,
|
||||
"A0": 18,
|
||||
|
||||
@ -45,21 +45,19 @@ void BLEClientBase::loop() {
|
||||
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
|
||||
|
||||
bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
|
||||
if (!this->auto_connect_)
|
||||
return false;
|
||||
if (this->address_ == 0 || device.address_uint64() != this->address_)
|
||||
return false;
|
||||
if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING)
|
||||
return false;
|
||||
|
||||
ESP_LOGD(TAG, "[%d] [%s] Found device", this->connection_index_, this->address_str_.c_str());
|
||||
this->set_state(espbt::ClientState::DISCOVERED);
|
||||
this->log_event_("Found device");
|
||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG)
|
||||
esp32_ble_tracker::global_esp32_ble_tracker->print_bt_device_info(device);
|
||||
|
||||
auto addr = device.address_uint64();
|
||||
this->remote_bda_[0] = (addr >> 40) & 0xFF;
|
||||
this->remote_bda_[1] = (addr >> 32) & 0xFF;
|
||||
this->remote_bda_[2] = (addr >> 24) & 0xFF;
|
||||
this->remote_bda_[3] = (addr >> 16) & 0xFF;
|
||||
this->remote_bda_[4] = (addr >> 8) & 0xFF;
|
||||
this->remote_bda_[5] = (addr >> 0) & 0xFF;
|
||||
this->set_state(espbt::ClientState::DISCOVERED);
|
||||
this->set_address(device.address_uint64());
|
||||
this->remote_addr_type_ = device.get_address_type();
|
||||
return true;
|
||||
}
|
||||
@ -108,6 +106,10 @@ void BLEClientBase::release_services() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void BLEClientBase::log_event_(const char *name) {
|
||||
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name);
|
||||
}
|
||||
|
||||
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
|
||||
@ -131,51 +133,73 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT", this->connection_index_, this->address_str_.c_str());
|
||||
if (!this->check_addr(param->open.remote_bda))
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_OPEN_EVT");
|
||||
this->conn_id_ = param->open.conn_id;
|
||||
this->service_count_ = 0;
|
||||
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(),
|
||||
param->open.status);
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id);
|
||||
if (ret) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
|
||||
this->address_str_.c_str(), ret);
|
||||
}
|
||||
this->set_state(espbt::ClientState::CONNECTED);
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str());
|
||||
this->set_state(espbt::ClientState::CONNECTED);
|
||||
// only set our state, subclients might have more stuff to do yet.
|
||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CFG_MTU_EVT: {
|
||||
if (param->cfg_mtu.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status);
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
break;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(),
|
||||
param->cfg_mtu.status, param->cfg_mtu.mtu);
|
||||
this->mtu_ = param->cfg_mtu.mtu;
|
||||
case ESP_GATTC_CONNECT_EVT: {
|
||||
if (!this->check_addr(param->connect.remote_bda))
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_CONNECT_EVT");
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
if (memcmp(param->disconnect.remote_bda, this->remote_bda_, 6) != 0)
|
||||
if (!this->check_addr(param->disconnect.remote_bda))
|
||||
return false;
|
||||
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_,
|
||||
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->disconnect.reason);
|
||||
this->release_services();
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_GATTC_CFG_MTU_EVT: {
|
||||
if (this->conn_id_ != param->cfg_mtu.conn_id)
|
||||
return false;
|
||||
if (param->cfg_mtu.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status);
|
||||
// No state change required here - disconnect event will follow if needed.
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(),
|
||||
param->cfg_mtu.status, param->cfg_mtu.mtu);
|
||||
this->mtu_ = param->cfg_mtu.mtu;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
if (this->conn_id_ != param->close.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_CLOSE_EVT");
|
||||
this->release_services();
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_SEARCH_RES_EVT: {
|
||||
if (this->conn_id_ != param->search_res.conn_id)
|
||||
return false;
|
||||
this->service_count_++;
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||
// V3 clients don't need services initialized since
|
||||
@ -191,7 +215,9 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_SEARCH_CMPL_EVT", this->connection_index_, this->address_str_.c_str());
|
||||
if (this->conn_id_ != param->search_cmpl.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_SEARCH_CMPL_EVT");
|
||||
for (auto &svc : this->services_) {
|
||||
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
|
||||
svc->uuid.to_string().c_str());
|
||||
@ -199,11 +225,41 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
this->address_str_.c_str(), svc->start_handle, svc->end_handle);
|
||||
}
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str());
|
||||
this->set_state(espbt::ClientState::CONNECTED);
|
||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_READ_DESCR_EVT: {
|
||||
if (this->conn_id_ != param->write.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_READ_DESCR_EVT");
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_WRITE_DESCR_EVT: {
|
||||
if (this->conn_id_ != param->write.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_WRITE_DESCR_EVT");
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_WRITE_CHAR_EVT: {
|
||||
if (this->conn_id_ != param->write.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_WRITE_CHAR_EVT");
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_READ_CHAR_EVT: {
|
||||
if (this->conn_id_ != param->read.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_READ_CHAR_EVT");
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (this->conn_id_ != param->notify.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_NOTIFY_EVT");
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
this->log_event_("ESP_GATTC_REG_FOR_NOTIFY_EVT");
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||
// Client is responsible for flipping the descriptor value
|
||||
@ -212,9 +268,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
}
|
||||
esp_gattc_descr_elem_t desc_result;
|
||||
uint16_t count = 1;
|
||||
esp_gatt_status_t descr_status =
|
||||
esp_ble_gattc_get_descr_by_char_handle(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle,
|
||||
NOTIFY_DESC_UUID, &desc_result, &count);
|
||||
esp_gatt_status_t descr_status = esp_ble_gattc_get_descr_by_char_handle(
|
||||
this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, NOTIFY_DESC_UUID, &desc_result, &count);
|
||||
if (descr_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), descr_status);
|
||||
@ -222,7 +277,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
}
|
||||
esp_gattc_char_elem_t char_result;
|
||||
esp_gatt_status_t char_status =
|
||||
esp_ble_gattc_get_all_char(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle,
|
||||
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, param->reg_for_notify.handle,
|
||||
param->reg_for_notify.handle, &char_result, &count, 0);
|
||||
if (char_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
|
||||
@ -238,6 +293,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
esp_err_t status =
|
||||
esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en),
|
||||
(uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), status);
|
||||
@ -246,24 +302,31 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
}
|
||||
|
||||
default:
|
||||
// ideally would check all other events for matching conn_id
|
||||
ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_.c_str(), event);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// clients can't call defer() directly since it's protected.
|
||||
void BLEClientBase::run_later(std::function<void()> &&f) { // NOLINT
|
||||
this->defer(std::move(f));
|
||||
}
|
||||
|
||||
void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
switch (event) {
|
||||
// This event is sent by the server when it requests security
|
||||
case ESP_GAP_BLE_SEC_REQ_EVT:
|
||||
if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0)
|
||||
break;
|
||||
if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr))
|
||||
return;
|
||||
ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event);
|
||||
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
|
||||
break;
|
||||
// This event is sent once authentication has completed
|
||||
case ESP_GAP_BLE_AUTH_CMPL_EVT:
|
||||
if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0)
|
||||
break;
|
||||
if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr))
|
||||
return;
|
||||
esp_bd_addr_t bd_addr;
|
||||
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
|
||||
ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(),
|
||||
@ -273,11 +336,12 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_
|
||||
param->ble_security.auth_cmpl.fail_reason);
|
||||
} else {
|
||||
this->paired_ = true;
|
||||
ESP_LOGV(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_,
|
||||
ESP_LOGD(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type,
|
||||
param->ble_security.auth_cmpl.auth_mode);
|
||||
}
|
||||
break;
|
||||
|
||||
// There are other events we'll want to implement at some point to support things like pass key
|
||||
// https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md
|
||||
default:
|
||||
|
||||
@ -27,6 +27,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
void loop() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void run_later(std::function<void()> &&f); // NOLINT
|
||||
bool parse_device(const espbt::ESPBTDevice &device) override;
|
||||
void on_scan_end() override {}
|
||||
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
@ -39,10 +40,17 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
|
||||
bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
|
||||
|
||||
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }
|
||||
|
||||
void set_address(uint64_t address) {
|
||||
this->address_ = address;
|
||||
this->remote_bda_[0] = (address >> 40) & 0xFF;
|
||||
this->remote_bda_[1] = (address >> 32) & 0xFF;
|
||||
this->remote_bda_[2] = (address >> 24) & 0xFF;
|
||||
this->remote_bda_[3] = (address >> 16) & 0xFF;
|
||||
this->remote_bda_[4] = (address >> 8) & 0xFF;
|
||||
this->remote_bda_[5] = (address >> 0) & 0xFF;
|
||||
if (address == 0) {
|
||||
memset(this->remote_bda_, 0, sizeof(this->remote_bda_));
|
||||
this->address_str_ = "";
|
||||
} else {
|
||||
this->address_str_ =
|
||||
@ -79,20 +87,24 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
|
||||
virtual void set_connection_type(espbt::ConnectionType ct) { this->connection_type_ = ct; }
|
||||
|
||||
bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; }
|
||||
|
||||
protected:
|
||||
int gattc_if_;
|
||||
esp_bd_addr_t remote_bda_;
|
||||
esp_ble_addr_type_t remote_addr_type_;
|
||||
esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC};
|
||||
uint16_t conn_id_{0xFFFF};
|
||||
uint64_t address_{0};
|
||||
bool auto_connect_{false};
|
||||
std::string address_str_{};
|
||||
uint8_t connection_index_;
|
||||
int16_t service_count_{0};
|
||||
uint16_t mtu_{23};
|
||||
bool paired_{false};
|
||||
espbt::ConnectionType connection_type_{espbt::ConnectionType::V1};
|
||||
|
||||
std::vector<BLEService *> services_;
|
||||
|
||||
void log_event_(const char *name);
|
||||
};
|
||||
|
||||
} // namespace esp32_ble_client
|
||||
|
||||
@ -1,23 +1,26 @@
|
||||
import re
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome.components import esp32_ble
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
from esphome.const import (
|
||||
CONF_ACTIVE,
|
||||
CONF_DURATION,
|
||||
CONF_ID,
|
||||
CONF_INTERVAL,
|
||||
CONF_DURATION,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_MAC_ADDRESS,
|
||||
CONF_SERVICE_UUID,
|
||||
CONF_MANUFACTURER_ID,
|
||||
CONF_ON_BLE_ADVERTISE,
|
||||
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
|
||||
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
|
||||
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
|
||||
CONF_SERVICE_UUID,
|
||||
CONF_TRIGGER_ID,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
)
|
||||
from esphome.components import esp32_ble
|
||||
from esphome.core import CORE
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
|
||||
AUTO_LOAD = ["esp32_ble"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
@ -263,7 +266,10 @@ async def to_code(config):
|
||||
# https://github.com/espressif/esp-idf/issues/2503
|
||||
# Match arduino CONFIG_BTU_TASK_STACK_SIZE
|
||||
# https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866
|
||||
add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192)
|
||||
if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(4, 4, 6):
|
||||
add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192)
|
||||
else:
|
||||
add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
|
||||
|
||||
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
|
||||
|
||||
@ -161,10 +161,12 @@ light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index
|
||||
break;
|
||||
}
|
||||
uint8_t multiplier = this->is_rgbw_ ? 4 : 3;
|
||||
return {this->buf_ + (index * multiplier) + r,
|
||||
this->buf_ + (index * multiplier) + g,
|
||||
this->buf_ + (index * multiplier) + b,
|
||||
this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr,
|
||||
uint8_t white = this->is_wrgb_ ? 0 : 3;
|
||||
|
||||
return {this->buf_ + (index * multiplier) + r + this->is_wrgb_,
|
||||
this->buf_ + (index * multiplier) + g + this->is_wrgb_,
|
||||
this->buf_ + (index * multiplier) + b + this->is_wrgb_,
|
||||
this->is_rgbw_ || this->is_wrgb_ ? this->buf_ + (index * multiplier) + white : nullptr,
|
||||
&this->effect_data_[index],
|
||||
&this->correction_};
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
|
||||
int32_t size() const override { return this->num_leds_; }
|
||||
light::LightTraits get_traits() override {
|
||||
auto traits = light::LightTraits();
|
||||
if (this->is_rgbw_) {
|
||||
if (this->is_rgbw_ || this->is_wrgb_) {
|
||||
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
|
||||
} else {
|
||||
traits.set_supported_color_modes({light::ColorMode::RGB});
|
||||
@ -44,6 +44,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
|
||||
void set_pin(uint8_t pin) { this->pin_ = pin; }
|
||||
void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; }
|
||||
void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; }
|
||||
void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; }
|
||||
|
||||
/// Set a maximum refresh rate in µs as some lights do not like being updated too often.
|
||||
void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; }
|
||||
@ -72,6 +73,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
|
||||
uint8_t pin_;
|
||||
uint16_t num_leds_;
|
||||
bool is_rgbw_;
|
||||
bool is_wrgb_;
|
||||
|
||||
rmt_item32_t bit0_, bit1_;
|
||||
RGBOrder rgb_order_;
|
||||
|
||||
@ -52,6 +52,7 @@ CHIPSETS = {
|
||||
|
||||
|
||||
CONF_IS_RGBW = "is_rgbw"
|
||||
CONF_IS_WRGB = "is_wrgb"
|
||||
CONF_BIT0_HIGH = "bit0_high"
|
||||
CONF_BIT0_LOW = "bit0_low"
|
||||
CONF_BIT1_HIGH = "bit1_high"
|
||||
@ -90,6 +91,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
|
||||
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
|
||||
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
|
||||
cv.Optional(CONF_IS_WRGB, default=False): cv.boolean,
|
||||
cv.Inclusive(
|
||||
CONF_BIT0_HIGH,
|
||||
"custom",
|
||||
@ -145,6 +147,7 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_rgb_order(config[CONF_RGB_ORDER]))
|
||||
cg.add(var.set_is_rgbw(config[CONF_IS_RGBW]))
|
||||
cg.add(var.set_is_wrgb(config[CONF_IS_WRGB]))
|
||||
|
||||
cg.add(
|
||||
var.set_rmt_channel(
|
||||
|
||||
@ -13,8 +13,11 @@ from esphome.const import (
|
||||
CONF_ON_ENROLLMENT_DONE,
|
||||
CONF_ON_ENROLLMENT_FAILED,
|
||||
CONF_ON_ENROLLMENT_SCAN,
|
||||
CONF_ON_FINGER_SCAN_START,
|
||||
CONF_ON_FINGER_SCAN_MATCHED,
|
||||
CONF_ON_FINGER_SCAN_UNMATCHED,
|
||||
CONF_ON_FINGER_SCAN_MISPLACED,
|
||||
CONF_ON_FINGER_SCAN_INVALID,
|
||||
CONF_PASSWORD,
|
||||
CONF_SENSING_PIN,
|
||||
CONF_SPEED,
|
||||
@ -34,6 +37,10 @@ FingerprintGrowComponent = fingerprint_grow_ns.class_(
|
||||
"FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice
|
||||
)
|
||||
|
||||
FingerScanStartTrigger = fingerprint_grow_ns.class_(
|
||||
"FingerScanStartTrigger", automation.Trigger.template()
|
||||
)
|
||||
|
||||
FingerScanMatchedTrigger = fingerprint_grow_ns.class_(
|
||||
"FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16)
|
||||
)
|
||||
@ -42,6 +49,14 @@ FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_(
|
||||
"FingerScanUnmatchedTrigger", automation.Trigger.template()
|
||||
)
|
||||
|
||||
FingerScanMisplacedTrigger = fingerprint_grow_ns.class_(
|
||||
"FingerScanMisplacedTrigger", automation.Trigger.template()
|
||||
)
|
||||
|
||||
FingerScanInvalidTrigger = fingerprint_grow_ns.class_(
|
||||
"FingerScanInvalidTrigger", automation.Trigger.template()
|
||||
)
|
||||
|
||||
EnrollmentScanTrigger = fingerprint_grow_ns.class_(
|
||||
"EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16)
|
||||
)
|
||||
@ -94,6 +109,13 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_SENSING_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Optional(CONF_PASSWORD): cv.uint32_t,
|
||||
cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t,
|
||||
cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
FingerScanStartTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
@ -108,6 +130,20 @@ CONFIG_SCHEMA = (
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_FINGER_SCAN_MISPLACED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
FingerScanMisplacedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_FINGER_SCAN_INVALID): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
FingerScanInvalidTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
@ -152,6 +188,10 @@ async def to_code(config):
|
||||
sensing_pin = await cg.gpio_pin_expression(config[CONF_SENSING_PIN])
|
||||
cg.add(var.set_sensing_pin(sensing_pin))
|
||||
|
||||
for conf in config.get(CONF_ON_FINGER_SCAN_START, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
@ -162,6 +202,14 @@ async def to_code(config):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_FINGER_SCAN_MISPLACED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
|
||||
@ -15,16 +15,18 @@ void FingerprintGrowComponent::update() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->sensing_pin_ != nullptr) {
|
||||
if (this->has_sensing_pin_) {
|
||||
if (this->sensing_pin_->digital_read()) {
|
||||
ESP_LOGV(TAG, "No touch sensing");
|
||||
this->waiting_removal_ = false;
|
||||
return;
|
||||
} else if (!this->waiting_removal_) {
|
||||
this->finger_scan_start_callback_.call();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->waiting_removal_) {
|
||||
if (this->scan_image_(1) == NO_FINGER) {
|
||||
if ((!this->has_sensing_pin_) && (this->scan_image_(1) == NO_FINGER)) {
|
||||
ESP_LOGD(TAG, "Finger removed");
|
||||
this->waiting_removal_ = false;
|
||||
}
|
||||
@ -51,6 +53,7 @@ void FingerprintGrowComponent::update() {
|
||||
|
||||
void FingerprintGrowComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader...");
|
||||
this->has_sensing_pin_ = (this->sensing_pin_ != nullptr);
|
||||
if (this->check_password_()) {
|
||||
if (this->new_password_ != -1) {
|
||||
if (this->set_password_())
|
||||
@ -91,7 +94,7 @@ void FingerprintGrowComponent::finish_enrollment(uint8_t result) {
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::scan_and_match_() {
|
||||
if (this->sensing_pin_ != nullptr) {
|
||||
if (this->has_sensing_pin_) {
|
||||
ESP_LOGD(TAG, "Scan and match");
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Scan and match");
|
||||
@ -122,43 +125,52 @@ void FingerprintGrowComponent::scan_and_match_() {
|
||||
}
|
||||
|
||||
uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) {
|
||||
if (this->sensing_pin_ != nullptr) {
|
||||
if (this->has_sensing_pin_) {
|
||||
ESP_LOGD(TAG, "Getting image %d", buffer);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Getting image %d", buffer);
|
||||
}
|
||||
this->data_ = {GET_IMAGE};
|
||||
switch (this->send_command_()) {
|
||||
uint8_t send_result = this->send_command_();
|
||||
switch (send_result) {
|
||||
case OK:
|
||||
break;
|
||||
case NO_FINGER:
|
||||
if (this->sensing_pin_ != nullptr) {
|
||||
ESP_LOGD(TAG, "No finger");
|
||||
if (this->has_sensing_pin_) {
|
||||
this->waiting_removal_ = true;
|
||||
ESP_LOGD(TAG, "Finger Misplaced");
|
||||
this->finger_scan_misplaced_callback_.call();
|
||||
} else {
|
||||
ESP_LOGV(TAG, "No finger");
|
||||
}
|
||||
return this->data_[0];
|
||||
return send_result;
|
||||
case IMAGE_FAIL:
|
||||
ESP_LOGE(TAG, "Imaging error");
|
||||
this->finger_scan_invalid_callback_.call();
|
||||
return send_result;
|
||||
default:
|
||||
return this->data_[0];
|
||||
ESP_LOGD(TAG, "Unknown Scan Error: %d", send_result);
|
||||
return send_result;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Processing image %d", buffer);
|
||||
this->data_ = {IMAGE_2_TZ, buffer};
|
||||
switch (this->send_command_()) {
|
||||
send_result = this->send_command_();
|
||||
switch (send_result) {
|
||||
case OK:
|
||||
ESP_LOGI(TAG, "Processed image %d", buffer);
|
||||
break;
|
||||
case IMAGE_MESS:
|
||||
ESP_LOGE(TAG, "Image too messy");
|
||||
this->finger_scan_invalid_callback_.call();
|
||||
break;
|
||||
case FEATURE_FAIL:
|
||||
case INVALID_IMAGE:
|
||||
ESP_LOGE(TAG, "Could not find fingerprint features");
|
||||
this->finger_scan_invalid_callback_.call();
|
||||
break;
|
||||
}
|
||||
return this->data_[0];
|
||||
return send_result;
|
||||
}
|
||||
|
||||
uint8_t FingerprintGrowComponent::save_fingerprint_() {
|
||||
@ -221,10 +233,11 @@ bool FingerprintGrowComponent::get_parameters_() {
|
||||
ESP_LOGD(TAG, "Getting parameters");
|
||||
this->data_ = {READ_SYS_PARAM};
|
||||
if (this->send_command_() == OK) {
|
||||
ESP_LOGD(TAG, "Got parameters");
|
||||
if (this->status_sensor_ != nullptr) {
|
||||
ESP_LOGD(TAG, "Got parameters"); // Bear in mind data_[0] is the transfer status,
|
||||
if (this->status_sensor_ != nullptr) { // the parameters table start at data_[1]
|
||||
this->status_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]);
|
||||
}
|
||||
this->system_identifier_code_ = ((uint16_t) this->data_[3] << 8) | this->data_[4];
|
||||
this->capacity_ = ((uint16_t) this->data_[5] << 8) | this->data_[6];
|
||||
if (this->capacity_sensor_ != nullptr) {
|
||||
this->capacity_sensor_->publish_state(this->capacity_);
|
||||
@ -426,13 +439,22 @@ uint8_t FingerprintGrowComponent::send_command_() {
|
||||
|
||||
void FingerprintGrowComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "GROW_FINGERPRINT_READER:");
|
||||
ESP_LOGCONFIG(TAG, " System Identifier Code: 0x%.4X", this->system_identifier_code_);
|
||||
ESP_LOGCONFIG(TAG, " Touch Sensing Pin: %s",
|
||||
this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None");
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Fingerprint Count", this->fingerprint_count_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %d", (uint16_t) this->fingerprint_count_sensor_->get_state());
|
||||
LOG_SENSOR(" ", "Status", this->status_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %d", (uint8_t) this->status_sensor_->get_state());
|
||||
LOG_SENSOR(" ", "Capacity", this->capacity_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %d", (uint16_t) this->capacity_sensor_->get_state());
|
||||
LOG_SENSOR(" ", "Security Level", this->security_level_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %d", (uint8_t) this->security_level_sensor_->get_state());
|
||||
LOG_SENSOR(" ", "Last Finger ID", this->last_finger_id_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %d", (uint32_t) this->last_finger_id_sensor_->get_state());
|
||||
LOG_SENSOR(" ", "Last Confidence", this->last_confidence_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %d", (uint32_t) this->last_confidence_sensor_->get_state());
|
||||
}
|
||||
|
||||
} // namespace fingerprint_grow
|
||||
|
||||
@ -118,12 +118,21 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
|
||||
void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) {
|
||||
this->enrolling_binary_sensor_ = enrolling_binary_sensor;
|
||||
}
|
||||
void add_on_finger_scan_start_callback(std::function<void()> callback) {
|
||||
this->finger_scan_start_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_finger_scan_matched_callback(std::function<void(uint16_t, uint16_t)> callback) {
|
||||
this->finger_scan_matched_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_finger_scan_unmatched_callback(std::function<void()> callback) {
|
||||
this->finger_scan_unmatched_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_finger_scan_misplaced_callback(std::function<void()> callback) {
|
||||
this->finger_scan_misplaced_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_finger_scan_invalid_callback(std::function<void()> callback) {
|
||||
this->finger_scan_invalid_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_enrollment_scan_callback(std::function<void(uint8_t, uint16_t)> callback) {
|
||||
this->enrollment_scan_callback_.add(std::move(callback));
|
||||
}
|
||||
@ -163,8 +172,10 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
|
||||
uint16_t enrollment_slot_ = ENROLLMENT_SLOT_UNUSED;
|
||||
uint8_t enrollment_buffers_ = 5;
|
||||
bool waiting_removal_ = false;
|
||||
bool has_sensing_pin_ = false;
|
||||
uint32_t last_aura_led_control_ = 0;
|
||||
uint16_t last_aura_led_duration_ = 0;
|
||||
uint16_t system_identifier_code_ = 0;
|
||||
sensor::Sensor *fingerprint_count_sensor_{nullptr};
|
||||
sensor::Sensor *status_sensor_{nullptr};
|
||||
sensor::Sensor *capacity_sensor_{nullptr};
|
||||
@ -172,13 +183,23 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
|
||||
sensor::Sensor *last_finger_id_sensor_{nullptr};
|
||||
sensor::Sensor *last_confidence_sensor_{nullptr};
|
||||
binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr};
|
||||
CallbackManager<void()> finger_scan_invalid_callback_;
|
||||
CallbackManager<void()> finger_scan_start_callback_;
|
||||
CallbackManager<void(uint16_t, uint16_t)> finger_scan_matched_callback_;
|
||||
CallbackManager<void()> finger_scan_unmatched_callback_;
|
||||
CallbackManager<void()> finger_scan_misplaced_callback_;
|
||||
CallbackManager<void(uint8_t, uint16_t)> enrollment_scan_callback_;
|
||||
CallbackManager<void(uint16_t)> enrollment_done_callback_;
|
||||
CallbackManager<void(uint16_t)> enrollment_failed_callback_;
|
||||
};
|
||||
|
||||
class FingerScanStartTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit FingerScanStartTrigger(FingerprintGrowComponent *parent) {
|
||||
parent->add_on_finger_scan_start_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class FingerScanMatchedTrigger : public Trigger<uint16_t, uint16_t> {
|
||||
public:
|
||||
explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) {
|
||||
@ -194,6 +215,20 @@ class FingerScanUnmatchedTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
class FingerScanMisplacedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit FingerScanMisplacedTrigger(FingerprintGrowComponent *parent) {
|
||||
parent->add_on_finger_scan_misplaced_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class FingerScanInvalidTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit FingerScanInvalidTrigger(FingerprintGrowComponent *parent) {
|
||||
parent->add_on_finger_scan_invalid_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class EnrollmentScanTrigger : public Trigger<uint8_t, uint16_t> {
|
||||
public:
|
||||
explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) {
|
||||
|
||||
@ -67,13 +67,13 @@ def validate_pillow_installed(value):
|
||||
except ImportError as err:
|
||||
raise cv.Invalid(
|
||||
"Please install the pillow python package to use this feature. "
|
||||
'(pip install "pillow==10.1.0")'
|
||||
'(pip install "pillow==10.2.0")'
|
||||
) from err
|
||||
|
||||
if version.parse(PIL.__version__) != version.parse("10.1.0"):
|
||||
if version.parse(PIL.__version__) != version.parse("10.2.0"):
|
||||
raise cv.Invalid(
|
||||
"Please update your pillow installation to 10.1.0. "
|
||||
'(pip install "pillow==10.1.0")'
|
||||
"Please update your pillow installation to 10.2.0. "
|
||||
'(pip install "pillow==10.2.0")'
|
||||
)
|
||||
|
||||
return value
|
||||
@ -235,7 +235,7 @@ FILE_SCHEMA = cv.Schema(_file_schema)
|
||||
|
||||
|
||||
DEFAULT_GLYPHS = (
|
||||
' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
||||
' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
||||
)
|
||||
CONF_RAW_GLYPH_ID = "raw_glyph_id"
|
||||
|
||||
|
||||
@ -13,14 +13,14 @@
|
||||
namespace esphome {
|
||||
namespace ft63x6 {
|
||||
|
||||
static const uint8_t FT63X6_ADDR_TOUCH_COUNT = 0x02;
|
||||
|
||||
static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05;
|
||||
static const uint8_t FT63X6_ADDR_TOUCH1_STATE = 0x03;
|
||||
static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03;
|
||||
static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05;
|
||||
static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05;
|
||||
|
||||
static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B;
|
||||
static const uint8_t FT63X6_ADDR_TOUCH2_STATE = 0x09;
|
||||
static const uint8_t FT63X6_ADDR_TOUCH2_X = 0x09;
|
||||
static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B;
|
||||
static const uint8_t FT63X6_ADDR_TOUCH2_Y = 0x0B;
|
||||
|
||||
static const char *const TAG = "FT63X6Touchscreen";
|
||||
@ -40,26 +40,11 @@ void FT63X6Touchscreen::setup() {
|
||||
this->hard_reset_();
|
||||
|
||||
// Get touch resolution
|
||||
this->x_raw_max_ = 320;
|
||||
this->y_raw_max_ = 480;
|
||||
}
|
||||
|
||||
void FT63X6Touchscreen::update_touches() {
|
||||
int touch_count = this->read_touch_count_();
|
||||
if (touch_count == 0) {
|
||||
return;
|
||||
if (this->x_raw_max_ == this->x_raw_min_) {
|
||||
this->x_raw_max_ = 320;
|
||||
}
|
||||
|
||||
uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID); // id1 = 0 or 1
|
||||
int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X);
|
||||
int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y);
|
||||
this->add_raw_touch_position_(touch_id, x, y);
|
||||
|
||||
if (touch_count >= 2) {
|
||||
touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID); // id2 = 0 or 1(~id1 & 0x01)
|
||||
x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X);
|
||||
y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y);
|
||||
this->add_raw_touch_position_(touch_id, x, y);
|
||||
if (this->y_raw_max_ == this->y_raw_min_) {
|
||||
this->y_raw_max_ = 480;
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,23 +61,31 @@ void FT63X6Touchscreen::dump_config() {
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
uint8_t FT63X6Touchscreen::read_touch_count_() { return this->read_byte_(FT63X6_ADDR_TOUCH_COUNT); }
|
||||
void FT63X6Touchscreen::update_touches() {
|
||||
uint8_t data[15];
|
||||
uint16_t touch_id, x, y;
|
||||
|
||||
// Touch functions
|
||||
uint16_t FT63X6Touchscreen::read_touch_coordinate_(uint8_t coordinate) {
|
||||
uint8_t read_buf[2];
|
||||
read_buf[0] = this->read_byte_(coordinate);
|
||||
read_buf[1] = this->read_byte_(coordinate + 1);
|
||||
return ((read_buf[0] & 0x0f) << 8) | read_buf[1];
|
||||
}
|
||||
uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t id_address) { return this->read_byte_(id_address) >> 4; }
|
||||
if (!this->read_bytes(0x00, (uint8_t *) data, 15)) {
|
||||
ESP_LOGE(TAG, "Failed to read touch data");
|
||||
this->skip_update_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) {
|
||||
uint8_t byte = 0;
|
||||
this->read_byte(addr, &byte);
|
||||
return byte;
|
||||
if (((data[FT63X6_ADDR_TOUCH1_STATE] >> 6) & 0x01) == 0) {
|
||||
touch_id = data[FT63X6_ADDR_TOUCH1_ID] >> 4; // id1 = 0 or 1
|
||||
x = encode_uint16(data[FT63X6_ADDR_TOUCH1_X] & 0x0F, data[FT63X6_ADDR_TOUCH1_X + 1]);
|
||||
y = encode_uint16(data[FT63X6_ADDR_TOUCH1_Y] & 0x0F, data[FT63X6_ADDR_TOUCH1_Y + 1]);
|
||||
this->add_raw_touch_position_(touch_id, x, y);
|
||||
}
|
||||
if (((data[FT63X6_ADDR_TOUCH2_STATE] >> 6) & 0x01) == 0) {
|
||||
touch_id = data[FT63X6_ADDR_TOUCH2_ID] >> 4; // id1 = 0 or 1
|
||||
x = encode_uint16(data[FT63X6_ADDR_TOUCH2_X] & 0x0F, data[FT63X6_ADDR_TOUCH2_X + 1]);
|
||||
y = encode_uint16(data[FT63X6_ADDR_TOUCH2_Y] & 0x0F, data[FT63X6_ADDR_TOUCH2_Y + 1]);
|
||||
this->add_raw_touch_position_(touch_id, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ft63x6
|
||||
|
||||
@ -61,6 +61,7 @@ VALUE_POSITION_TYPE = {
|
||||
"BELOW": ValuePositionType.VALUE_POSITION_TYPE_BELOW,
|
||||
}
|
||||
|
||||
CONF_CONTINUOUS = "continuous"
|
||||
|
||||
GRAPH_TRACE_SCHEMA = cv.Schema(
|
||||
{
|
||||
@ -70,6 +71,7 @@ GRAPH_TRACE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_LINE_THICKNESS): cv.positive_int,
|
||||
cv.Optional(CONF_LINE_TYPE): cv.enum(LINE_TYPE, upper=True),
|
||||
cv.Optional(CONF_COLOR): cv.use_id(color.ColorStruct),
|
||||
cv.Optional(CONF_CONTINUOUS): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
@ -186,6 +188,8 @@ async def to_code(config):
|
||||
if CONF_COLOR in trace:
|
||||
c = await cg.get_variable(trace[CONF_COLOR])
|
||||
cg.add(tr.set_line_color(c))
|
||||
if CONF_CONTINUOUS in trace:
|
||||
cg.add(tr.set_continuous(trace[CONF_CONTINUOUS]))
|
||||
cg.add(var.add_trace(tr))
|
||||
# Add legend
|
||||
if CONF_LEGEND in config:
|
||||
|
||||
@ -165,17 +165,42 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
|
||||
for (auto *trace : traces_) {
|
||||
Color c = trace->get_line_color();
|
||||
uint16_t thick = trace->get_line_thickness();
|
||||
bool continuous = trace->get_continuous();
|
||||
bool has_prev = false;
|
||||
bool prev_b = false;
|
||||
int16_t prev_y = 0;
|
||||
for (uint32_t i = 0; i < this->width_; i++) {
|
||||
float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange;
|
||||
if (!std::isnan(v) && (thick > 0)) {
|
||||
int16_t x = this->width_ - 1 - i;
|
||||
uint8_t b = (i % (thick * LineType::PATTERN_LENGTH)) / thick;
|
||||
if (((uint8_t) trace->get_line_type() & (1 << b)) == (1 << b)) {
|
||||
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2;
|
||||
for (uint16_t t = 0; t < thick; t++) {
|
||||
buff->draw_pixel_at(x_offset + x, y_offset + y + t, c);
|
||||
int16_t x = this->width_ - 1 - i + x_offset;
|
||||
uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick);
|
||||
bool b = (trace->get_line_type() & bit) == bit;
|
||||
if (b) {
|
||||
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
|
||||
if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) {
|
||||
for (uint16_t t = 0; t < thick; t++) {
|
||||
buff->draw_pixel_at(x, y + t, c);
|
||||
}
|
||||
} else {
|
||||
int16_t mid_y = (y + prev_y + thick) / 2;
|
||||
if (y > prev_y) {
|
||||
for (uint16_t t = prev_y + thick; t <= mid_y; t++)
|
||||
buff->draw_pixel_at(x + 1, t, c);
|
||||
for (uint16_t t = mid_y + 1; t < y + thick; t++)
|
||||
buff->draw_pixel_at(x, t, c);
|
||||
} else {
|
||||
for (uint16_t t = prev_y - 1; t >= mid_y; t--)
|
||||
buff->draw_pixel_at(x + 1, t, c);
|
||||
for (uint16_t t = mid_y - 1; t >= y; t--)
|
||||
buff->draw_pixel_at(x, t, c);
|
||||
}
|
||||
}
|
||||
prev_y = y;
|
||||
}
|
||||
prev_b = b;
|
||||
has_prev = true;
|
||||
} else {
|
||||
has_prev = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,6 +116,8 @@ class GraphTrace {
|
||||
void set_line_type(enum LineType val) { this->line_type_ = val; }
|
||||
Color get_line_color() { return this->line_color_; }
|
||||
void set_line_color(Color val) { this->line_color_ = val; }
|
||||
bool get_continuous() { return this->continuous_; }
|
||||
void set_continuous(bool continuous) { this->continuous_ = continuous; }
|
||||
std::string get_name() { return name_; }
|
||||
const HistoryData *get_tracedata() { return &data_; }
|
||||
|
||||
@ -125,6 +127,7 @@ class GraphTrace {
|
||||
uint8_t line_thickness_{3};
|
||||
enum LineType line_type_ { LINE_TYPE_SOLID };
|
||||
Color line_color_{COLOR_ON};
|
||||
bool continuous_{false};
|
||||
HistoryData data_;
|
||||
|
||||
friend Graph;
|
||||
|
||||
@ -14,6 +14,7 @@ static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F};
|
||||
static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D};
|
||||
static const uint8_t GET_MAX_VALUES[2] = {0x80, 0x48};
|
||||
static const size_t MAX_TOUCHES = 5; // max number of possible touches reported
|
||||
static const size_t MAX_BUTTONS = 4; // max number of buttons scanned
|
||||
|
||||
#define ERROR_CHECK(err) \
|
||||
if ((err) != i2c::ERROR_OK) { \
|
||||
@ -79,9 +80,6 @@ void GT911Touchscreen::update_touches() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (num_of_touches == 0)
|
||||
return;
|
||||
|
||||
err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false);
|
||||
ERROR_CHECK(err);
|
||||
// num_of_touches is guaranteed to be 0..5. Also read the key data
|
||||
@ -94,10 +92,13 @@ void GT911Touchscreen::update_touches() {
|
||||
uint16_t y = encode_uint16(data[i][4], data[i][3]);
|
||||
this->add_raw_touch_position_(id, x, y);
|
||||
}
|
||||
auto keys = data[num_of_touches][0];
|
||||
for (size_t i = 0; i != 4; i++) {
|
||||
for (auto *listener : this->button_listeners_)
|
||||
listener->update_button(i, (keys & (1 << i)) != 0);
|
||||
auto keys = data[num_of_touches][0] & ((1 << MAX_BUTTONS) - 1);
|
||||
if (keys != this->button_state_) {
|
||||
this->button_state_ = keys;
|
||||
for (size_t i = 0; i != MAX_BUTTONS; i++) {
|
||||
for (auto *listener : this->button_listeners_)
|
||||
listener->update_button(i, (keys & (1 << i)) != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{};
|
||||
std::vector<GT911ButtonListener *> button_listeners_;
|
||||
uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update.
|
||||
};
|
||||
|
||||
} // namespace gt911
|
||||
|
||||
@ -18,6 +18,7 @@ from esphome.const import (
|
||||
CONF_SUPPORTED_SWING_MODES,
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
CONF_TEMPERATURE_STEP,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_VISUAL,
|
||||
CONF_WIFI,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@ -49,6 +50,8 @@ CONF_CONTROL_METHOD = "control_method"
|
||||
CONF_CONTROL_PACKET_SIZE = "control_packet_size"
|
||||
CONF_DISPLAY = "display"
|
||||
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
|
||||
CONF_ON_ALARM_START = "on_alarm_start"
|
||||
CONF_ON_ALARM_END = "on_alarm_end"
|
||||
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
|
||||
CONF_VERTICAL_AIRFLOW = "vertical_airflow"
|
||||
CONF_WIFI_SIGNAL = "wifi_signal"
|
||||
@ -85,8 +88,8 @@ AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = {
|
||||
}
|
||||
|
||||
SUPPORTED_SWING_MODES_OPTIONS = {
|
||||
"OFF": ClimateSwingMode.CLIMATE_SWING_OFF, # always available
|
||||
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, # always available
|
||||
"OFF": ClimateSwingMode.CLIMATE_SWING_OFF,
|
||||
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
|
||||
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
|
||||
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
|
||||
}
|
||||
@ -101,13 +104,15 @@ SUPPORTED_CLIMATE_MODES_OPTIONS = {
|
||||
}
|
||||
|
||||
SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = {
|
||||
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
|
||||
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
|
||||
"COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT,
|
||||
}
|
||||
|
||||
SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = {
|
||||
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
|
||||
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
|
||||
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
|
||||
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
|
||||
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
|
||||
}
|
||||
|
||||
@ -118,6 +123,16 @@ SUPPORTED_HON_CONTROL_METHODS = {
|
||||
"SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER,
|
||||
}
|
||||
|
||||
HaierAlarmStartTrigger = haier_ns.class_(
|
||||
"HaierAlarmStartTrigger",
|
||||
automation.Trigger.template(cg.uint8, cg.const_char_ptr),
|
||||
)
|
||||
|
||||
HaierAlarmEndTrigger = haier_ns.class_(
|
||||
"HaierAlarmEndTrigger",
|
||||
automation.Trigger.template(cg.uint8, cg.const_char_ptr),
|
||||
)
|
||||
|
||||
|
||||
def validate_visual(config):
|
||||
if CONF_VISUAL in config:
|
||||
@ -200,9 +215,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_SUPPORTED_PRESETS,
|
||||
default=list(
|
||||
SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS.keys()
|
||||
),
|
||||
default=list(["BOOST", "COMFORT"]), # No AWAY by default
|
||||
): cv.ensure_list(
|
||||
cv.enum(SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS, upper=True)
|
||||
),
|
||||
@ -222,7 +235,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50),
|
||||
cv.Optional(
|
||||
CONF_SUPPORTED_PRESETS,
|
||||
default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()),
|
||||
default=list(["BOOST", "ECO", "SLEEP"]), # No AWAY by default
|
||||
): cv.ensure_list(
|
||||
cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True)
|
||||
),
|
||||
@ -233,6 +246,20 @@ CONFIG_SCHEMA = cv.All(
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_ON_ALARM_START): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
HaierAlarmStartTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ALARM_END): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
HaierAlarmEndTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
),
|
||||
},
|
||||
@ -457,5 +484,15 @@ async def to_code(config):
|
||||
config[CONF_CONTROL_PACKET_SIZE] - PROTOCOL_CONTROL_PACKET_SIZE
|
||||
)
|
||||
)
|
||||
for conf in config.get(CONF_ON_ALARM_START, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_ALARM_END, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf
|
||||
)
|
||||
# https://github.com/paveldn/HaierProtocol
|
||||
cg.add_library("pavlodn/HaierProtocol", "0.9.24")
|
||||
cg.add_library("pavlodn/HaierProtocol", "0.9.25")
|
||||
|
||||
@ -25,13 +25,14 @@ const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
|
||||
"SENDING_INIT_1",
|
||||
"SENDING_INIT_2",
|
||||
"SENDING_FIRST_STATUS_REQUEST",
|
||||
"SENDING_ALARM_STATUS_REQUEST",
|
||||
"SENDING_FIRST_ALARM_STATUS_REQUEST",
|
||||
"IDLE",
|
||||
"SENDING_STATUS_REQUEST",
|
||||
"SENDING_UPDATE_SIGNAL_REQUEST",
|
||||
"SENDING_SIGNAL_LEVEL",
|
||||
"SENDING_CONTROL",
|
||||
"SENDING_ACTION_COMMAND",
|
||||
"SENDING_ALARM_STATUS_REQUEST",
|
||||
"UNKNOWN" // Should be the last!
|
||||
};
|
||||
static_assert(
|
||||
|
||||
@ -64,7 +64,7 @@ class HaierClimateBase : public esphome::Component,
|
||||
SENDING_INIT_1 = 0,
|
||||
SENDING_INIT_2,
|
||||
SENDING_FIRST_STATUS_REQUEST,
|
||||
SENDING_ALARM_STATUS_REQUEST,
|
||||
SENDING_FIRST_ALARM_STATUS_REQUEST,
|
||||
// FUNCTIONAL STATE
|
||||
IDLE,
|
||||
SENDING_STATUS_REQUEST,
|
||||
@ -72,6 +72,7 @@ class HaierClimateBase : public esphome::Component,
|
||||
SENDING_SIGNAL_LEVEL,
|
||||
SENDING_CONTROL,
|
||||
SENDING_ACTION_COMMAND,
|
||||
SENDING_ALARM_STATUS_REQUEST,
|
||||
NUM_PROTOCOL_PHASES
|
||||
};
|
||||
const char *phase_to_string_(ProtocolPhases phase);
|
||||
|
||||
@ -16,6 +16,7 @@ constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
|
||||
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
|
||||
constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5;
|
||||
constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500);
|
||||
constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000;
|
||||
|
||||
hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) {
|
||||
switch (direction) {
|
||||
@ -110,6 +111,14 @@ void HonClimate::start_steri_cleaning() {
|
||||
}
|
||||
}
|
||||
|
||||
void HonClimate::add_alarm_start_callback(std::function<void(uint8_t, const char *)> &&callback) {
|
||||
this->alarm_start_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void HonClimate::add_alarm_end_callback(std::function<void(uint8_t, const char *)> &&callback) {
|
||||
this->alarm_end_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type,
|
||||
haier_protocol::FrameType message_type,
|
||||
const uint8_t *data, size_t data_size) {
|
||||
@ -194,7 +203,7 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy
|
||||
switch (this->protocol_phase_) {
|
||||
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
|
||||
ESP_LOGI(TAG, "First HVAC status received");
|
||||
this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
|
||||
this->set_phase(ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST);
|
||||
break;
|
||||
case ProtocolPhases::SENDING_ACTION_COMMAND:
|
||||
// Do nothing, phase will be changed in process_phase
|
||||
@ -251,12 +260,15 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
|
||||
}
|
||||
if (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) {
|
||||
if ((this->protocol_phase_ != ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST) &&
|
||||
(this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST)) {
|
||||
// Don't expect this answer now
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
|
||||
}
|
||||
memcpy(this->active_alarms_, data + 2, 8);
|
||||
if (data_size < sizeof(active_alarms_) + 2)
|
||||
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
|
||||
this->process_alarm_message_(data, data_size, this->protocol_phase_ >= ProtocolPhases::IDLE);
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
return haier_protocol::HandlerError::HANDLER_OK;
|
||||
} else {
|
||||
@ -265,6 +277,19 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_
|
||||
}
|
||||
}
|
||||
|
||||
haier_protocol::HandlerError HonClimate::alarm_status_message_handler_(haier_protocol::FrameType type,
|
||||
const uint8_t *buffer, size_t size) {
|
||||
haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK;
|
||||
if (size < sizeof(this->active_alarms_) + 2) {
|
||||
// Log error but confirm anyway to avoid to many messages
|
||||
result = haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
|
||||
}
|
||||
this->process_alarm_message_(buffer, size, true);
|
||||
this->haier_protocol_.send_answer(haier_protocol::HaierMessage(haier_protocol::FrameType::CONFIRM));
|
||||
this->last_alarm_request_ = std::chrono::steady_clock::now();
|
||||
return result;
|
||||
}
|
||||
|
||||
void HonClimate::set_handlers() {
|
||||
// Set handlers
|
||||
this->haier_protocol_.set_answer_handler(
|
||||
@ -291,6 +316,10 @@ void HonClimate::set_handlers() {
|
||||
haier_protocol::FrameType::REPORT_NETWORK_STATUS,
|
||||
std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
|
||||
std::placeholders::_3, std::placeholders::_4));
|
||||
this->haier_protocol_.set_message_handler(
|
||||
haier_protocol::FrameType::ALARM_STATUS,
|
||||
std::bind(&HonClimate::alarm_status_message_handler_, this, std::placeholders::_1, std::placeholders::_2,
|
||||
std::placeholders::_3));
|
||||
}
|
||||
|
||||
void HonClimate::dump_config() {
|
||||
@ -363,10 +392,12 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
break;
|
||||
#endif
|
||||
case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST:
|
||||
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(haier_protocol::FrameType::GET_ALARM_STATUS);
|
||||
this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_);
|
||||
this->last_alarm_request_ = now;
|
||||
}
|
||||
break;
|
||||
case ProtocolPhases::SENDING_CONTROL:
|
||||
@ -417,12 +448,16 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
|
||||
if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
|
||||
this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST);
|
||||
this->forced_request_status_ = false;
|
||||
} else if (std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_alarm_request_).count() >
|
||||
ALARM_STATUS_REQUEST_INTERVAL_MS) {
|
||||
this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
|
||||
}
|
||||
#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))
|
||||
SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) {
|
||||
this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
|
||||
}
|
||||
#endif
|
||||
} break;
|
||||
default:
|
||||
@ -452,6 +487,7 @@ 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;
|
||||
control_out_buffer[4] = 0; // This byte should be cleared before setting values
|
||||
bool has_hvac_settings = false;
|
||||
if (this->current_hvac_settings_.valid) {
|
||||
has_hvac_settings = true;
|
||||
@ -552,31 +588,41 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
|
||||
out_data->quiet_mode = 0;
|
||||
out_data->fast_mode = 0;
|
||||
out_data->sleep_mode = 0;
|
||||
out_data->ten_degree = 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;
|
||||
out_data->ten_degree = 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;
|
||||
out_data->ten_degree = 0;
|
||||
break;
|
||||
case CLIMATE_PRESET_AWAY:
|
||||
out_data->quiet_mode = 0;
|
||||
out_data->fast_mode = 0;
|
||||
out_data->sleep_mode = 0;
|
||||
// 10 degrees allowed only in heat mode
|
||||
out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0;
|
||||
break;
|
||||
case CLIMATE_PRESET_SLEEP:
|
||||
out_data->quiet_mode = 0;
|
||||
out_data->fast_mode = 0;
|
||||
out_data->sleep_mode = 1;
|
||||
out_data->ten_degree = 0;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE("Control", "Unsupported preset");
|
||||
out_data->quiet_mode = 0;
|
||||
out_data->fast_mode = 0;
|
||||
out_data->sleep_mode = 0;
|
||||
out_data->ten_degree = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -595,6 +641,50 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
|
||||
control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
|
||||
}
|
||||
|
||||
void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, bool check_new) {
|
||||
constexpr size_t active_alarms_size = sizeof(this->active_alarms_);
|
||||
if (size >= active_alarms_size + 2) {
|
||||
if (check_new) {
|
||||
size_t alarm_code = 0;
|
||||
for (int i = active_alarms_size - 1; i >= 0; i--) {
|
||||
if (packet[2 + i] != active_alarms_[i]) {
|
||||
uint8_t alarm_bit = 1;
|
||||
for (int b = 0; b < 8; b++) {
|
||||
if ((packet[2 + i] & alarm_bit) != (this->active_alarms_[i] & alarm_bit)) {
|
||||
bool alarm_status = (packet[2 + i] & alarm_bit) != 0;
|
||||
int log_level = alarm_status ? ESPHOME_LOG_LEVEL_WARN : ESPHOME_LOG_LEVEL_INFO;
|
||||
const char *alarm_message = alarm_code < esphome::haier::hon_protocol::HON_ALARM_COUNT
|
||||
? esphome::haier::hon_protocol::HON_ALARM_MESSAGES[alarm_code].c_str()
|
||||
: "Unknown";
|
||||
esp_log_printf_(log_level, TAG, __LINE__, "Alarm %s (%d): %s", alarm_status ? "activated" : "deactivated",
|
||||
alarm_code, alarm_message);
|
||||
if (alarm_status) {
|
||||
this->alarm_start_callback_.call(alarm_code, alarm_message);
|
||||
this->active_alarm_count_ += 1.0f;
|
||||
} else {
|
||||
this->alarm_end_callback_.call(alarm_code, alarm_message);
|
||||
this->active_alarm_count_ -= 1.0f;
|
||||
}
|
||||
}
|
||||
alarm_bit <<= 1;
|
||||
alarm_code++;
|
||||
}
|
||||
active_alarms_[i] = packet[2 + i];
|
||||
} else
|
||||
alarm_code += 8;
|
||||
}
|
||||
} else {
|
||||
float alarm_count = 0.0f;
|
||||
static uint8_t nibble_bits_count[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
|
||||
for (size_t i = 0; i < sizeof(this->active_alarms_); i++) {
|
||||
alarm_count += (float) (nibble_bits_count[packet[2 + i] & 0x0F] + nibble_bits_count[packet[2 + i] >> 4]);
|
||||
}
|
||||
this->active_alarm_count_ = alarm_count;
|
||||
memcpy(this->active_alarms_, packet + 2, sizeof(this->active_alarms_));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
|
||||
if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_)
|
||||
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
|
||||
@ -626,6 +716,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
||||
this->preset = CLIMATE_PRESET_BOOST;
|
||||
} else if (packet.control.sleep_mode != 0) {
|
||||
this->preset = CLIMATE_PRESET_SLEEP;
|
||||
} else if (packet.control.ten_degree != 0) {
|
||||
this->preset = CLIMATE_PRESET_AWAY;
|
||||
} else {
|
||||
this->preset = CLIMATE_PRESET_NONE;
|
||||
}
|
||||
@ -882,25 +974,35 @@ void HonClimate::fill_control_messages_queue_() {
|
||||
// CLimate preset
|
||||
{
|
||||
uint8_t fast_mode_buf[] = {0x00, 0xFF};
|
||||
uint8_t away_mode_buf[] = {0x00, 0xFF};
|
||||
if (!new_power) {
|
||||
// If AC is off - no presets allowed
|
||||
quiet_mode_buf[1] = 0x00;
|
||||
fast_mode_buf[1] = 0x00;
|
||||
away_mode_buf[1] = 0x00;
|
||||
} else if (climate_control.preset.has_value()) {
|
||||
switch (climate_control.preset.value()) {
|
||||
case CLIMATE_PRESET_NONE:
|
||||
quiet_mode_buf[1] = 0x00;
|
||||
fast_mode_buf[1] = 0x00;
|
||||
away_mode_buf[1] = 0x00;
|
||||
break;
|
||||
case CLIMATE_PRESET_ECO:
|
||||
// Eco is not supported in Fan only mode
|
||||
quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
|
||||
fast_mode_buf[1] = 0x00;
|
||||
away_mode_buf[1] = 0x00;
|
||||
break;
|
||||
case CLIMATE_PRESET_BOOST:
|
||||
quiet_mode_buf[1] = 0x00;
|
||||
// Boost is not supported in Fan only mode
|
||||
fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
|
||||
away_mode_buf[1] = 0x00;
|
||||
break;
|
||||
case CLIMATE_PRESET_AWAY:
|
||||
quiet_mode_buf[1] = 0x00;
|
||||
fast_mode_buf[1] = 0x00;
|
||||
away_mode_buf[1] = (this->mode == CLIMATE_MODE_HEAT) ? 0x01 : 0x00;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE("Control", "Unsupported preset");
|
||||
@ -921,6 +1023,13 @@ void HonClimate::fill_control_messages_queue_() {
|
||||
(uint8_t) hon_protocol::DataParameters::FAST_MODE,
|
||||
fast_mode_buf, 2));
|
||||
}
|
||||
if (away_mode_buf[1] != 0xFF) {
|
||||
this->control_messages_queue_.push(
|
||||
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
|
||||
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
|
||||
(uint8_t) hon_protocol::DataParameters::TEN_DEGREE,
|
||||
away_mode_buf, 2));
|
||||
}
|
||||
}
|
||||
// Target temperature
|
||||
if (climate_control.target_temperature.has_value()) {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "haier_base.h"
|
||||
|
||||
namespace esphome {
|
||||
@ -52,6 +53,9 @@ class HonClimate : public HaierClimateBase {
|
||||
void start_steri_cleaning();
|
||||
void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; };
|
||||
void set_control_method(HonControlMethod method) { this->control_method_ = method; };
|
||||
void add_alarm_start_callback(std::function<void(uint8_t, const char *)> &&callback);
|
||||
void add_alarm_end_callback(std::function<void(uint8_t, const char *)> &&callback);
|
||||
float get_active_alarm_count() const { return this->active_alarm_count_; }
|
||||
|
||||
protected:
|
||||
void set_handlers() override;
|
||||
@ -77,8 +81,11 @@ class HonClimate : public HaierClimateBase {
|
||||
haier_protocol::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type,
|
||||
haier_protocol::FrameType message_type,
|
||||
const uint8_t *data, size_t data_size);
|
||||
haier_protocol::HandlerError alarm_status_message_handler_(haier_protocol::FrameType type, const uint8_t *buffer,
|
||||
size_t size);
|
||||
// Helper functions
|
||||
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
|
||||
void process_alarm_message_(const uint8_t *packet, uint8_t size, bool check_new);
|
||||
void fill_control_messages_queue_();
|
||||
void clear_control_messages_queue_();
|
||||
|
||||
@ -101,6 +108,26 @@ class HonClimate : public HaierClimateBase {
|
||||
HonControlMethod control_method_;
|
||||
esphome::sensor::Sensor *outdoor_sensor_;
|
||||
std::queue<haier_protocol::HaierMessage> control_messages_queue_;
|
||||
CallbackManager<void(uint8_t, const char *)> alarm_start_callback_{};
|
||||
CallbackManager<void(uint8_t, const char *)> alarm_end_callback_{};
|
||||
float active_alarm_count_{NAN};
|
||||
std::chrono::steady_clock::time_point last_alarm_request_;
|
||||
};
|
||||
|
||||
class HaierAlarmStartTrigger : public Trigger<uint8_t, const char *> {
|
||||
public:
|
||||
explicit HaierAlarmStartTrigger(HonClimate *parent) {
|
||||
parent->add_alarm_start_callback(
|
||||
[this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); });
|
||||
}
|
||||
};
|
||||
|
||||
class HaierAlarmEndTrigger : public Trigger<uint8_t, const char *> {
|
||||
public:
|
||||
explicit HaierAlarmEndTrigger(HonClimate *parent) {
|
||||
parent->add_alarm_end_callback(
|
||||
[this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace haier
|
||||
|
||||
@ -163,6 +163,62 @@ enum class SubcommandsControl : uint16_t {
|
||||
// content: all values like in status packet)
|
||||
};
|
||||
|
||||
const std::string HON_ALARM_MESSAGES[] = {
|
||||
"Outdoor module failure",
|
||||
"Outdoor defrost sensor failure",
|
||||
"Outdoor compressor exhaust sensor failure",
|
||||
"Outdoor EEPROM abnormality",
|
||||
"Indoor coil sensor failure",
|
||||
"Indoor-outdoor communication failure",
|
||||
"Power supply overvoltage protection",
|
||||
"Communication failure between panel and indoor unit",
|
||||
"Outdoor compressor overheat protection",
|
||||
"Outdoor environmental sensor abnormality",
|
||||
"Full water protection",
|
||||
"Indoor EEPROM failure",
|
||||
"Outdoor out air sensor failure",
|
||||
"CBD and module communication failure",
|
||||
"Indoor DC fan failure",
|
||||
"Outdoor DC fan failure",
|
||||
"Door switch failure",
|
||||
"Dust filter needs cleaning reminder",
|
||||
"Water shortage protection",
|
||||
"Humidity sensor failure",
|
||||
"Indoor temperature sensor failure",
|
||||
"Manipulator limit failure",
|
||||
"Indoor PM2.5 sensor failure",
|
||||
"Outdoor PM2.5 sensor failure",
|
||||
"Indoor heating overload/high load alarm",
|
||||
"Outdoor AC current protection",
|
||||
"Outdoor compressor operation abnormality",
|
||||
"Outdoor DC current protection",
|
||||
"Outdoor no-load failure",
|
||||
"CT current abnormality",
|
||||
"Indoor cooling freeze protection",
|
||||
"High and low pressure protection",
|
||||
"Compressor out air temperature is too high",
|
||||
"Outdoor evaporator sensor failure",
|
||||
"Outdoor cooling overload",
|
||||
"Water pump drainage failure",
|
||||
"Three-phase power supply failure",
|
||||
"Four-way valve failure",
|
||||
"External alarm/scraper flow switch failure",
|
||||
"Temperature cutoff protection alarm",
|
||||
"Different mode operation failure",
|
||||
"Electronic expansion valve failure",
|
||||
"Dual heat source sensor Tw failure",
|
||||
"Communication failure with the wired controller",
|
||||
"Indoor unit address duplication failure",
|
||||
"50Hz zero crossing failure",
|
||||
"Outdoor unit failure",
|
||||
"Formaldehyde sensor failure",
|
||||
"VOC sensor failure",
|
||||
"CO2 sensor failure",
|
||||
"Firewall failure",
|
||||
};
|
||||
|
||||
constexpr size_t HON_ALARM_COUNT = sizeof(HON_ALARM_MESSAGES) / sizeof(HON_ALARM_MESSAGES[0]);
|
||||
|
||||
} // namespace hon_protocol
|
||||
} // namespace haier
|
||||
} // namespace esphome
|
||||
|
||||
@ -95,7 +95,7 @@ haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cyc
|
||||
ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type,
|
||||
phase_to_string_(this->protocol_phase_));
|
||||
ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1);
|
||||
if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST)
|
||||
if (new_phase >= ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST)
|
||||
new_phase = ProtocolPhases::SENDING_INIT_1;
|
||||
this->set_phase(new_phase);
|
||||
return haier_protocol::HandlerError::HANDLER_OK;
|
||||
@ -170,9 +170,12 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
|
||||
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
|
||||
this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL);
|
||||
break;
|
||||
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
|
||||
case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST:
|
||||
this->set_phase(ProtocolPhases::SENDING_INIT_1);
|
||||
break;
|
||||
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
|
||||
this->set_phase(ProtocolPhases::IDLE);
|
||||
break;
|
||||
case ProtocolPhases::SENDING_CONTROL:
|
||||
if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
|
||||
ESP_LOGI(TAG, "Sending control packet");
|
||||
@ -343,19 +346,29 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
|
||||
} else if (climate_control.preset.has_value()) {
|
||||
switch (climate_control.preset.value()) {
|
||||
case CLIMATE_PRESET_NONE:
|
||||
out_data->ten_degree = 0;
|
||||
out_data->turbo_mode = 0;
|
||||
out_data->quiet_mode = 0;
|
||||
break;
|
||||
case CLIMATE_PRESET_BOOST:
|
||||
out_data->ten_degree = 0;
|
||||
out_data->turbo_mode = 1;
|
||||
out_data->quiet_mode = 0;
|
||||
break;
|
||||
case CLIMATE_PRESET_COMFORT:
|
||||
out_data->ten_degree = 0;
|
||||
out_data->turbo_mode = 0;
|
||||
out_data->quiet_mode = 1;
|
||||
break;
|
||||
case CLIMATE_PRESET_AWAY:
|
||||
// Only allowed in heat mode
|
||||
out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0;
|
||||
out_data->turbo_mode = 0;
|
||||
out_data->quiet_mode = 0;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE("Control", "Unsupported preset");
|
||||
out_data->ten_degree = 0;
|
||||
out_data->turbo_mode = 0;
|
||||
out_data->quiet_mode = 0;
|
||||
break;
|
||||
@ -381,6 +394,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
|
||||
this->preset = CLIMATE_PRESET_BOOST;
|
||||
} else if (packet.control.quiet_mode != 0) {
|
||||
this->preset = CLIMATE_PRESET_COMFORT;
|
||||
} else if (packet.control.ten_degree != 0) {
|
||||
this->preset = CLIMATE_PRESET_AWAY;
|
||||
} else {
|
||||
this->preset = CLIMATE_PRESET_NONE;
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@ void HLW8012Component::update() {
|
||||
this->energy_sensor_->publish_state(energy);
|
||||
}
|
||||
|
||||
if (this->change_mode_at_++ == this->change_mode_every_) {
|
||||
if (this->change_mode_every_ != 0 && this->change_mode_at_++ == this->change_mode_every_) {
|
||||
this->current_mode_ = !this->current_mode_;
|
||||
ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE");
|
||||
this->change_mode_at_ = 0;
|
||||
|
||||
@ -79,8 +79,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance,
|
||||
cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float,
|
||||
cv.Optional(CONF_MODEL, default="HLW8012"): cv.enum(MODELS, upper=True),
|
||||
cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.All(
|
||||
cv.uint32_t, cv.Range(min=1)
|
||||
cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.Any(
|
||||
"never",
|
||||
cv.All(cv.uint32_t, cv.Range(min=1)),
|
||||
),
|
||||
cv.Optional(CONF_INITIAL_MODE, default=CONF_VOLTAGE): cv.one_of(
|
||||
*INITIAL_MODES, lower=True
|
||||
@ -114,6 +115,10 @@ async def to_code(config):
|
||||
cg.add(var.set_energy_sensor(sens))
|
||||
cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR]))
|
||||
cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER]))
|
||||
cg.add(var.set_change_mode_every(config[CONF_CHANGE_MODE_EVERY]))
|
||||
cg.add(var.set_initial_mode(INITIAL_MODES[config[CONF_INITIAL_MODE]]))
|
||||
cg.add(var.set_sensor_model(config[CONF_MODEL]))
|
||||
|
||||
interval = config[CONF_CHANGE_MODE_EVERY]
|
||||
if interval == "never":
|
||||
interval = 0
|
||||
cg.add(var.set_change_mode_every(interval))
|
||||
|
||||
2
esphome/components/honeywell_hih_i2c/__init__.py
Normal file
2
esphome/components/honeywell_hih_i2c/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
"""Support for Honeywell HumidIcon HIH"""
|
||||
CODEOWNERS = ["@Benichou34"]
|
||||
97
esphome/components/honeywell_hih_i2c/honeywell_hih.cpp
Normal file
97
esphome/components/honeywell_hih_i2c/honeywell_hih.cpp
Normal file
@ -0,0 +1,97 @@
|
||||
// Honeywell HumidIcon I2C Sensors
|
||||
// https://prod-edam.honeywell.com/content/dam/honeywell-edam/sps/siot/en-us/products/sensors/humidity-with-temperature-sensors/common/documents/sps-siot-i2c-comms-humidicon-tn-009061-2-en-ciid-142171.pdf
|
||||
//
|
||||
|
||||
#include "honeywell_hih.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace honeywell_hih_i2c {
|
||||
|
||||
static const char *const TAG = "honeywell_hih.i2c";
|
||||
|
||||
static const uint8_t REQUEST_CMD[1] = {0x00}; // Measurement Request Format
|
||||
static const uint16_t MAX_COUNT = 0x3FFE; // 2^14 - 2
|
||||
|
||||
void HoneywellHIComponent::read_sensor_data_() {
|
||||
uint8_t data[4];
|
||||
|
||||
if (this->read(data, sizeof(data)) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Communication with Honeywell HIH failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
const uint16_t raw_humidity = (static_cast<uint16_t>(data[0] & 0x3F) << 8) | data[1];
|
||||
float humidity = (static_cast<float>(raw_humidity) / MAX_COUNT) * 100;
|
||||
|
||||
const uint16_t raw_temperature = (static_cast<uint16_t>(data[2]) << 6) | (data[3] >> 2);
|
||||
float temperature = (static_cast<float>(raw_temperature) / MAX_COUNT) * 165 - 40;
|
||||
|
||||
ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
}
|
||||
|
||||
void HoneywellHIComponent::start_measurement_() {
|
||||
if (this->write(REQUEST_CMD, sizeof(REQUEST_CMD)) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Communication with Honeywell HIH failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->measurement_running_ = true;
|
||||
}
|
||||
|
||||
bool HoneywellHIComponent::is_measurement_ready_() {
|
||||
uint8_t data[1];
|
||||
|
||||
if (this->read(data, sizeof(data)) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Communication with Honeywell HIH failed!");
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check status bits
|
||||
return ((data[0] & 0xC0) == 0x00);
|
||||
}
|
||||
|
||||
void HoneywellHIComponent::measurement_timeout_() {
|
||||
ESP_LOGE(TAG, "Honeywell HIH Timeout!");
|
||||
this->measurement_running_ = false;
|
||||
this->mark_failed();
|
||||
}
|
||||
|
||||
void HoneywellHIComponent::update() {
|
||||
ESP_LOGV(TAG, "Update Honeywell HIH Sensor");
|
||||
|
||||
this->start_measurement_();
|
||||
// The measurement cycle duration is typically 36.65 ms for temperature and humidity readings.
|
||||
this->set_timeout("meas_timeout", 100, [this] { this->measurement_timeout_(); });
|
||||
}
|
||||
|
||||
void HoneywellHIComponent::loop() {
|
||||
if (this->measurement_running_ && this->is_measurement_ready_()) {
|
||||
this->measurement_running_ = false;
|
||||
this->cancel_timeout("meas_timeout");
|
||||
this->read_sensor_data_();
|
||||
}
|
||||
}
|
||||
|
||||
void HoneywellHIComponent::dump_config() {
|
||||
ESP_LOGD(TAG, "Honeywell HIH:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication with Honeywell HIH failed!");
|
||||
}
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
float HoneywellHIComponent::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
} // namespace honeywell_hih_i2c
|
||||
} // namespace esphome
|
||||
34
esphome/components/honeywell_hih_i2c/honeywell_hih.h
Normal file
34
esphome/components/honeywell_hih_i2c/honeywell_hih.h
Normal file
@ -0,0 +1,34 @@
|
||||
// Honeywell HumidIcon I2C Sensors
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace honeywell_hih_i2c {
|
||||
|
||||
class HoneywellHIComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void loop() override;
|
||||
void update() override;
|
||||
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
|
||||
protected:
|
||||
bool measurement_running_{false};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
|
||||
private:
|
||||
void read_sensor_data_();
|
||||
void start_measurement_();
|
||||
bool is_measurement_ready_();
|
||||
void measurement_timeout_();
|
||||
};
|
||||
|
||||
} // namespace honeywell_hih_i2c
|
||||
} // namespace esphome
|
||||
56
esphome/components/honeywell_hih_i2c/sensor.py
Normal file
56
esphome/components/honeywell_hih_i2c/sensor.py
Normal file
@ -0,0 +1,56 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
honeywell_hih_ns = cg.esphome_ns.namespace("honeywell_hih_i2c")
|
||||
HONEYWELLHIComponent = honeywell_hih_ns.class_(
|
||||
"HoneywellHIComponent", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HONEYWELLHIComponent),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x27))
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temperature_config)
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
|
||||
if humidity_config := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(humidity_config)
|
||||
cg.add(var.set_humidity_sensor(sens))
|
||||
@ -4,8 +4,10 @@ from esphome.const import (
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_HOST,
|
||||
CONF_MAC_ADDRESS,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import IS_MACOS
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
|
||||
@ -14,7 +16,6 @@ from .const import KEY_HOST
|
||||
# force import gpio to register pin schema
|
||||
from .gpio import host_pin_to_code # noqa
|
||||
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
AUTO_LOAD = ["network"]
|
||||
|
||||
@ -28,12 +29,21 @@ def set_core_data(config):
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema({}),
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MAC_ADDRESS, default="98:35:69:ab:f6:79"): cv.mac_address,
|
||||
}
|
||||
),
|
||||
set_core_data,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
cg.add_build_flag("-DUSE_HOST")
|
||||
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
|
||||
cg.add_build_flag("-std=c++17")
|
||||
cg.add_build_flag("-lsodium")
|
||||
if IS_MACOS:
|
||||
cg.add_build_flag("-L/opt/homebrew/lib")
|
||||
cg.add_define("ESPHOME_BOARD", "host")
|
||||
cg.add_platformio_option("platform", "platformio/native")
|
||||
|
||||
@ -17,6 +17,12 @@ void HydreonRGxxComponent::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!");
|
||||
}
|
||||
if (model_ == RG9) {
|
||||
ESP_LOGCONFIG(TAG, " Model: RG9");
|
||||
ESP_LOGCONFIG(TAG, " Disable Led: %s", TRUEFALSE(this->disable_led_));
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Model: RG15");
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
int i = 0;
|
||||
@ -25,10 +31,6 @@ void HydreonRGxxComponent::dump_config() {
|
||||
LOG_SENSOR(" ", #s, this->sensors_[i - 1]); \
|
||||
}
|
||||
HYDREON_RGXX_PROTOCOL_LIST(HYDREON_RGXX_LOG_SENSOR, );
|
||||
|
||||
if (this->model_ == RG9) {
|
||||
ESP_LOGCONFIG(TAG, "disable_led: %s", TRUEFALSE(this->disable_led_));
|
||||
}
|
||||
}
|
||||
|
||||
void HydreonRGxxComponent::setup() {
|
||||
|
||||
@ -138,6 +138,7 @@ async def to_code(config):
|
||||
sens = await sensor.new_sensor(config[conf])
|
||||
cg.add(var.set_sensor(sens, i))
|
||||
|
||||
cg.add(var.set_model(config[CONF_MODEL]))
|
||||
cg.add(var.set_request_temperature(CONF_TEMPERATURE in config))
|
||||
|
||||
if CONF_DISABLE_LED in config:
|
||||
|
||||
@ -20,7 +20,9 @@ DEPENDENCIES = ["i2s_audio"]
|
||||
CONF_ADC_PIN = "adc_pin"
|
||||
CONF_ADC_TYPE = "adc_type"
|
||||
CONF_PDM = "pdm"
|
||||
CONF_SAMPLE_RATE = "sample_rate"
|
||||
CONF_BITS_PER_SAMPLE = "bits_per_sample"
|
||||
CONF_USE_APLL = "use_apll"
|
||||
|
||||
I2SAudioMicrophone = i2s_audio_ns.class_(
|
||||
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
|
||||
@ -62,9 +64,11 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
|
||||
cv.GenerateID(): cv.declare_id(I2SAudioMicrophone),
|
||||
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
|
||||
cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS),
|
||||
cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1),
|
||||
cv.Optional(CONF_BITS_PER_SAMPLE, default="32bit"): cv.All(
|
||||
_validate_bits, cv.enum(BITS_PER_SAMPLE)
|
||||
),
|
||||
cv.Optional(CONF_USE_APLL, default=False): cv.boolean,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
@ -105,6 +109,8 @@ async def to_code(config):
|
||||
cg.add(var.set_pdm(config[CONF_PDM]))
|
||||
|
||||
cg.add(var.set_channel(config[CONF_CHANNEL]))
|
||||
cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))
|
||||
cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
|
||||
cg.add(var.set_use_apll(config[CONF_USE_APLL]))
|
||||
|
||||
await microphone.register_microphone(var, config)
|
||||
|
||||
@ -47,14 +47,14 @@ void I2SAudioMicrophone::start_() {
|
||||
}
|
||||
i2s_driver_config_t config = {
|
||||
.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX),
|
||||
.sample_rate = 16000,
|
||||
.sample_rate = this->sample_rate_,
|
||||
.bits_per_sample = this->bits_per_sample_,
|
||||
.channel_format = this->channel_,
|
||||
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
||||
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
||||
.dma_buf_count = 4,
|
||||
.dma_buf_len = 256,
|
||||
.use_apll = false,
|
||||
.use_apll = this->use_apll_,
|
||||
.tx_desc_auto_clear = false,
|
||||
.fixed_mclk = 0,
|
||||
.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT,
|
||||
|
||||
@ -31,7 +31,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
||||
#endif
|
||||
|
||||
void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; }
|
||||
void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; }
|
||||
void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
|
||||
void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; }
|
||||
|
||||
protected:
|
||||
void start_();
|
||||
@ -45,7 +47,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
||||
#endif
|
||||
bool pdm_{false};
|
||||
i2s_channel_fmt_t channel_;
|
||||
uint32_t sample_rate_;
|
||||
i2s_bits_per_sample_t bits_per_sample_;
|
||||
bool use_apll_;
|
||||
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
};
|
||||
|
||||
@ -17,6 +17,14 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
CONF_HEIGHT,
|
||||
CONF_ROTATION,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_SWAP_XY,
|
||||
CONF_COLOR_ORDER,
|
||||
CONF_OFFSET_HEIGHT,
|
||||
CONF_OFFSET_WIDTH,
|
||||
CONF_TRANSFORM,
|
||||
CONF_INVERT_COLORS,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
@ -58,6 +66,7 @@ MODELS = {
|
||||
"ST7789V": ili9xxx_ns.class_("ILI9XXXST7789V", ILI9XXXDisplay),
|
||||
"S3BOX": ili9xxx_ns.class_("ILI9XXXS3Box", ILI9XXXDisplay),
|
||||
"S3BOX_LITE": ili9xxx_ns.class_("ILI9XXXS3BoxLite", ILI9XXXDisplay),
|
||||
"WAVESHARE_RES_3_5": ili9xxx_ns.class_("WAVESHARERES35", ILI9XXXDisplay),
|
||||
}
|
||||
|
||||
COLOR_ORDERS = {
|
||||
@ -70,14 +79,6 @@ COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE")
|
||||
CONF_LED_PIN = "led_pin"
|
||||
CONF_COLOR_PALETTE_IMAGES = "color_palette_images"
|
||||
CONF_INVERT_DISPLAY = "invert_display"
|
||||
CONF_INVERT_COLORS = "invert_colors"
|
||||
CONF_MIRROR_X = "mirror_x"
|
||||
CONF_MIRROR_Y = "mirror_y"
|
||||
CONF_SWAP_XY = "swap_xy"
|
||||
CONF_COLOR_ORDER = "color_order"
|
||||
CONF_OFFSET_HEIGHT = "offset_height"
|
||||
CONF_OFFSET_WIDTH = "offset_width"
|
||||
CONF_TRANSFORM = "transform"
|
||||
|
||||
|
||||
def _validate(config):
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
namespace esphome {
|
||||
namespace ili9xxx {
|
||||
|
||||
static const char *const TAG = "ili9xxx";
|
||||
static const uint16_t SPI_SETUP_US = 100; // estimated fixed overhead in microseconds for an SPI write
|
||||
static const uint16_t SPI_MAX_BLOCK_SIZE = 4092; // Max size of continuous SPI transfer
|
||||
|
||||
@ -17,13 +16,7 @@ static inline void put16_be(uint8_t *buf, uint16_t value) {
|
||||
buf[1] = value;
|
||||
}
|
||||
|
||||
void ILI9XXXDisplay::setup() {
|
||||
ESP_LOGD(TAG, "Setting up ILI9xxx");
|
||||
|
||||
this->setup_pins_();
|
||||
this->init_lcd_();
|
||||
|
||||
this->command(this->pre_invertcolors_ ? ILI9XXX_INVON : ILI9XXX_INVOFF);
|
||||
void ILI9XXXDisplay::set_madctl() {
|
||||
// custom x/y transform and color order
|
||||
uint8_t mad = this->color_order_ == display::COLOR_ORDER_BGR ? MADCTL_BGR : MADCTL_RGB;
|
||||
if (this->swap_xy_)
|
||||
@ -32,8 +25,19 @@ void ILI9XXXDisplay::setup() {
|
||||
mad |= MADCTL_MX;
|
||||
if (this->mirror_y_)
|
||||
mad |= MADCTL_MY;
|
||||
this->send_command(ILI9XXX_MADCTL, &mad, 1);
|
||||
this->command(ILI9XXX_MADCTL);
|
||||
this->data(mad);
|
||||
esph_log_d(TAG, "Wrote MADCTL 0x%02X", mad);
|
||||
}
|
||||
|
||||
void ILI9XXXDisplay::setup() {
|
||||
ESP_LOGD(TAG, "Setting up ILI9xxx");
|
||||
|
||||
this->setup_pins_();
|
||||
this->init_lcd_();
|
||||
|
||||
this->set_madctl();
|
||||
this->command(this->pre_invertcolors_ ? ILI9XXX_INVON : ILI9XXX_INVOFF);
|
||||
this->x_low_ = this->width_;
|
||||
this->y_low_ = this->height_;
|
||||
this->x_high_ = 0;
|
||||
@ -89,6 +93,7 @@ void ILI9XXXDisplay::dump_config() {
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Color order: %s", this->color_order_ == display::COLOR_ORDER_BGR ? "BGR" : "RGB");
|
||||
ESP_LOGCONFIG(TAG, " Swap_xy: %s", YESNO(this->swap_xy_));
|
||||
ESP_LOGCONFIG(TAG, " Mirror_x: %s", YESNO(this->mirror_x_));
|
||||
ESP_LOGCONFIG(TAG, " Mirror_y: %s", YESNO(this->mirror_y_));
|
||||
@ -196,7 +201,6 @@ void ILI9XXXDisplay::display_() {
|
||||
uint8_t transfer_buffer[ILI9XXX_TRANSFER_BUFFER_SIZE];
|
||||
// check if something was displayed
|
||||
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) {
|
||||
ESP_LOGV(TAG, "Nothing to display");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -211,14 +215,13 @@ void ILI9XXXDisplay::display_() {
|
||||
size_t mw_time = (w * h * 16) / mhz + w * h * 2 / ILI9XXX_TRANSFER_BUFFER_SIZE * SPI_SETUP_US;
|
||||
ESP_LOGV(TAG,
|
||||
"Start display(xlow:%d, ylow:%d, xhigh:%d, yhigh:%d, width:%d, "
|
||||
"height:%d, mode=%d, 18bit=%d, sw_time=%dus, mw_time=%dus)",
|
||||
"height:%zu, mode=%d, 18bit=%d, sw_time=%zuus, mw_time=%zuus)",
|
||||
this->x_low_, this->y_low_, this->x_high_, this->y_high_, w, h, this->buffer_color_mode_,
|
||||
this->is_18bitdisplay_, sw_time, mw_time);
|
||||
auto now = millis();
|
||||
this->enable();
|
||||
if (this->buffer_color_mode_ == BITS_16 && !this->is_18bitdisplay_ && sw_time < mw_time) {
|
||||
// 16 bit mode maps directly to display format
|
||||
ESP_LOGV(TAG, "Doing single write of %d bytes", this->width_ * h * 2);
|
||||
ESP_LOGV(TAG, "Doing single write of %zu bytes", this->width_ * h * 2);
|
||||
set_addr_window_(0, this->y_low_, this->width_ - 1, this->y_high_);
|
||||
this->write_array(this->buffer_ + this->y_low_ * this->width_ * 2, h * this->width_ * 2);
|
||||
} else {
|
||||
@ -267,7 +270,7 @@ void ILI9XXXDisplay::display_() {
|
||||
this->write_array(transfer_buffer, idx);
|
||||
}
|
||||
}
|
||||
this->disable();
|
||||
this->end_data_();
|
||||
ESP_LOGV(TAG, "Data write took %dms", (unsigned) (millis() - now));
|
||||
// invalidate watermarks
|
||||
this->x_low_ = this->width_;
|
||||
@ -276,6 +279,34 @@ void ILI9XXXDisplay::display_() {
|
||||
this->y_high_ = 0;
|
||||
}
|
||||
|
||||
// note that this bypasses the buffer and writes directly to the display.
|
||||
void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr,
|
||||
display::ColorOrder order, display::ColorBitness bitness, bool big_endian,
|
||||
int x_offset, int y_offset, int x_pad) {
|
||||
if (w <= 0 || h <= 0)
|
||||
return;
|
||||
// if color mapping or software rotation is required, hand this off to the parent implementation. This will
|
||||
// do color conversion pixel-by-pixel into the buffer and draw it later. If this is happening the user has not
|
||||
// configured the renderer well.
|
||||
if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || !big_endian ||
|
||||
this->is_18bitdisplay_) {
|
||||
return display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
}
|
||||
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
|
||||
// x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display.
|
||||
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
|
||||
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother
|
||||
this->write_array(ptr, w * h * 2);
|
||||
} else {
|
||||
auto stride = x_offset + w + x_pad;
|
||||
for (size_t y = 0; y != h; y++) {
|
||||
this->write_array(ptr + (y + y_offset) * stride + x_offset, w * 2);
|
||||
}
|
||||
}
|
||||
this->end_data_();
|
||||
}
|
||||
|
||||
// should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color
|
||||
// values per bit is huge
|
||||
uint32_t ILI9XXXDisplay::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal(); }
|
||||
@ -299,20 +330,6 @@ void ILI9XXXDisplay::send_command(uint8_t command_byte, const uint8_t *data_byte
|
||||
this->end_data_();
|
||||
}
|
||||
|
||||
uint8_t ILI9XXXDisplay::read_command(uint8_t command_byte, uint8_t index) {
|
||||
uint8_t data = 0x10 + index;
|
||||
this->send_command(0xD9, &data, 1); // Set Index Register
|
||||
uint8_t result;
|
||||
this->start_command_();
|
||||
this->write_byte(command_byte);
|
||||
this->start_data_();
|
||||
do {
|
||||
result = this->read_byte();
|
||||
} while (index--);
|
||||
this->end_data_();
|
||||
return result;
|
||||
}
|
||||
|
||||
void ILI9XXXDisplay::start_command_() {
|
||||
this->dc_pin_->digital_write(false);
|
||||
this->enable();
|
||||
@ -328,9 +345,9 @@ void ILI9XXXDisplay::end_data_() { this->disable(); }
|
||||
void ILI9XXXDisplay::reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
delay(20);
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(10);
|
||||
delay(20);
|
||||
}
|
||||
}
|
||||
|
||||
@ -340,7 +357,7 @@ void ILI9XXXDisplay::init_lcd_() {
|
||||
while ((cmd = *addr++) > 0) {
|
||||
x = *addr++;
|
||||
num_args = x & 0x7F;
|
||||
send_command(cmd, addr, num_args);
|
||||
this->send_command(cmd, addr, num_args);
|
||||
addr += num_args;
|
||||
if (x & 0x80)
|
||||
delay(150); // NOLINT
|
||||
@ -348,24 +365,23 @@ void ILI9XXXDisplay::init_lcd_() {
|
||||
}
|
||||
|
||||
// Tell the display controller where we want to draw pixels.
|
||||
// when called, the SPI should have already been enabled, only the D/C pin will be toggled here.
|
||||
void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
|
||||
uint8_t buf[4];
|
||||
this->dc_pin_->digital_write(false);
|
||||
this->write_byte(ILI9XXX_CASET); // Column address set
|
||||
put16_be(buf, x1 + this->offset_x_);
|
||||
put16_be(buf + 2, x2 + this->offset_x_);
|
||||
this->dc_pin_->digital_write(true);
|
||||
this->write_array(buf, sizeof buf);
|
||||
this->dc_pin_->digital_write(false);
|
||||
this->write_byte(ILI9XXX_PASET); // Row address set
|
||||
put16_be(buf, y1 + this->offset_y_);
|
||||
put16_be(buf + 2, y2 + this->offset_y_);
|
||||
this->dc_pin_->digital_write(true);
|
||||
this->write_array(buf, sizeof buf);
|
||||
this->dc_pin_->digital_write(false);
|
||||
this->write_byte(ILI9XXX_RAMWR); // Write to RAM
|
||||
this->dc_pin_->digital_write(true);
|
||||
x1 += this->offset_x_;
|
||||
x2 += this->offset_x_;
|
||||
y1 += this->offset_y_;
|
||||
y2 += this->offset_y_;
|
||||
this->command(ILI9XXX_CASET);
|
||||
this->data(x1 >> 8);
|
||||
this->data(x1 & 0xFF);
|
||||
this->data(x2 >> 8);
|
||||
this->data(x2 & 0xFF);
|
||||
this->command(ILI9XXX_PASET); // Page address set
|
||||
this->data(y1 >> 8);
|
||||
this->data(y1 & 0xFF);
|
||||
this->data(y2 >> 8);
|
||||
this->data(y2 & 0xFF);
|
||||
this->command(ILI9XXX_RAMWR); // Write to RAM
|
||||
this->start_data_();
|
||||
}
|
||||
|
||||
void ILI9XXXDisplay::invert_colors(bool invert) {
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
#pragma once
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "ili9xxx_defines.h"
|
||||
#include "ili9xxx_init.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ili9xxx {
|
||||
|
||||
static const char *const TAG = "ili9xxx";
|
||||
const size_t ILI9XXX_TRANSFER_BUFFER_SIZE = 126; // ensure this is divisible by 6
|
||||
|
||||
enum ILI9XXXColorMode {
|
||||
@ -31,6 +33,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
|
||||
while ((cmd = *addr++) != 0) {
|
||||
num_args = *addr++ & 0x7F;
|
||||
bits = *addr;
|
||||
esph_log_d(TAG, "Command %02X, length %d, bits %02X", cmd, num_args, bits);
|
||||
switch (cmd) {
|
||||
case ILI9XXX_MADCTL: {
|
||||
this->swap_xy_ = (bits & MADCTL_MV) != 0;
|
||||
@ -67,10 +70,9 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
|
||||
this->offset_y_ = offset_y;
|
||||
}
|
||||
void invert_colors(bool invert);
|
||||
void command(uint8_t value);
|
||||
void data(uint8_t value);
|
||||
virtual void command(uint8_t value);
|
||||
virtual void data(uint8_t value);
|
||||
void send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes);
|
||||
uint8_t read_command(uint8_t command_byte, uint8_t index);
|
||||
void set_color_order(display::ColorOrder color_order) { this->color_order_ = color_order; }
|
||||
void set_swap_xy(bool swap_xy) { this->swap_xy_ = swap_xy; }
|
||||
void set_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; }
|
||||
@ -84,11 +86,14 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
|
||||
void setup() override;
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
void setup_pins_();
|
||||
|
||||
virtual void set_madctl();
|
||||
void display_();
|
||||
void init_lcd_();
|
||||
void set_addr_window_(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2);
|
||||
@ -124,7 +129,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
|
||||
bool need_update_ = false;
|
||||
bool is_18bitdisplay_ = false;
|
||||
bool pre_invertcolors_ = false;
|
||||
display::ColorOrder color_order_{};
|
||||
display::ColorOrder color_order_{display::COLOR_ORDER_BGR};
|
||||
bool swap_xy_{};
|
||||
bool mirror_x_{};
|
||||
bool mirror_y_{};
|
||||
@ -178,10 +183,48 @@ class ILI9XXXILI9486 : public ILI9XXXDisplay {
|
||||
ILI9XXXILI9486() : ILI9XXXDisplay(INITCMD_ILI9486, 480, 320, false) {}
|
||||
};
|
||||
|
||||
//----------- ILI9XXX_35_TFT rotated display --------------
|
||||
class ILI9XXXILI9488 : public ILI9XXXDisplay {
|
||||
public:
|
||||
ILI9XXXILI9488() : ILI9XXXDisplay(INITCMD_ILI9488, 480, 320, true) {}
|
||||
ILI9XXXILI9488(const uint8_t *seq = INITCMD_ILI9488) : ILI9XXXDisplay(seq, 480, 320, true) {}
|
||||
|
||||
protected:
|
||||
void set_madctl() override {
|
||||
uint8_t mad = this->color_order_ == display::COLOR_ORDER_BGR ? MADCTL_BGR : MADCTL_RGB;
|
||||
uint8_t dfun = 0x22;
|
||||
this->width_ = 320;
|
||||
this->height_ = 480;
|
||||
if (!(this->swap_xy_ || this->mirror_x_ || this->mirror_y_)) {
|
||||
// no transforms
|
||||
} else if (this->mirror_y_ && this->mirror_x_) {
|
||||
// rotate 180
|
||||
dfun = 0x42;
|
||||
} else if (this->swap_xy_) {
|
||||
this->width_ = 480;
|
||||
this->height_ = 320;
|
||||
mad |= 0x20;
|
||||
if (this->mirror_x_) {
|
||||
dfun = 0x02;
|
||||
} else {
|
||||
dfun = 0x62;
|
||||
}
|
||||
}
|
||||
this->command(ILI9XXX_DFUNCTR);
|
||||
this->data(0);
|
||||
this->data(dfun);
|
||||
this->command(ILI9XXX_MADCTL);
|
||||
this->data(mad);
|
||||
}
|
||||
};
|
||||
//----------- Waveshare 3.5 Res Touch - ILI9488 interfaced via 16 bit shift register to parallel */
|
||||
class WAVESHARERES35 : public ILI9XXXILI9488 {
|
||||
public:
|
||||
WAVESHARERES35() : ILI9XXXILI9488(INITCMD_WAVESHARE_RES_3_5) {}
|
||||
void data(uint8_t value) override {
|
||||
this->start_data_();
|
||||
this->write_byte(0);
|
||||
this->write_byte(value);
|
||||
this->end_data_();
|
||||
}
|
||||
};
|
||||
|
||||
//----------- ILI9XXX_35_TFT origin colors rotated display --------------
|
||||
|
||||
@ -141,7 +141,8 @@ static const uint8_t PROGMEM INITCMD_ILI9486[] = {
|
||||
0x00 // End of list
|
||||
};
|
||||
|
||||
static const uint8_t PROGMEM INITCMD_ILI9488[] = {
|
||||
|
||||
static const uint8_t INITCMD_ILI9488[] = {
|
||||
ILI9XXX_GMCTRP1,15, 0x0f, 0x24, 0x1c, 0x0a, 0x0f, 0x08, 0x43, 0x88, 0x32, 0x0f, 0x10, 0x06, 0x0f, 0x07, 0x00,
|
||||
ILI9XXX_GMCTRN1,15, 0x0F, 0x38, 0x30, 0x09, 0x0f, 0x0f, 0x4e, 0x77, 0x3c, 0x07, 0x10, 0x05, 0x23, 0x1b, 0x00,
|
||||
|
||||
@ -153,28 +154,27 @@ static const uint8_t PROGMEM INITCMD_ILI9488[] = {
|
||||
ILI9XXX_FRMCTR1, 1, 0xA0, // Frame rate = 60Hz
|
||||
ILI9XXX_INVCTR, 1, 0x02, // Display Inversion Control = 2dot
|
||||
|
||||
ILI9XXX_DFUNCTR, 2, 0x02, 0x02, // Nomal scan
|
||||
|
||||
0xE9, 1, 0x00, // Set Image Functio. Disable 24 bit data
|
||||
|
||||
ILI9XXX_ADJCTL3, 4, 0xA9, 0x51, 0x2C, 0x82, // Adjust Control 3
|
||||
|
||||
ILI9XXX_MADCTL, 1, 0x28,
|
||||
//ILI9XXX_PIXFMT, 1, 0x55, // Interface Pixel Format = 16bit
|
||||
ILI9XXX_PIXFMT, 1, 0x66, //ILI9488 only supports 18-bit pixel format in 4/3 wire SPI mode
|
||||
|
||||
|
||||
|
||||
// 5 frames
|
||||
//ILI9XXX_ETMOD, 1, 0xC6, //
|
||||
|
||||
|
||||
ILI9XXX_SLPOUT, 0x80, // Exit sleep mode
|
||||
//ILI9XXX_INVON , 0,
|
||||
ILI9XXX_DISPON, 0x80, // Set display on
|
||||
0x00 // end
|
||||
};
|
||||
|
||||
static const uint8_t INITCMD_WAVESHARE_RES_3_5[] = {
|
||||
ILI9XXX_PWCTR3, 1, 0x33,
|
||||
ILI9XXX_VMCTR1, 3, 0x00, 0x1e, 0x80,
|
||||
ILI9XXX_FRMCTR1, 1, 0xA0,
|
||||
ILI9XXX_GMCTRP1, 15, 0x0, 0x13, 0x18, 0x04, 0x0F, 0x06, 0x3a, 0x56, 0x4d, 0x03, 0x0a, 0x06, 0x30, 0x3e, 0x0f,
|
||||
ILI9XXX_GMCTRN1, 15, 0x0, 0x13, 0x18, 0x01, 0x11, 0x06, 0x38, 0x34, 0x4d, 0x06, 0x0d, 0x0b, 0x31, 0x37, 0x0f,
|
||||
ILI9XXX_PIXFMT, 1, 0x55,
|
||||
ILI9XXX_SLPOUT, 0x80, // slpout, delay
|
||||
ILI9XXX_DISPON, 0,
|
||||
0x00 // End of list
|
||||
};
|
||||
|
||||
static const uint8_t PROGMEM INITCMD_ILI9488_A[] = {
|
||||
ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F,
|
||||
ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F,
|
||||
|
||||
@ -37,6 +37,7 @@ class Image : public display::BaseImage {
|
||||
Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const;
|
||||
int get_width() const override;
|
||||
int get_height() const override;
|
||||
const uint8_t *get_data_start() { return this->data_start_; }
|
||||
ImageType get_type() const;
|
||||
|
||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||
|
||||
@ -1 +0,0 @@
|
||||
CODEOWNERS = ["@Cat-Ion"]
|
||||
@ -1,82 +0,0 @@
|
||||
#include "kalman_combinator.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
|
||||
namespace esphome {
|
||||
namespace kalman_combinator {
|
||||
|
||||
void KalmanCombinatorComponent::dump_config() {
|
||||
ESP_LOGCONFIG("kalman_combinator", "Kalman Combinator:");
|
||||
ESP_LOGCONFIG("kalman_combinator", " Update variance: %f per ms", this->update_variance_value_);
|
||||
ESP_LOGCONFIG("kalman_combinator", " Sensors:");
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
auto &entity = *sensor.first;
|
||||
ESP_LOGCONFIG("kalman_combinator", " - %s", entity.get_name().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void KalmanCombinatorComponent::setup() {
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
const auto stddev = sensor.second;
|
||||
sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); });
|
||||
}
|
||||
}
|
||||
|
||||
void KalmanCombinatorComponent::add_source(Sensor *sensor, std::function<float(float)> const &stddev) {
|
||||
this->sensors_.emplace_back(sensor, stddev);
|
||||
}
|
||||
|
||||
void KalmanCombinatorComponent::add_source(Sensor *sensor, float stddev) {
|
||||
this->add_source(sensor, std::function<float(float)>{[stddev](float x) -> float { return stddev; }});
|
||||
}
|
||||
|
||||
void KalmanCombinatorComponent::update_variance_() {
|
||||
uint32_t now = millis();
|
||||
|
||||
// Variance increases by update_variance_ each millisecond
|
||||
auto dt = now - this->last_update_;
|
||||
auto dv = this->update_variance_value_ * dt;
|
||||
this->variance_ += dv;
|
||||
this->last_update_ = now;
|
||||
}
|
||||
|
||||
void KalmanCombinatorComponent::correct_(float value, float stddev) {
|
||||
if (std::isnan(value) || std::isinf(stddev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::isnan(this->state_) || std::isinf(this->variance_)) {
|
||||
this->state_ = value;
|
||||
this->variance_ = stddev * stddev;
|
||||
if (this->std_dev_sensor_ != nullptr) {
|
||||
this->std_dev_sensor_->publish_state(stddev);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this->update_variance_();
|
||||
|
||||
// Combine two gaussian distributions mu1+-var1, mu2+-var2 to a new one around mu
|
||||
// Use the value with the smaller variance as mu1 to prevent precision errors
|
||||
const bool this_first = this->variance_ < (stddev * stddev);
|
||||
const float mu1 = this_first ? this->state_ : value;
|
||||
const float mu2 = this_first ? value : this->state_;
|
||||
|
||||
const float var1 = this_first ? this->variance_ : stddev * stddev;
|
||||
const float var2 = this_first ? stddev * stddev : this->variance_;
|
||||
|
||||
const float mu = mu1 + var1 * (mu2 - mu1) / (var1 + var2);
|
||||
const float var = var1 - (var1 * var1) / (var1 + var2);
|
||||
|
||||
// Update and publish state
|
||||
this->state_ = mu;
|
||||
this->variance_ = var;
|
||||
|
||||
this->publish_state(mu);
|
||||
if (this->std_dev_sensor_ != nullptr) {
|
||||
this->std_dev_sensor_->publish_state(std::sqrt(var));
|
||||
}
|
||||
}
|
||||
} // namespace kalman_combinator
|
||||
} // namespace esphome
|
||||
@ -1,46 +0,0 @@
|
||||
#pragma once
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace kalman_combinator {
|
||||
|
||||
class KalmanCombinatorComponent : public Component, public sensor::Sensor {
|
||||
public:
|
||||
KalmanCombinatorComponent() = default;
|
||||
|
||||
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
|
||||
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
|
||||
void add_source(Sensor *sensor, std::function<float(float)> const &stddev);
|
||||
void add_source(Sensor *sensor, float stddev);
|
||||
void set_process_std_dev(float process_std_dev) {
|
||||
this->update_variance_value_ = process_std_dev * process_std_dev * 0.001f;
|
||||
}
|
||||
void set_std_dev_sensor(Sensor *sensor) { this->std_dev_sensor_ = sensor; }
|
||||
|
||||
private:
|
||||
void update_variance_();
|
||||
void correct_(float value, float stddev);
|
||||
|
||||
// Source sensors and their error functions
|
||||
std::vector<std::pair<Sensor *, std::function<float(float)>>> sensors_;
|
||||
|
||||
// Optional sensor for publishing the current error
|
||||
sensor::Sensor *std_dev_sensor_{nullptr};
|
||||
|
||||
// Tick of the last update
|
||||
uint32_t last_update_{0};
|
||||
// Change of the variance, per ms
|
||||
float update_variance_value_{0.f};
|
||||
|
||||
// Best guess for the state and its variance
|
||||
float state_{NAN};
|
||||
float variance_{INFINITY};
|
||||
};
|
||||
} // namespace kalman_combinator
|
||||
} // namespace esphome
|
||||
@ -1,90 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_SOURCE,
|
||||
CONF_ACCURACY_DECIMALS,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
|
||||
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid(
|
||||
"The kalman_combinator sensor has moved.\nPlease use the combination platform instead with type: kalman.\n"
|
||||
"See https://esphome.io/components/sensor/combination.html"
|
||||
)
|
||||
from esphome.core.entity_helpers import inherit_property_from
|
||||
|
||||
kalman_combinator_ns = cg.esphome_ns.namespace("kalman_combinator")
|
||||
KalmanCombinatorComponent = kalman_combinator_ns.class_(
|
||||
"KalmanCombinatorComponent", cg.Component, sensor.Sensor
|
||||
)
|
||||
|
||||
CONF_ERROR = "error"
|
||||
CONF_SOURCES = "sources"
|
||||
CONF_PROCESS_STD_DEV = "process_std_dev"
|
||||
CONF_STD_DEV = "std_dev"
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(KalmanCombinatorComponent)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float,
|
||||
cv.Required(CONF_SOURCES): cv.ensure_list(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
|
||||
cv.Required(CONF_ERROR): cv.templatable(cv.positive_float),
|
||||
}
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_STD_DEV): sensor.sensor_schema(),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Inherit some sensor values from the first source, for both the state and the error value
|
||||
properties_to_inherit = [
|
||||
CONF_ACCURACY_DECIMALS,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
# CONF_STATE_CLASS could also be inherited, but might lead to unexpected behaviour with "total_increasing"
|
||||
]
|
||||
inherit_schema_for_state = [
|
||||
inherit_property_from(property, [CONF_SOURCES, 0, CONF_SOURCE])
|
||||
for property in properties_to_inherit
|
||||
]
|
||||
inherit_schema_for_std_dev = [
|
||||
inherit_property_from([CONF_STD_DEV, property], [CONF_SOURCES, 0, CONF_SOURCE])
|
||||
for property in properties_to_inherit
|
||||
]
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
CONFIG_SCHEMA.extend(
|
||||
{cv.Required(CONF_ID): cv.use_id(KalmanCombinatorComponent)},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
),
|
||||
*inherit_schema_for_state,
|
||||
*inherit_schema_for_std_dev,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
|
||||
cg.add(var.set_process_std_dev(config[CONF_PROCESS_STD_DEV]))
|
||||
for source_conf in config[CONF_SOURCES]:
|
||||
source = await cg.get_variable(source_conf[CONF_SOURCE])
|
||||
error = await cg.templatable(
|
||||
source_conf[CONF_ERROR],
|
||||
[(float, "x")],
|
||||
cg.float_,
|
||||
)
|
||||
cg.add(var.add_source(source, error))
|
||||
|
||||
if CONF_STD_DEV in config:
|
||||
sens = await sensor.new_sensor(config[CONF_STD_DEV])
|
||||
cg.add(var.set_std_dev_sensor(sens))
|
||||
|
||||
@ -100,6 +100,7 @@ struct AddressableColorWipeEffectColor {
|
||||
uint8_t r, g, b, w;
|
||||
bool random;
|
||||
size_t num_leds;
|
||||
bool gradient;
|
||||
};
|
||||
|
||||
class AddressableColorWipeEffect : public AddressableLightEffect {
|
||||
@ -117,8 +118,15 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
|
||||
it.shift_left(1);
|
||||
else
|
||||
it.shift_right(1);
|
||||
const AddressableColorWipeEffectColor color = this->colors_[this->at_color_];
|
||||
const Color esp_color = Color(color.r, color.g, color.b, color.w);
|
||||
const AddressableColorWipeEffectColor &color = this->colors_[this->at_color_];
|
||||
Color esp_color = Color(color.r, color.g, color.b, color.w);
|
||||
if (color.gradient) {
|
||||
size_t next_color_index = (this->at_color_ + 1) % this->colors_.size();
|
||||
const AddressableColorWipeEffectColor &next_color = this->colors_[next_color_index];
|
||||
const Color next_esp_color = Color(next_color.r, next_color.g, next_color.b, next_color.w);
|
||||
uint8_t gradient = 255 * ((float) this->leds_added_ / color.num_leds);
|
||||
esp_color = esp_color.gradient(next_esp_color, gradient);
|
||||
}
|
||||
if (this->reverse_)
|
||||
it[-1] = esp_color;
|
||||
else
|
||||
|
||||
@ -58,6 +58,7 @@ from .types import (
|
||||
|
||||
CONF_ADD_LED_INTERVAL = "add_led_interval"
|
||||
CONF_REVERSE = "reverse"
|
||||
CONF_GRADIENT = "gradient"
|
||||
CONF_MOVE_INTERVAL = "move_interval"
|
||||
CONF_SCAN_WIDTH = "scan_width"
|
||||
CONF_TWINKLE_PROBABILITY = "twinkle_probability"
|
||||
@ -386,6 +387,7 @@ async def addressable_rainbow_effect_to_code(config, effect_id):
|
||||
cv.Optional(CONF_WHITE, default=1.0): cv.percentage,
|
||||
cv.Optional(CONF_RANDOM, default=False): cv.boolean,
|
||||
cv.Required(CONF_NUM_LEDS): cv.All(cv.uint32_t, cv.Range(min=1)),
|
||||
cv.Optional(CONF_GRADIENT, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
@ -409,6 +411,7 @@ async def addressable_color_wipe_effect_to_code(config, effect_id):
|
||||
("w", int(round(color[CONF_WHITE] * 255))),
|
||||
("random", color[CONF_RANDOM]),
|
||||
("num_leds", color[CONF_NUM_LEDS]),
|
||||
("gradient", color[CONF_GRADIENT]),
|
||||
)
|
||||
)
|
||||
cg.add(var.set_colors(colors))
|
||||
|
||||
@ -120,6 +120,7 @@ void LightState::loop() {
|
||||
// Apply transformer (if any)
|
||||
if (this->transformer_ != nullptr) {
|
||||
auto values = this->transformer_->apply();
|
||||
this->is_transformer_active_ = true;
|
||||
if (values.has_value()) {
|
||||
this->current_values = *values;
|
||||
this->output_->update_state(this);
|
||||
@ -131,6 +132,7 @@ void LightState::loop() {
|
||||
this->current_values = this->transformer_->get_target_values();
|
||||
|
||||
this->transformer_->stop();
|
||||
this->is_transformer_active_ = false;
|
||||
this->transformer_ = nullptr;
|
||||
this->target_state_reached_callback_.call();
|
||||
}
|
||||
@ -214,6 +216,8 @@ void LightState::current_values_as_ct(float *color_temperature, float *white_bri
|
||||
this->gamma_correct_);
|
||||
}
|
||||
|
||||
bool LightState::is_transformer_active() { return this->is_transformer_active_; }
|
||||
|
||||
void LightState::start_effect_(uint32_t effect_index) {
|
||||
this->stop_effect_();
|
||||
if (effect_index == 0)
|
||||
@ -263,6 +267,7 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b
|
||||
}
|
||||
|
||||
void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) {
|
||||
this->is_transformer_active_ = false;
|
||||
this->transformer_ = nullptr;
|
||||
this->current_values = target;
|
||||
if (set_remote_values) {
|
||||
|
||||
@ -144,6 +144,17 @@ class LightState : public EntityBase, public Component {
|
||||
|
||||
void current_values_as_ct(float *color_temperature, float *white_brightness);
|
||||
|
||||
/**
|
||||
* Indicator if a transformer (e.g. transition) is active. This is useful
|
||||
* for effects e.g. at the start of the apply() method, add a check like:
|
||||
*
|
||||
* if (this->state_->is_transformer_active()) {
|
||||
* // Something is already running.
|
||||
* return;
|
||||
* }
|
||||
*/
|
||||
bool is_transformer_active();
|
||||
|
||||
protected:
|
||||
friend LightOutput;
|
||||
friend LightCall;
|
||||
@ -203,6 +214,9 @@ class LightState : public EntityBase, public Component {
|
||||
LightRestoreMode restore_mode_;
|
||||
/// List of effects for this light.
|
||||
std::vector<LightEffect *> effects_;
|
||||
|
||||
// for effects, true if a transformer (transition) is active.
|
||||
bool is_transformer_active_ = false;
|
||||
};
|
||||
|
||||
} // namespace light
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace lightwaverf {
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ LilygoT547Touchscreen = lilygo_t5_47_ns.class_(
|
||||
|
||||
CONF_LILYGO_T5_47_TOUCHSCREEN_ID = "lilygo_t5_47_touchscreen_id"
|
||||
|
||||
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
|
||||
CONFIG_SCHEMA = touchscreen.touchscreen_schema("250ms").extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(LilygoT547Touchscreen),
|
||||
|
||||
@ -212,6 +212,14 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) {
|
||||
return;
|
||||
#endif
|
||||
#ifdef USE_HOST
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
char buffer[80];
|
||||
|
||||
time(&rawtime);
|
||||
timeinfo = localtime(&rawtime);
|
||||
strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo);
|
||||
fputs(buffer, stdout);
|
||||
puts(msg);
|
||||
#endif
|
||||
|
||||
|
||||
363
esphome/components/micro_wake_word/__init__.py
Normal file
363
esphome/components/micro_wake_word/__init__.py
Normal file
@ -0,0 +1,363 @@
|
||||
import logging
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
from urllib.parse import urljoin
|
||||
from pathlib import Path
|
||||
import requests
|
||||
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
|
||||
from esphome.core import CORE, HexInt, EsphomeError
|
||||
|
||||
from esphome.components import esp32, microphone
|
||||
from esphome import automation, git, external_files
|
||||
from esphome.automation import register_action, register_condition
|
||||
|
||||
|
||||
from esphome.const import (
|
||||
__version__,
|
||||
CONF_ID,
|
||||
CONF_MICROPHONE,
|
||||
CONF_MODEL,
|
||||
CONF_URL,
|
||||
CONF_FILE,
|
||||
CONF_PATH,
|
||||
CONF_REF,
|
||||
CONF_REFRESH,
|
||||
CONF_TYPE,
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_RAW_DATA_ID,
|
||||
TYPE_GIT,
|
||||
TYPE_LOCAL,
|
||||
)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@kahrendt", "@jesserockz"]
|
||||
DEPENDENCIES = ["microphone"]
|
||||
DOMAIN = "micro_wake_word"
|
||||
|
||||
CONF_PROBABILITY_CUTOFF = "probability_cutoff"
|
||||
CONF_SLIDING_WINDOW_AVERAGE_SIZE = "sliding_window_average_size"
|
||||
CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected"
|
||||
|
||||
TYPE_HTTP = "http"
|
||||
|
||||
micro_wake_word_ns = cg.esphome_ns.namespace("micro_wake_word")
|
||||
|
||||
MicroWakeWord = micro_wake_word_ns.class_("MicroWakeWord", cg.Component)
|
||||
|
||||
StartAction = micro_wake_word_ns.class_("StartAction", automation.Action)
|
||||
StopAction = micro_wake_word_ns.class_("StopAction", automation.Action)
|
||||
|
||||
IsRunningCondition = micro_wake_word_ns.class_(
|
||||
"IsRunningCondition", automation.Condition
|
||||
)
|
||||
|
||||
|
||||
def _validate_json_filename(value):
|
||||
value = cv.string(value)
|
||||
if not value.endswith(".json"):
|
||||
raise cv.Invalid("Manifest filename must end with .json")
|
||||
return value
|
||||
|
||||
|
||||
def _process_git_source(config):
|
||||
repo_dir, _ = git.clone_or_update(
|
||||
url=config[CONF_URL],
|
||||
ref=config.get(CONF_REF),
|
||||
refresh=config[CONF_REFRESH],
|
||||
domain=DOMAIN,
|
||||
username=config.get(CONF_USERNAME),
|
||||
password=config.get(CONF_PASSWORD),
|
||||
)
|
||||
|
||||
if not (repo_dir / config[CONF_FILE]).exists():
|
||||
raise cv.Invalid("File does not exist in repository")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
CV_GIT_SCHEMA = cv.GIT_SCHEMA
|
||||
if isinstance(CV_GIT_SCHEMA, dict):
|
||||
CV_GIT_SCHEMA = cv.Schema(CV_GIT_SCHEMA)
|
||||
|
||||
GIT_SCHEMA = cv.All(
|
||||
CV_GIT_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_FILE): _validate_json_filename,
|
||||
cv.Optional(CONF_REFRESH, default="1d"): cv.All(
|
||||
cv.string, cv.source_refresh
|
||||
),
|
||||
}
|
||||
),
|
||||
_process_git_source,
|
||||
)
|
||||
|
||||
KEY_WAKE_WORD = "wake_word"
|
||||
KEY_AUTHOR = "author"
|
||||
KEY_WEBSITE = "website"
|
||||
KEY_VERSION = "version"
|
||||
KEY_MICRO = "micro"
|
||||
|
||||
MANIFEST_SCHEMA_V1 = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_TYPE): "micro",
|
||||
cv.Required(KEY_WAKE_WORD): cv.string,
|
||||
cv.Required(KEY_AUTHOR): cv.string,
|
||||
cv.Required(KEY_WEBSITE): cv.url,
|
||||
cv.Required(KEY_VERSION): cv.All(cv.int_, 1),
|
||||
cv.Required(CONF_MODEL): cv.string,
|
||||
cv.Required(KEY_MICRO): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PROBABILITY_CUTOFF): cv.float_,
|
||||
cv.Required(CONF_SLIDING_WINDOW_AVERAGE_SIZE): cv.positive_int,
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _compute_local_file_path(config: dict) -> Path:
|
||||
url = config[CONF_URL]
|
||||
h = hashlib.new("sha256")
|
||||
h.update(url.encode())
|
||||
key = h.hexdigest()[:8]
|
||||
base_dir = external_files.compute_local_file_dir(DOMAIN)
|
||||
return base_dir / key
|
||||
|
||||
|
||||
def _download_file(url: str, path: Path) -> bytes:
|
||||
if not external_files.has_remote_file_changed(url, path):
|
||||
_LOGGER.debug("Remote file has not changed, skipping download")
|
||||
return path.read_bytes()
|
||||
|
||||
try:
|
||||
req = requests.get(
|
||||
url,
|
||||
timeout=external_files.NETWORK_TIMEOUT,
|
||||
headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"},
|
||||
)
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise cv.Invalid(f"Could not download file from {url}: {e}") from e
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(req.content)
|
||||
return req.content
|
||||
|
||||
|
||||
def _process_http_source(config):
|
||||
url = config[CONF_URL]
|
||||
path = _compute_local_file_path(config)
|
||||
|
||||
json_path = path / "manifest.json"
|
||||
|
||||
json_contents = _download_file(url, json_path)
|
||||
|
||||
manifest_data = json.loads(json_contents)
|
||||
if not isinstance(manifest_data, dict):
|
||||
raise cv.Invalid("Manifest file must contain a JSON object")
|
||||
|
||||
try:
|
||||
MANIFEST_SCHEMA_V1(manifest_data)
|
||||
except cv.Invalid as e:
|
||||
raise cv.Invalid(f"Invalid manifest file: {e}") from e
|
||||
|
||||
model = manifest_data[CONF_MODEL]
|
||||
model_url = urljoin(url, model)
|
||||
|
||||
model_path = path / model
|
||||
|
||||
_download_file(str(model_url), model_path)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
HTTP_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_URL): cv.url,
|
||||
},
|
||||
_process_http_source,
|
||||
)
|
||||
|
||||
LOCAL_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PATH): cv.All(_validate_json_filename, cv.file_),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_source_model_name(value):
|
||||
if not isinstance(value, str):
|
||||
raise cv.Invalid("Model name must be a string")
|
||||
|
||||
if value.endswith(".json"):
|
||||
raise cv.Invalid("Model name must not end with .json")
|
||||
|
||||
return MODEL_SOURCE_SCHEMA(
|
||||
{
|
||||
CONF_TYPE: TYPE_HTTP,
|
||||
CONF_URL: f"https://github.com/esphome/micro-wake-word-models/raw/main/models/{value}.json",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_source_shorthand(value):
|
||||
if not isinstance(value, str):
|
||||
raise cv.Invalid("Shorthand only for strings")
|
||||
|
||||
try: # Test for model name
|
||||
return _validate_source_model_name(value)
|
||||
except cv.Invalid:
|
||||
pass
|
||||
|
||||
try: # Test for local path
|
||||
return MODEL_SOURCE_SCHEMA({CONF_TYPE: TYPE_LOCAL, CONF_PATH: value})
|
||||
except cv.Invalid:
|
||||
pass
|
||||
|
||||
try: # Test for http url
|
||||
return MODEL_SOURCE_SCHEMA({CONF_TYPE: TYPE_HTTP, CONF_URL: value})
|
||||
except cv.Invalid:
|
||||
pass
|
||||
|
||||
git_file = git.GitFile.from_shorthand(value)
|
||||
|
||||
conf = {
|
||||
CONF_TYPE: TYPE_GIT,
|
||||
CONF_URL: git_file.git_url,
|
||||
CONF_FILE: git_file.filename,
|
||||
}
|
||||
if git_file.ref:
|
||||
conf[CONF_REF] = git_file.ref
|
||||
|
||||
try:
|
||||
return MODEL_SOURCE_SCHEMA(conf)
|
||||
except cv.Invalid as e:
|
||||
raise cv.Invalid(
|
||||
f"Could not find file '{git_file.filename}' in the repository. Please make sure it exists."
|
||||
) from e
|
||||
|
||||
|
||||
MODEL_SOURCE_SCHEMA = cv.Any(
|
||||
_validate_source_shorthand,
|
||||
cv.typed_schema(
|
||||
{
|
||||
TYPE_GIT: GIT_SCHEMA,
|
||||
TYPE_LOCAL: LOCAL_SCHEMA,
|
||||
TYPE_HTTP: HTTP_SCHEMA,
|
||||
}
|
||||
),
|
||||
msg="Not a valid model name, local path, http(s) url, or github shorthand",
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(MicroWakeWord),
|
||||
cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone),
|
||||
cv.Optional(CONF_PROBABILITY_CUTOFF): cv.float_,
|
||||
cv.Optional(CONF_SLIDING_WINDOW_AVERAGE_SIZE): cv.positive_int,
|
||||
cv.Optional(CONF_ON_WAKE_WORD_DETECTED): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
cv.Required(CONF_MODEL): MODEL_SOURCE_SCHEMA,
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_with_esp_idf,
|
||||
)
|
||||
|
||||
|
||||
def _load_model_data(manifest_path: Path):
|
||||
with open(manifest_path, encoding="utf-8") as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
try:
|
||||
MANIFEST_SCHEMA_V1(manifest)
|
||||
except cv.Invalid as e:
|
||||
raise EsphomeError(f"Invalid manifest file: {e}") from e
|
||||
|
||||
model_path = urljoin(str(manifest_path), manifest[CONF_MODEL])
|
||||
|
||||
with open(model_path, "rb") as f:
|
||||
model = f.read()
|
||||
|
||||
return manifest, model
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
mic = await cg.get_variable(config[CONF_MICROPHONE])
|
||||
cg.add(var.set_microphone(mic))
|
||||
|
||||
if on_wake_word_detection_config := config.get(CONF_ON_WAKE_WORD_DETECTED):
|
||||
await automation.build_automation(
|
||||
var.get_wake_word_detected_trigger(),
|
||||
[(cg.std_string, "wake_word")],
|
||||
on_wake_word_detection_config,
|
||||
)
|
||||
|
||||
esp32.add_idf_component(
|
||||
name="esp-tflite-micro",
|
||||
repo="https://github.com/espressif/esp-tflite-micro",
|
||||
)
|
||||
|
||||
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
|
||||
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
|
||||
cg.add_build_flag("-DESP_NN")
|
||||
|
||||
model_config = config.get(CONF_MODEL)
|
||||
data = []
|
||||
if model_config[CONF_TYPE] == TYPE_GIT:
|
||||
# compute path to model file
|
||||
key = f"{model_config[CONF_URL]}@{model_config.get(CONF_REF)}"
|
||||
base_dir = Path(CORE.data_dir) / DOMAIN
|
||||
h = hashlib.new("sha256")
|
||||
h.update(key.encode())
|
||||
file: Path = base_dir / h.hexdigest()[:8] / model_config[CONF_FILE]
|
||||
|
||||
elif model_config[CONF_TYPE] == TYPE_LOCAL:
|
||||
file = model_config[CONF_PATH]
|
||||
|
||||
elif model_config[CONF_TYPE] == TYPE_HTTP:
|
||||
file = _compute_local_file_path(model_config) / "manifest.json"
|
||||
|
||||
manifest, data = _load_model_data(file)
|
||||
|
||||
rhs = [HexInt(x) for x in data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
cg.add(var.set_model_start(prog_arr))
|
||||
|
||||
probability_cutoff = config.get(
|
||||
CONF_PROBABILITY_CUTOFF, manifest[KEY_MICRO][CONF_PROBABILITY_CUTOFF]
|
||||
)
|
||||
cg.add(var.set_probability_cutoff(probability_cutoff))
|
||||
sliding_window_average_size = config.get(
|
||||
CONF_SLIDING_WINDOW_AVERAGE_SIZE,
|
||||
manifest[KEY_MICRO][CONF_SLIDING_WINDOW_AVERAGE_SIZE],
|
||||
)
|
||||
cg.add(var.set_sliding_window_average_size(sliding_window_average_size))
|
||||
|
||||
cg.add(var.set_wake_word(manifest[KEY_WAKE_WORD]))
|
||||
|
||||
|
||||
MICRO_WAKE_WORD_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(MicroWakeWord)})
|
||||
|
||||
|
||||
@register_action("micro_wake_word.start", StartAction, MICRO_WAKE_WORD_ACTION_SCHEMA)
|
||||
@register_action("micro_wake_word.stop", StopAction, MICRO_WAKE_WORD_ACTION_SCHEMA)
|
||||
@register_condition(
|
||||
"micro_wake_word.is_running", IsRunningCondition, MICRO_WAKE_WORD_ACTION_SCHEMA
|
||||
)
|
||||
async def micro_wake_word_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
@ -0,0 +1,493 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
// Converted audio_preprocessor_int8.tflite
|
||||
// From https://github.com/tensorflow/tflite-micro/tree/main/tensorflow/lite/micro/examples/micro_speech/models accessed
|
||||
// January 2024
|
||||
//
|
||||
// Copyright 2023 The TensorFlow Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
namespace esphome {
|
||||
namespace micro_wake_word {
|
||||
|
||||
const unsigned char G_AUDIO_PREPROCESSOR_INT8_TFLITE[] = {
|
||||
0x1c, 0x00, 0x00, 0x00, 0x54, 0x46, 0x4c, 0x33, 0x14, 0x00, 0x20, 0x00, 0x1c, 0x00, 0x18, 0x00, 0x14, 0x00, 0x10,
|
||||
0x00, 0x0c, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x88, 0x00,
|
||||
0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x80, 0x0e, 0x00, 0x00, 0x90, 0x0e, 0x00, 0x00, 0xcc, 0x1f, 0x00, 0x00, 0x03,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xe2, 0xeb, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00,
|
||||
0x1c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67,
|
||||
0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x94, 0xff,
|
||||
0xff, 0xff, 0x2a, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x70, 0x75,
|
||||
0x74, 0x5f, 0x30, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc2, 0xf5, 0xff, 0xff,
|
||||
0x04, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65,
|
||||
0x00, 0x02, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xdc, 0xff, 0xff, 0xff, 0x2d, 0x00,
|
||||
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x43, 0x4f, 0x4e, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f,
|
||||
0x4e, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x44, 0x41, 0x54, 0x41, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x04, 0x00,
|
||||
0x08, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x6d, 0x69, 0x6e,
|
||||
0x5f, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x2e, 0x00,
|
||||
0x00, 0x00, 0x9c, 0x0d, 0x00, 0x00, 0x94, 0x0d, 0x00, 0x00, 0xc4, 0x09, 0x00, 0x00, 0x6c, 0x09, 0x00, 0x00, 0x48,
|
||||
0x09, 0x00, 0x00, 0x34, 0x09, 0x00, 0x00, 0x20, 0x09, 0x00, 0x00, 0x0c, 0x09, 0x00, 0x00, 0xf8, 0x08, 0x00, 0x00,
|
||||
0xec, 0x07, 0x00, 0x00, 0x88, 0x07, 0x00, 0x00, 0x24, 0x07, 0x00, 0x00, 0xc0, 0x06, 0x00, 0x00, 0x38, 0x04, 0x00,
|
||||
0x00, 0xb0, 0x01, 0x00, 0x00, 0x9c, 0x01, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, 0x60, 0x01,
|
||||
0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x2c,
|
||||
0x01, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00,
|
||||
0x04, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00,
|
||||
0x00, 0xdc, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0xbc, 0x00,
|
||||
0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0x94,
|
||||
0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xf2, 0xf6, 0xff, 0xff,
|
||||
0x04, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x04,
|
||||
0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x08, 0x00,
|
||||
0x07, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x0a, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x04, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
|
||||
0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x32, 0x2e, 0x31, 0x32, 0x2e, 0x30, 0x00,
|
||||
0x00, 0x56, 0xf7, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x32, 0x2e, 0x38, 0x2e, 0x30, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4, 0xe1, 0xff, 0xff, 0xd8, 0xe1, 0xff, 0xff, 0xdc,
|
||||
0xe1, 0xff, 0xff, 0xe0, 0xe1, 0xff, 0xff, 0xe4, 0xe1, 0xff, 0xff, 0xe8, 0xe1, 0xff, 0xff, 0xec, 0xe1, 0xff, 0xff,
|
||||
0xf0, 0xe1, 0xff, 0xff, 0xf4, 0xe1, 0xff, 0xff, 0xf8, 0xe1, 0xff, 0xff, 0xfc, 0xe1, 0xff, 0xff, 0x00, 0xe2, 0xff,
|
||||
0xff, 0x04, 0xe2, 0xff, 0xff, 0x08, 0xe2, 0xff, 0xff, 0x0c, 0xe2, 0xff, 0xff, 0x10, 0xe2, 0xff, 0xff, 0x14, 0xe2,
|
||||
0xff, 0xff, 0x18, 0xe2, 0xff, 0xff, 0x1c, 0xe2, 0xff, 0xff, 0x20, 0xe2, 0xff, 0xff, 0x24, 0xe2, 0xff, 0xff, 0x28,
|
||||
0xe2, 0xff, 0xff, 0x2c, 0xe2, 0xff, 0xff, 0x30, 0xe2, 0xff, 0xff, 0xd2, 0xf7, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00,
|
||||
0x04, 0x00, 0x00, 0x00, 0x9a, 0x02, 0x00, 0x00, 0xe2, 0xf7, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
|
||||
0x00, 0x00, 0x01, 0x00, 0x00, 0xf2, 0xf7, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x80, 0xff,
|
||||
0xff, 0xff, 0x02, 0xf8, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x12,
|
||||
0xf8, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x22, 0xf8, 0xff, 0xff,
|
||||
0x04, 0x00, 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x61, 0x05, 0x00, 0x00, 0x00, 0x00, 0x23, 0x0b, 0x41,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0xb3, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 0x0e, 0x80, 0x05,
|
||||
0x00, 0x00, 0x00, 0x00, 0xd1, 0x0c, 0x63, 0x04, 0x00, 0x00, 0x00, 0x00, 0x34, 0x0c, 0x3f, 0x04, 0x00, 0x00, 0x00,
|
||||
0x00, 0x81, 0x0c, 0xf7, 0x04, 0x00, 0x00, 0x00, 0x00, 0x9f, 0x0d, 0x77, 0x06, 0x00, 0x00, 0x00, 0x00, 0x7b, 0x0f,
|
||||
0xa9, 0x08, 0x01, 0x02, 0x7f, 0x0b, 0x22, 0x05, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x0e, 0xd1, 0x08, 0xdb, 0x02, 0x00,
|
||||
0x00, 0x00, 0x00, 0x03, 0x0d, 0x4a, 0x07, 0xad, 0x01, 0x2c, 0x0c, 0xc6, 0x06, 0x79, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x45, 0x0c, 0x29, 0x07, 0x23, 0x02, 0x34, 0x0d, 0x5b, 0x08, 0x96, 0x03, 0x00, 0x00, 0x00, 0x00, 0xe5, 0x0e, 0x48,
|
||||
0x0a, 0xbd, 0x05, 0x45, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde, 0x0c, 0x88, 0x08, 0x43, 0x04,
|
||||
0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x0b, 0xd3, 0x07, 0xcb, 0x03, 0xd2, 0x0f, 0xe7,
|
||||
0x0b, 0x09, 0x08, 0x39, 0x04, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0x0c, 0x14, 0x09,
|
||||
0x75, 0x05, 0xe2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5a, 0x0e, 0xdd, 0x0a, 0x6b, 0x07, 0x03,
|
||||
0x04, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x0d, 0x09, 0x0a, 0xc9, 0x06, 0x93, 0x03, 0x65, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x0d, 0x25, 0x0a, 0x12, 0x07, 0x07, 0x04, 0x05, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x0a, 0x0e, 0x17, 0x0b, 0x2c, 0x08, 0x49, 0x05, 0x6d, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x98, 0x0f, 0xcb, 0x0c, 0x04, 0x0a, 0x44, 0x07, 0x8b, 0x04, 0xd8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x0f, 0x87,
|
||||
0x0c, 0xe7, 0x09, 0x4e, 0x07, 0xba, 0x04, 0x2d, 0x02, 0x00, 0x00, 0x00, 0x00, 0xa5, 0x0f, 0x23, 0x0d, 0xa7, 0x0a,
|
||||
0x30, 0x08, 0xbe, 0x05, 0x52, 0x03, 0xeb, 0x00, 0x89, 0x0e, 0x2c, 0x0c, 0xd4, 0x09, 0x81, 0x07, 0x33, 0x05, 0xe9,
|
||||
0x02, 0xa5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x0e, 0x29, 0x0c, 0xf1, 0x09, 0xbe, 0x07, 0x90, 0x05, 0x65, 0x03,
|
||||
0x3f, 0x01, 0x1d, 0x0f, 0xff, 0x0c, 0xe5, 0x0a, 0xcf, 0x08, 0xbc, 0x06, 0xae, 0x04, 0xa3, 0x02, 0x9c, 0x00, 0x99,
|
||||
0x0e, 0x99, 0x0c, 0x9d, 0x0a, 0xa4, 0x08, 0xaf, 0x06, 0xbd, 0x04, 0xcf, 0x02, 0xe4, 0x00, 0xfc, 0x0e, 0x17, 0x0d,
|
||||
0x36, 0x0b, 0x57, 0x09, 0x7c, 0x07, 0xa4, 0x05, 0xcf, 0x03, 0xfd, 0x01, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x62, 0x0e, 0x98, 0x0c, 0xd2, 0x0a, 0x0e, 0x09, 0x4d, 0x07, 0x8f, 0x05, 0xd4, 0x03, 0x1b, 0x02,
|
||||
0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb1, 0x0e, 0x00, 0x0d, 0x52, 0x0b, 0xa6, 0x09, 0xfd, 0x07, 0x56, 0x06, 0xb1,
|
||||
0x04, 0x0f, 0x03, 0x6f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd2, 0x0f, 0x37, 0x0e, 0x9e, 0x0c,
|
||||
0x08, 0x0b, 0x73, 0x09, 0xe1, 0x07, 0x52, 0x06, 0xc4, 0x04, 0x38, 0x03, 0xaf, 0x01, 0x28, 0x00, 0xa3, 0x0e, 0x1f,
|
||||
0x0d, 0x9e, 0x0b, 0x1f, 0x0a, 0xa2, 0x08, 0x27, 0x07, 0xae, 0x05, 0x37, 0x04, 0xc2, 0x02, 0x4e, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0xdd, 0x0f, 0x6d, 0x0e, 0xff, 0x0c, 0x93, 0x0b, 0x29, 0x0a, 0xc1, 0x08, 0x5a, 0x07, 0xf5, 0x05, 0x92,
|
||||
0x04, 0x30, 0x03, 0xd1, 0x01, 0x73, 0x00, 0x16, 0x0f, 0xbc, 0x0d, 0x62, 0x0c, 0x0b, 0x0b, 0xb5, 0x09, 0x61, 0x08,
|
||||
0x0e, 0x07, 0xbd, 0x05, 0x6d, 0x04, 0x1f, 0x03, 0xd3, 0x01, 0x88, 0x00, 0x3e, 0x0f, 0xf6, 0x0d, 0xaf, 0x0c, 0x6a,
|
||||
0x0b, 0x27, 0x0a, 0xe4, 0x08, 0xa3, 0x07, 0x64, 0x06, 0x26, 0x05, 0xe9, 0x03, 0xae, 0x02, 0x74, 0x01, 0x3b, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x0f, 0xce, 0x0d, 0x99, 0x0c, 0x66, 0x0b, 0x34, 0x0a, 0x03,
|
||||
0x09, 0xd3, 0x07, 0xa5, 0x06, 0x78, 0x05, 0x4c, 0x04, 0x22, 0x03, 0xf8, 0x01, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xa9, 0x0f, 0x83, 0x0e, 0x5f, 0x0d, 0x3b, 0x0c, 0x19, 0x0b, 0xf8, 0x09, 0xd8, 0x08, 0xb9, 0x07, 0x9b, 0x06, 0x7e,
|
||||
0x05, 0x63, 0x04, 0x48, 0x03, 0x2f, 0x02, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa6, 0xfa, 0xff, 0xff, 0x04, 0x00,
|
||||
0x00, 0x00, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x9e, 0x0a, 0x00, 0x00, 0x00, 0x00, 0xdc, 0x04, 0xbe, 0x0e, 0x00,
|
||||
0x00, 0x00, 0x00, 0x4c, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8b, 0x01, 0x7f, 0x0a, 0x00, 0x00,
|
||||
0x00, 0x00, 0x2e, 0x03, 0x9c, 0x0b, 0x00, 0x00, 0x00, 0x00, 0xcb, 0x03, 0xc0, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x7e,
|
||||
0x03, 0x08, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x60, 0x02, 0x88, 0x09, 0x00, 0x00, 0x00, 0x00, 0x84, 0x00, 0x56, 0x07,
|
||||
0xfe, 0x0d, 0x80, 0x04, 0xdd, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x16, 0x01, 0x2e, 0x07, 0x24, 0x0d, 0x00, 0x00, 0x00,
|
||||
0x00, 0xfc, 0x02, 0xb5, 0x08, 0x52, 0x0e, 0xd3, 0x03, 0x39, 0x09, 0x86, 0x0e, 0x00, 0x00, 0x00, 0x00, 0xba, 0x03,
|
||||
0xd6, 0x08, 0xdc, 0x0d, 0xcb, 0x02, 0xa4, 0x07, 0x69, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x1a, 0x01, 0xb7, 0x05, 0x42,
|
||||
0x0a, 0xba, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x03, 0x77, 0x07, 0xbc, 0x0b, 0xf1, 0x0f,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x04, 0x2c, 0x08, 0x34, 0x0c, 0x2d, 0x00, 0x18, 0x04, 0xf6,
|
||||
0x07, 0xc6, 0x0b, 0x89, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x03, 0xeb, 0x06, 0x8a, 0x0a,
|
||||
0x1d, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa5, 0x01, 0x22, 0x05, 0x94, 0x08, 0xfc, 0x0b, 0x59,
|
||||
0x0f, 0x00, 0x00, 0x00, 0x00, 0xac, 0x02, 0xf6, 0x05, 0x36, 0x09, 0x6c, 0x0c, 0x9a, 0x0f, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xbe, 0x02, 0xda, 0x05, 0xed, 0x08, 0xf8, 0x0b, 0xfa, 0x0e, 0x00, 0x00, 0x00, 0x00, 0xf5,
|
||||
0x01, 0xe8, 0x04, 0xd3, 0x07, 0xb6, 0x0a, 0x92, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67, 0x00,
|
||||
0x34, 0x03, 0xfb, 0x05, 0xbb, 0x08, 0x74, 0x0b, 0x27, 0x0e, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x78, 0x03, 0x18,
|
||||
0x06, 0xb1, 0x08, 0x45, 0x0b, 0xd2, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x5a, 0x00, 0xdc, 0x02, 0x58, 0x05, 0xcf, 0x07,
|
||||
0x41, 0x0a, 0xad, 0x0c, 0x14, 0x0f, 0x76, 0x01, 0xd3, 0x03, 0x2b, 0x06, 0x7e, 0x08, 0xcc, 0x0a, 0x16, 0x0d, 0x5a,
|
||||
0x0f, 0x00, 0x00, 0x00, 0x00, 0x9b, 0x01, 0xd6, 0x03, 0x0e, 0x06, 0x41, 0x08, 0x6f, 0x0a, 0x9a, 0x0c, 0xc0, 0x0e,
|
||||
0xe2, 0x00, 0x00, 0x03, 0x1a, 0x05, 0x30, 0x07, 0x43, 0x09, 0x51, 0x0b, 0x5c, 0x0d, 0x63, 0x0f, 0x66, 0x01, 0x66,
|
||||
0x03, 0x62, 0x05, 0x5b, 0x07, 0x50, 0x09, 0x42, 0x0b, 0x30, 0x0d, 0x1b, 0x0f, 0x03, 0x01, 0xe8, 0x02, 0xc9, 0x04,
|
||||
0xa8, 0x06, 0x83, 0x08, 0x5b, 0x0a, 0x30, 0x0c, 0x02, 0x0e, 0xd1, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x9d, 0x01, 0x67, 0x03, 0x2d, 0x05, 0xf1, 0x06, 0xb2, 0x08, 0x70, 0x0a, 0x2b, 0x0c, 0xe4, 0x0d, 0x9a, 0x0f,
|
||||
0x00, 0x00, 0x00, 0x00, 0x4e, 0x01, 0xff, 0x02, 0xad, 0x04, 0x59, 0x06, 0x02, 0x08, 0xa9, 0x09, 0x4e, 0x0b, 0xf0,
|
||||
0x0c, 0x90, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x00, 0xc8, 0x01, 0x61, 0x03, 0xf7, 0x04,
|
||||
0x8c, 0x06, 0x1e, 0x08, 0xad, 0x09, 0x3b, 0x0b, 0xc7, 0x0c, 0x50, 0x0e, 0xd7, 0x0f, 0x5c, 0x01, 0xe0, 0x02, 0x61,
|
||||
0x04, 0xe0, 0x05, 0x5d, 0x07, 0xd8, 0x08, 0x51, 0x0a, 0xc8, 0x0b, 0x3d, 0x0d, 0xb1, 0x0e, 0x00, 0x00, 0x00, 0x00,
|
||||
0x22, 0x00, 0x92, 0x01, 0x00, 0x03, 0x6c, 0x04, 0xd6, 0x05, 0x3e, 0x07, 0xa5, 0x08, 0x0a, 0x0a, 0x6d, 0x0b, 0xcf,
|
||||
0x0c, 0x2e, 0x0e, 0x8c, 0x0f, 0xe9, 0x00, 0x43, 0x02, 0x9d, 0x03, 0xf4, 0x04, 0x4a, 0x06, 0x9e, 0x07, 0xf1, 0x08,
|
||||
0x42, 0x0a, 0x92, 0x0b, 0xe0, 0x0c, 0x2c, 0x0e, 0x77, 0x0f, 0xc1, 0x00, 0x09, 0x02, 0x50, 0x03, 0x95, 0x04, 0xd8,
|
||||
0x05, 0x1b, 0x07, 0x5c, 0x08, 0x9b, 0x09, 0xd9, 0x0a, 0x16, 0x0c, 0x51, 0x0d, 0x8b, 0x0e, 0xc4, 0x0f, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x31, 0x02, 0x66, 0x03, 0x99, 0x04, 0xcb, 0x05, 0xfc, 0x06, 0x2c,
|
||||
0x08, 0x5a, 0x09, 0x87, 0x0a, 0xb3, 0x0b, 0xdd, 0x0c, 0x07, 0x0e, 0x2f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x56, 0x00,
|
||||
0x7c, 0x01, 0xa0, 0x02, 0xc4, 0x03, 0xe6, 0x04, 0x07, 0x06, 0x27, 0x07, 0x46, 0x08, 0x64, 0x09, 0x81, 0x0a, 0x9c,
|
||||
0x0b, 0xb7, 0x0c, 0xd0, 0x0d, 0xe8, 0x0e, 0x00, 0x10, 0x00, 0x00, 0x2a, 0xfd, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00,
|
||||
0x52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x06, 0x00, 0x08, 0x00, 0x08, 0x00, 0x0a, 0x00, 0x0c, 0x00, 0x0e, 0x00, 0x10,
|
||||
0x00, 0x12, 0x00, 0x16, 0x00, 0x18, 0x00, 0x1a, 0x00, 0x1e, 0x00, 0x20, 0x00, 0x24, 0x00, 0x26, 0x00, 0x2a, 0x00,
|
||||
0x2e, 0x00, 0x32, 0x00, 0x36, 0x00, 0x3a, 0x00, 0x40, 0x00, 0x44, 0x00, 0x4a, 0x00, 0x4e, 0x00, 0x54, 0x00, 0x5a,
|
||||
0x00, 0x62, 0x00, 0x68, 0x00, 0x70, 0x00, 0x78, 0x00, 0x80, 0x00, 0x88, 0x00, 0x92, 0x00, 0x9a, 0x00, 0xa6, 0x00,
|
||||
0xb0, 0x00, 0xbc, 0x00, 0xc8, 0x00, 0xd4, 0x00, 0xe2, 0x00, 0x00, 0x00, 0x8a, 0xfd, 0xff, 0xff, 0x04, 0x00, 0x00,
|
||||
0x00, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x10, 0x00, 0x14, 0x00, 0x18, 0x00,
|
||||
0x1c, 0x00, 0x20, 0x00, 0x24, 0x00, 0x28, 0x00, 0x2c, 0x00, 0x30, 0x00, 0x34, 0x00, 0x38, 0x00, 0x3c, 0x00, 0x44,
|
||||
0x00, 0x4c, 0x00, 0x50, 0x00, 0x58, 0x00, 0x60, 0x00, 0x68, 0x00, 0x70, 0x00, 0x78, 0x00, 0x80, 0x00, 0x88, 0x00,
|
||||
0x90, 0x00, 0x98, 0x00, 0xa0, 0x00, 0xa8, 0x00, 0xb0, 0x00, 0xb8, 0x00, 0xc4, 0x00, 0xd0, 0x00, 0xdc, 0x00, 0xe8,
|
||||
0x00, 0xf4, 0x00, 0x00, 0x01, 0x0c, 0x01, 0x1c, 0x01, 0x2c, 0x01, 0x00, 0x00, 0xea, 0xfd, 0xff, 0xff, 0x04, 0x00,
|
||||
0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04,
|
||||
0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x08, 0x00,
|
||||
0x08, 0x00, 0x04, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08,
|
||||
0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x0c, 0x00, 0x0c, 0x00, 0x0c, 0x00,
|
||||
0x0c, 0x00, 0x0c, 0x00, 0x0c, 0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x00, 0x4a, 0xfe, 0xff, 0xff, 0x04,
|
||||
0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0x7c, 0x7f, 0x79, 0x7f, 0x76, 0x7f, 0xfa, 0xff, 0x00, 0x00, 0x00, 0x00,
|
||||
0x70, 0x7f, 0xf4, 0xff, 0x00, 0x00, 0x00, 0x00, 0x64, 0x7f, 0xe9, 0xff, 0xfe, 0xff, 0x00, 0x00, 0x4b, 0x7f, 0xd0,
|
||||
0xff, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x7f, 0xa0, 0xff, 0x00, 0x00, 0x00, 0x00, 0xbb, 0x7e, 0x42, 0xff, 0x00, 0x00,
|
||||
0x00, 0x00, 0xfd, 0x7d, 0x86, 0xfe, 0x04, 0x00, 0x00, 0x00, 0x87, 0x7c, 0x1d, 0xfd, 0x12, 0x00, 0x00, 0x00, 0xb6,
|
||||
0x79, 0x7f, 0xfa, 0x3e, 0x00, 0x00, 0x00, 0x73, 0x74, 0xf9, 0xf5, 0xca, 0x00, 0x00, 0x00, 0x36, 0x6b, 0x33, 0xef,
|
||||
0x32, 0x02, 0x00, 0x00, 0x9b, 0x5c, 0x87, 0xe7, 0xce, 0x04, 0x00, 0x00, 0xf0, 0x48, 0xde, 0xe2, 0xa0, 0x07, 0x00,
|
||||
0x00, 0x6e, 0x33, 0x8a, 0xe4, 0xa4, 0x08, 0x00, 0x00, 0x9c, 0x20, 0x22, 0xeb, 0x4c, 0x07, 0x00, 0x00, 0x0a, 0x13,
|
||||
0x7d, 0xf2, 0x02, 0x05, 0x00, 0x00, 0x89, 0x0a, 0x17, 0xf8, 0x06, 0x03, 0x00, 0x00, 0xa6, 0x05, 0xa0, 0xfb, 0xb4,
|
||||
0x01, 0x00, 0x00, 0xfa, 0x02, 0xac, 0xfd, 0xe8, 0x00, 0x00, 0x00, 0x8e, 0x01, 0xc7, 0xfe, 0x7a, 0x00, 0x00, 0x00,
|
||||
0xcf, 0x00, 0x5c, 0xff, 0x40, 0x00, 0x00, 0x00, 0x6b, 0x00, 0xab, 0xff, 0x22, 0x00, 0x00, 0x00, 0x38, 0x00, 0xd3,
|
||||
0xff, 0x12, 0x00, 0x00, 0x00, 0x1d, 0x00, 0xea, 0xff, 0x08, 0x00, 0x00, 0x00, 0x0f, 0x00, 0xf3, 0xff, 0x06, 0x00,
|
||||
0x00, 0x00, 0x08, 0x00, 0xf8, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0xfd, 0xff, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0xfd, 0xff,
|
||||
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00,
|
||||
0x00, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x62, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00,
|
||||
0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, 0x72, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01,
|
||||
0x00, 0x00, 0x00, 0x82, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x4d, 0x01, 0x00, 0x00,
|
||||
0x92, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb2, 0xff, 0xff, 0xff, 0x04, 0x00,
|
||||
0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00,
|
||||
0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||
0x00, 0x02, 0x00, 0x04, 0x00, 0x05, 0x00, 0x07, 0x00, 0x0a, 0x00, 0x0d, 0x00, 0x10, 0x00, 0x13, 0x00, 0x17, 0x00,
|
||||
0x1b, 0x00, 0x20, 0x00, 0x25, 0x00, 0x2a, 0x00, 0x30, 0x00, 0x35, 0x00, 0x3c, 0x00, 0x42, 0x00, 0x49, 0x00, 0x51,
|
||||
0x00, 0x58, 0x00, 0x60, 0x00, 0x68, 0x00, 0x71, 0x00, 0x7a, 0x00, 0x83, 0x00, 0x8d, 0x00, 0x97, 0x00, 0xa1, 0x00,
|
||||
0xac, 0x00, 0xb7, 0x00, 0xc2, 0x00, 0xcd, 0x00, 0xd9, 0x00, 0xe5, 0x00, 0xf2, 0x00, 0xff, 0x00, 0x0c, 0x01, 0x19,
|
||||
0x01, 0x27, 0x01, 0x35, 0x01, 0x43, 0x01, 0x52, 0x01, 0x61, 0x01, 0x70, 0x01, 0x7f, 0x01, 0x8f, 0x01, 0x9f, 0x01,
|
||||
0xaf, 0x01, 0xc0, 0x01, 0xd1, 0x01, 0xe2, 0x01, 0xf3, 0x01, 0x05, 0x02, 0x17, 0x02, 0x29, 0x02, 0x3c, 0x02, 0x4e,
|
||||
0x02, 0x61, 0x02, 0x75, 0x02, 0x88, 0x02, 0x9c, 0x02, 0xb0, 0x02, 0xc4, 0x02, 0xd8, 0x02, 0xed, 0x02, 0x02, 0x03,
|
||||
0x17, 0x03, 0x2c, 0x03, 0x41, 0x03, 0x57, 0x03, 0x6d, 0x03, 0x83, 0x03, 0x99, 0x03, 0xb0, 0x03, 0xc7, 0x03, 0xdd,
|
||||
0x03, 0xf4, 0x03, 0x0c, 0x04, 0x23, 0x04, 0x3b, 0x04, 0x52, 0x04, 0x6a, 0x04, 0x82, 0x04, 0x9a, 0x04, 0xb3, 0x04,
|
||||
0xcb, 0x04, 0xe4, 0x04, 0xfd, 0x04, 0x16, 0x05, 0x2f, 0x05, 0x48, 0x05, 0x61, 0x05, 0x7a, 0x05, 0x94, 0x05, 0xad,
|
||||
0x05, 0xc7, 0x05, 0xe1, 0x05, 0xfb, 0x05, 0x15, 0x06, 0x2f, 0x06, 0x49, 0x06, 0x63, 0x06, 0x7e, 0x06, 0x98, 0x06,
|
||||
0xb2, 0x06, 0xcd, 0x06, 0xe7, 0x06, 0x02, 0x07, 0x1d, 0x07, 0x37, 0x07, 0x52, 0x07, 0x6d, 0x07, 0x87, 0x07, 0xa2,
|
||||
0x07, 0xbd, 0x07, 0xd8, 0x07, 0xf3, 0x07, 0x0d, 0x08, 0x28, 0x08, 0x43, 0x08, 0x5e, 0x08, 0x79, 0x08, 0x93, 0x08,
|
||||
0xae, 0x08, 0xc9, 0x08, 0xe3, 0x08, 0xfe, 0x08, 0x19, 0x09, 0x33, 0x09, 0x4e, 0x09, 0x68, 0x09, 0x82, 0x09, 0x9d,
|
||||
0x09, 0xb7, 0x09, 0xd1, 0x09, 0xeb, 0x09, 0x05, 0x0a, 0x1f, 0x0a, 0x39, 0x0a, 0x53, 0x0a, 0x6c, 0x0a, 0x86, 0x0a,
|
||||
0x9f, 0x0a, 0xb8, 0x0a, 0xd1, 0x0a, 0xea, 0x0a, 0x03, 0x0b, 0x1c, 0x0b, 0x35, 0x0b, 0x4d, 0x0b, 0x66, 0x0b, 0x7e,
|
||||
0x0b, 0x96, 0x0b, 0xae, 0x0b, 0xc5, 0x0b, 0xdd, 0x0b, 0xf4, 0x0b, 0x0c, 0x0c, 0x23, 0x0c, 0x39, 0x0c, 0x50, 0x0c,
|
||||
0x67, 0x0c, 0x7d, 0x0c, 0x93, 0x0c, 0xa9, 0x0c, 0xbf, 0x0c, 0xd4, 0x0c, 0xe9, 0x0c, 0xfe, 0x0c, 0x13, 0x0d, 0x28,
|
||||
0x0d, 0x3c, 0x0d, 0x50, 0x0d, 0x64, 0x0d, 0x78, 0x0d, 0x8b, 0x0d, 0x9f, 0x0d, 0xb2, 0x0d, 0xc4, 0x0d, 0xd7, 0x0d,
|
||||
0xe9, 0x0d, 0xfb, 0x0d, 0x0d, 0x0e, 0x1e, 0x0e, 0x2f, 0x0e, 0x40, 0x0e, 0x51, 0x0e, 0x61, 0x0e, 0x71, 0x0e, 0x81,
|
||||
0x0e, 0x90, 0x0e, 0x9f, 0x0e, 0xae, 0x0e, 0xbd, 0x0e, 0xcb, 0x0e, 0xd9, 0x0e, 0xe7, 0x0e, 0xf4, 0x0e, 0x01, 0x0f,
|
||||
0x0e, 0x0f, 0x1b, 0x0f, 0x27, 0x0f, 0x33, 0x0f, 0x3e, 0x0f, 0x49, 0x0f, 0x54, 0x0f, 0x5f, 0x0f, 0x69, 0x0f, 0x73,
|
||||
0x0f, 0x7d, 0x0f, 0x86, 0x0f, 0x8f, 0x0f, 0x98, 0x0f, 0xa0, 0x0f, 0xa8, 0x0f, 0xaf, 0x0f, 0xb7, 0x0f, 0xbe, 0x0f,
|
||||
0xc4, 0x0f, 0xcb, 0x0f, 0xd0, 0x0f, 0xd6, 0x0f, 0xdb, 0x0f, 0xe0, 0x0f, 0xe5, 0x0f, 0xe9, 0x0f, 0xed, 0x0f, 0xf0,
|
||||
0x0f, 0xf3, 0x0f, 0xf6, 0x0f, 0xf9, 0x0f, 0xfb, 0x0f, 0xfc, 0x0f, 0xfe, 0x0f, 0xff, 0x0f, 0x00, 0x10, 0x00, 0x10,
|
||||
0x00, 0x10, 0x00, 0x10, 0xff, 0x0f, 0xfe, 0x0f, 0xfc, 0x0f, 0xfb, 0x0f, 0xf9, 0x0f, 0xf6, 0x0f, 0xf3, 0x0f, 0xf0,
|
||||
0x0f, 0xed, 0x0f, 0xe9, 0x0f, 0xe5, 0x0f, 0xe0, 0x0f, 0xdb, 0x0f, 0xd6, 0x0f, 0xd0, 0x0f, 0xcb, 0x0f, 0xc4, 0x0f,
|
||||
0xbe, 0x0f, 0xb7, 0x0f, 0xaf, 0x0f, 0xa8, 0x0f, 0xa0, 0x0f, 0x98, 0x0f, 0x8f, 0x0f, 0x86, 0x0f, 0x7d, 0x0f, 0x73,
|
||||
0x0f, 0x69, 0x0f, 0x5f, 0x0f, 0x54, 0x0f, 0x49, 0x0f, 0x3e, 0x0f, 0x33, 0x0f, 0x27, 0x0f, 0x1b, 0x0f, 0x0e, 0x0f,
|
||||
0x01, 0x0f, 0xf4, 0x0e, 0xe7, 0x0e, 0xd9, 0x0e, 0xcb, 0x0e, 0xbd, 0x0e, 0xae, 0x0e, 0x9f, 0x0e, 0x90, 0x0e, 0x81,
|
||||
0x0e, 0x71, 0x0e, 0x61, 0x0e, 0x51, 0x0e, 0x40, 0x0e, 0x2f, 0x0e, 0x1e, 0x0e, 0x0d, 0x0e, 0xfb, 0x0d, 0xe9, 0x0d,
|
||||
0xd7, 0x0d, 0xc4, 0x0d, 0xb2, 0x0d, 0x9f, 0x0d, 0x8b, 0x0d, 0x78, 0x0d, 0x64, 0x0d, 0x50, 0x0d, 0x3c, 0x0d, 0x28,
|
||||
0x0d, 0x13, 0x0d, 0xfe, 0x0c, 0xe9, 0x0c, 0xd4, 0x0c, 0xbf, 0x0c, 0xa9, 0x0c, 0x93, 0x0c, 0x7d, 0x0c, 0x67, 0x0c,
|
||||
0x50, 0x0c, 0x39, 0x0c, 0x23, 0x0c, 0x0c, 0x0c, 0xf4, 0x0b, 0xdd, 0x0b, 0xc5, 0x0b, 0xae, 0x0b, 0x96, 0x0b, 0x7e,
|
||||
0x0b, 0x66, 0x0b, 0x4d, 0x0b, 0x35, 0x0b, 0x1c, 0x0b, 0x03, 0x0b, 0xea, 0x0a, 0xd1, 0x0a, 0xb8, 0x0a, 0x9f, 0x0a,
|
||||
0x86, 0x0a, 0x6c, 0x0a, 0x53, 0x0a, 0x39, 0x0a, 0x1f, 0x0a, 0x05, 0x0a, 0xeb, 0x09, 0xd1, 0x09, 0xb7, 0x09, 0x9d,
|
||||
0x09, 0x82, 0x09, 0x68, 0x09, 0x4e, 0x09, 0x33, 0x09, 0x19, 0x09, 0xfe, 0x08, 0xe3, 0x08, 0xc9, 0x08, 0xae, 0x08,
|
||||
0x93, 0x08, 0x79, 0x08, 0x5e, 0x08, 0x43, 0x08, 0x28, 0x08, 0x0d, 0x08, 0xf3, 0x07, 0xd8, 0x07, 0xbd, 0x07, 0xa2,
|
||||
0x07, 0x87, 0x07, 0x6d, 0x07, 0x52, 0x07, 0x37, 0x07, 0x1d, 0x07, 0x02, 0x07, 0xe7, 0x06, 0xcd, 0x06, 0xb2, 0x06,
|
||||
0x98, 0x06, 0x7e, 0x06, 0x63, 0x06, 0x49, 0x06, 0x2f, 0x06, 0x15, 0x06, 0xfb, 0x05, 0xe1, 0x05, 0xc7, 0x05, 0xad,
|
||||
0x05, 0x94, 0x05, 0x7a, 0x05, 0x61, 0x05, 0x48, 0x05, 0x2f, 0x05, 0x16, 0x05, 0xfd, 0x04, 0xe4, 0x04, 0xcb, 0x04,
|
||||
0xb3, 0x04, 0x9a, 0x04, 0x82, 0x04, 0x6a, 0x04, 0x52, 0x04, 0x3b, 0x04, 0x23, 0x04, 0x0c, 0x04, 0xf4, 0x03, 0xdd,
|
||||
0x03, 0xc7, 0x03, 0xb0, 0x03, 0x99, 0x03, 0x83, 0x03, 0x6d, 0x03, 0x57, 0x03, 0x41, 0x03, 0x2c, 0x03, 0x17, 0x03,
|
||||
0x02, 0x03, 0xed, 0x02, 0xd8, 0x02, 0xc4, 0x02, 0xb0, 0x02, 0x9c, 0x02, 0x88, 0x02, 0x75, 0x02, 0x61, 0x02, 0x4e,
|
||||
0x02, 0x3c, 0x02, 0x29, 0x02, 0x17, 0x02, 0x05, 0x02, 0xf3, 0x01, 0xe2, 0x01, 0xd1, 0x01, 0xc0, 0x01, 0xaf, 0x01,
|
||||
0x9f, 0x01, 0x8f, 0x01, 0x7f, 0x01, 0x70, 0x01, 0x61, 0x01, 0x52, 0x01, 0x43, 0x01, 0x35, 0x01, 0x27, 0x01, 0x19,
|
||||
0x01, 0x0c, 0x01, 0xff, 0x00, 0xf2, 0x00, 0xe5, 0x00, 0xd9, 0x00, 0xcd, 0x00, 0xc2, 0x00, 0xb7, 0x00, 0xac, 0x00,
|
||||
0xa1, 0x00, 0x97, 0x00, 0x8d, 0x00, 0x83, 0x00, 0x7a, 0x00, 0x71, 0x00, 0x68, 0x00, 0x60, 0x00, 0x58, 0x00, 0x51,
|
||||
0x00, 0x49, 0x00, 0x42, 0x00, 0x3c, 0x00, 0x35, 0x00, 0x30, 0x00, 0x2a, 0x00, 0x25, 0x00, 0x20, 0x00, 0x1b, 0x00,
|
||||
0x17, 0x00, 0x13, 0x00, 0x10, 0x00, 0x0d, 0x00, 0x0a, 0x00, 0x07, 0x00, 0x05, 0x00, 0x04, 0x00, 0x02, 0x00, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xee, 0xff, 0xff, 0x38, 0xee, 0xff, 0xff, 0x0f, 0x00, 0x00, 0x00, 0x4d, 0x4c,
|
||||
0x49, 0x52, 0x20, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x65, 0x64, 0x2e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x14,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x18, 0x00, 0x14, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x04, 0x00,
|
||||
0x0e, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xf4, 0x05, 0x00, 0x00, 0xf8, 0x05, 0x00,
|
||||
0x00, 0xfc, 0x05, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00,
|
||||
0x00, 0x00, 0xa0, 0x05, 0x00, 0x00, 0x68, 0x05, 0x00, 0x00, 0x24, 0x05, 0x00, 0x00, 0xc8, 0x04, 0x00, 0x00, 0x70,
|
||||
0x04, 0x00, 0x00, 0x4c, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0xc8, 0x03, 0x00, 0x00, 0xa4, 0x03, 0x00, 0x00,
|
||||
0x4c, 0x03, 0x00, 0x00, 0x14, 0x03, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00, 0x6c, 0x01, 0x00,
|
||||
0x00, 0x48, 0x01, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0x78, 0x00,
|
||||
0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xf6, 0xfa, 0xff, 0xff, 0x0c,
|
||||
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x16, 0xfb, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00,
|
||||
0x00, 0x11, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x28, 0x00,
|
||||
0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x3a, 0xfb, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00,
|
||||
0x0e, 0x00, 0x00, 0x00, 0xa6, 0xfc, 0xff, 0xff, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x10, 0x00, 0x00,
|
||||
0x00, 0x14, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x68, 0xef, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x27, 0x00,
|
||||
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xd6, 0xfc, 0xff, 0xff, 0x14,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x10, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00,
|
||||
0x98, 0xef, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00,
|
||||
0x00, 0x12, 0x00, 0x00, 0x00, 0x06, 0xfd, 0xff, 0xff, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x10, 0x00,
|
||||
0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xc8, 0xef, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x25,
|
||||
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x36, 0xfd, 0xff, 0xff,
|
||||
0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x10, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00,
|
||||
0x00, 0xf8, 0xef, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00,
|
||||
0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x1e, 0xfc, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x05,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00,
|
||||
0x84, 0xfc, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00,
|
||||
0x00, 0x30, 0x00, 0x00, 0x00, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x63, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x5f, 0x62, 0x69, 0x74, 0x73, 0x00, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x73, 0x63, 0x61, 0x6c,
|
||||
0x65, 0x00, 0x02, 0x24, 0x0f, 0x02, 0x01, 0x02, 0x03, 0x40, 0x04, 0x04, 0x04, 0x24, 0x01, 0x01, 0x00, 0x00, 0x00,
|
||||
0x22, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0xdc, 0xfc, 0xff, 0xff, 0x10, 0x00, 0x00,
|
||||
0x00, 0x24, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x73, 0x6e,
|
||||
0x72, 0x5f, 0x73, 0x68, 0x69, 0x66, 0x74, 0x00, 0x01, 0x0b, 0x01, 0x01, 0x01, 0x06, 0x04, 0x02, 0x24, 0x01, 0x01,
|
||||
0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
|
||||
0x08, 0x00, 0x00, 0x00, 0x20, 0xfd, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00,
|
||||
0x00, 0x0a, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x61, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x65, 0x5f,
|
||||
0x6f, 0x6e, 0x65, 0x5f, 0x6d, 0x69, 0x6e, 0x75, 0x73, 0x5f, 0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67,
|
||||
0x00, 0x61, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e,
|
||||
0x67, 0x00, 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x69, 0x6e, 0x67, 0x00, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x69, 0x67, 0x6e,
|
||||
0x61, 0x6c, 0x5f, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x00, 0x6e, 0x75, 0x6d, 0x5f, 0x63, 0x68,
|
||||
0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x00, 0x6f, 0x6e, 0x65, 0x5f, 0x6d, 0x69, 0x6e, 0x75, 0x73, 0x5f, 0x73, 0x6d,
|
||||
0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x00, 0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x00, 0x73,
|
||||
0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x62, 0x69, 0x74, 0x73, 0x00, 0x73, 0x70, 0x65, 0x63, 0x74,
|
||||
0x72, 0x61, 0x6c, 0x5f, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x62, 0x69, 0x74,
|
||||
0x73, 0x00, 0x09, 0xa5, 0x88, 0x75, 0x6d, 0x59, 0x4d, 0x3a, 0x31, 0x23, 0x09, 0x00, 0x01, 0x00, 0x09, 0x00, 0x29,
|
||||
0x3c, 0xd7, 0x03, 0x00, 0x00, 0x33, 0x03, 0x28, 0x00, 0x67, 0x3e, 0x99, 0x01, 0x0a, 0x00, 0x0e, 0x00, 0x05, 0x05,
|
||||
0x69, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x1b, 0x25, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00,
|
||||
0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x20, 0xfe, 0xff, 0xff, 0x10, 0x00,
|
||||
0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x24, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
||||
0x1d, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x54, 0xfe, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00,
|
||||
0x00, 0x2c, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x6e, 0x75, 0x6d, 0x5f, 0x63, 0x68,
|
||||
0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x00, 0x01, 0x0e, 0x01, 0x01, 0x01, 0x28, 0x04, 0x02, 0x24, 0x01, 0x00, 0x01,
|
||||
0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00,
|
||||
0x0c, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x62, 0xfe, 0xff,
|
||||
0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1c, 0x00,
|
||||
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0xca, 0xff, 0xff, 0xff, 0x14, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x0a, 0x10, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8c, 0xf2, 0xff, 0xff,
|
||||
0x01, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00,
|
||||
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x18, 0x00, 0x14, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x0b, 0x00,
|
||||
0x04, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x00, 0x00, 0x14,
|
||||
0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd0, 0xf2, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00,
|
||||
0x04, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00,
|
||||
0x00, 0xfe, 0xfe, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x64, 0xff, 0xff, 0xff, 0x10,
|
||||
0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00,
|
||||
0x65, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x69, 0x6e, 0x64,
|
||||
0x65, 0x78, 0x00, 0x02, 0x17, 0x0e, 0x00, 0x03, 0x00, 0x01, 0x00, 0x02, 0x00, 0xf1, 0x00, 0x05, 0x00, 0x05, 0x05,
|
||||
0x06, 0x25, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x17,
|
||||
0x00, 0x00, 0x00, 0xb8, 0xff, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00,
|
||||
0x03, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x54, 0x00, 0x66, 0x66, 0x74, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74,
|
||||
0x68, 0x00, 0x02, 0x0e, 0x0d, 0x02, 0x00, 0x01, 0x00, 0x02, 0x00, 0x07, 0x00, 0x00, 0x02, 0x05, 0x05, 0x06, 0x25,
|
||||
0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x10,
|
||||
0x00, 0x14, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00,
|
||||
0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x24, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x16, 0x00,
|
||||
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08,
|
||||
0x00, 0x04, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00,
|
||||
0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x10, 0x00,
|
||||
0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73,
|
||||
0x68, 0x69, 0x66, 0x74, 0x00, 0x01, 0x07, 0x01, 0x01, 0x01, 0x0c, 0x04, 0x02, 0x24, 0x01, 0x01, 0x00, 0x00, 0x00,
|
||||
0x13, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x2a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xc4, 0x0a,
|
||||
0x00, 0x00, 0x74, 0x0a, 0x00, 0x00, 0x3c, 0x0a, 0x00, 0x00, 0x04, 0x0a, 0x00, 0x00, 0xd0, 0x09, 0x00, 0x00, 0x88,
|
||||
0x09, 0x00, 0x00, 0x40, 0x09, 0x00, 0x00, 0xfc, 0x08, 0x00, 0x00, 0xb8, 0x08, 0x00, 0x00, 0x6c, 0x08, 0x00, 0x00,
|
||||
0x20, 0x08, 0x00, 0x00, 0xd4, 0x07, 0x00, 0x00, 0x88, 0x07, 0x00, 0x00, 0x3c, 0x07, 0x00, 0x00, 0xf8, 0x06, 0x00,
|
||||
0x00, 0xb8, 0x06, 0x00, 0x00, 0x84, 0x06, 0x00, 0x00, 0x50, 0x06, 0x00, 0x00, 0x1c, 0x06, 0x00, 0x00, 0xd8, 0x05,
|
||||
0x00, 0x00, 0xa0, 0x05, 0x00, 0x00, 0x58, 0x05, 0x00, 0x00, 0x14, 0x05, 0x00, 0x00, 0xd8, 0x04, 0x00, 0x00, 0x98,
|
||||
0x04, 0x00, 0x00, 0x60, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0xb0, 0x03, 0x00, 0x00,
|
||||
0x6c, 0x03, 0x00, 0x00, 0x1c, 0x03, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, 0x68, 0x02, 0x00, 0x00, 0x2c, 0x02, 0x00,
|
||||
0x00, 0xe4, 0x01, 0x00, 0x00, 0xac, 0x01, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x08, 0x01,
|
||||
0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xfe,
|
||||
0xf5, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x09, 0x20, 0x00, 0x00, 0x00, 0x44, 0xf5, 0xff, 0xff, 0x11, 0x00, 0x00, 0x00, 0x50, 0x61, 0x72,
|
||||
0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x43, 0x61, 0x6c, 0x6c, 0x3a, 0x30, 0x00, 0x00, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x3e, 0xf6, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14,
|
||||
0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1c, 0x00, 0x00, 0x00, 0x84, 0xf5, 0xff, 0xff,
|
||||
0x0d, 0x00, 0x00, 0x00, 0x63, 0x6c, 0x69, 0x70, 0x5f, 0x62, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x7a, 0xf6, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00,
|
||||
0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x24, 0x00, 0x00, 0x00, 0xc0,
|
||||
0xf5, 0xff, 0xff, 0x15, 0x00, 0x00, 0x00, 0x63, 0x6c, 0x69, 0x70, 0x5f, 0x62, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x2f, 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00,
|
||||
0x00, 0xbe, 0xf6, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x28, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0x04, 0xf6, 0xff, 0xff, 0x05, 0x00, 0x00, 0x00, 0x61,
|
||||
0x64, 0x64, 0x5f, 0x31, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xf2, 0xf6, 0xff, 0xff,
|
||||
0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x02, 0x18, 0x00, 0x00, 0x00, 0x38, 0xf6, 0xff, 0xff, 0x0b, 0x00, 0x00, 0x00, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61,
|
||||
0x74, 0x65, 0x44, 0x69, 0x76, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x2a, 0xf7, 0xff, 0xff, 0x00,
|
||||
0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
|
||||
0x10, 0x00, 0x00, 0x00, 0x70, 0xf6, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x61, 0x64, 0x64, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x28, 0x00, 0x00, 0x00, 0x5a, 0xf7, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00,
|
||||
0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x00, 0x00, 0xa0, 0xf6, 0xff, 0xff, 0x03,
|
||||
0x00, 0x00, 0x00, 0x6d, 0x75, 0x6c, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x8a, 0xf7, 0xff, 0xff,
|
||||
0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x02, 0x14, 0x00, 0x00, 0x00, 0xd0, 0xf6, 0xff, 0xff, 0x06, 0x00, 0x00, 0x00, 0x43, 0x61, 0x73, 0x74, 0x5f, 0x32,
|
||||
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xbe, 0xf7, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14,
|
||||
0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x24, 0x00, 0x00, 0x00,
|
||||
0x04, 0xf7, 0xff, 0xff, 0x16, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74,
|
||||
0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x6c, 0x6f, 0x67, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00,
|
||||
0x00, 0x00, 0x02, 0xf8, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x22,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x18, 0x00, 0x00, 0x00, 0x48, 0xf7, 0xff, 0xff, 0x0b, 0x00, 0x00, 0x00,
|
||||
0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x63, 0x61, 0x6e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00,
|
||||
0x00, 0x3a, 0xf8, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x21, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x38, 0x00, 0x00, 0x00, 0x80, 0xf7, 0xff, 0xff, 0x28, 0x00, 0x00, 0x00, 0x73,
|
||||
0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x73,
|
||||
0x70, 0x65, 0x63, 0x74, 0x72, 0x61, 0x6c, 0x5f, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x31, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x92, 0xf8, 0xff, 0xff, 0x00, 0x00,
|
||||
0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x34,
|
||||
0x00, 0x00, 0x00, 0xd8, 0xf7, 0xff, 0xff, 0x27, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66,
|
||||
0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x74, 0x72, 0x61, 0x6c,
|
||||
0x5f, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00,
|
||||
0x00, 0x00, 0xe6, 0xf8, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1f,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x2c, 0x00, 0x00, 0x00, 0x2c, 0xf8, 0xff, 0xff, 0x1e, 0x00, 0x00, 0x00,
|
||||
0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x5f,
|
||||
0x73, 0x71, 0x75, 0x61, 0x72, 0x65, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00,
|
||||
0x00, 0x00, 0x32, 0xf9, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1e,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x20, 0x00, 0x00, 0x00, 0x78, 0xf8, 0xff, 0xff, 0x12, 0x00, 0x00, 0x00,
|
||||
0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x72, 0xf9, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00,
|
||||
0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x14, 0x00, 0x00, 0x00, 0xb8,
|
||||
0xf8, 0xff, 0xff, 0x06, 0x00, 0x00, 0x00, 0x43, 0x61, 0x73, 0x74, 0x5f, 0x31, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0x01, 0x01, 0x00, 0x00, 0xa6, 0xf9, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00,
|
||||
0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0xec, 0xf8, 0xff, 0xff, 0x06, 0x00,
|
||||
0x00, 0x00, 0x63, 0x6f, 0x6e, 0x63, 0x61, 0x74, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0xda,
|
||||
0xf9, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x02, 0x1c, 0x00, 0x00, 0x00, 0x20, 0xf9, 0xff, 0xff, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72,
|
||||
0x69, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xec, 0x00,
|
||||
0x00, 0x00, 0x16, 0xfa, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1a,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0x5c, 0xf9, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00,
|
||||
0x43, 0x61, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x4a, 0xfa, 0xff,
|
||||
0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x0f, 0x1c, 0x00, 0x00, 0x00, 0x90, 0xf9, 0xff, 0xff, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61,
|
||||
0x6c, 0x5f, 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00,
|
||||
0x86, 0xfa, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x07, 0x18, 0x00, 0x00, 0x00, 0xcc, 0xf9, 0xff, 0xff, 0x0b, 0x00, 0x00, 0x00, 0x73, 0x69,
|
||||
0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x66, 0x66, 0x74, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0xbe,
|
||||
0xfa, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x02, 0x24, 0x00, 0x00, 0x00, 0x04, 0xfa, 0xff, 0xff, 0x16, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67,
|
||||
0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x66, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x5f, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x31,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xfa, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14,
|
||||
0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x24, 0x00, 0x00, 0x00, 0x44, 0xfa, 0xff, 0xff,
|
||||
0x15, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x66, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x6f,
|
||||
0x5f, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x42, 0xfb,
|
||||
0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x07, 0x14, 0x00, 0x00, 0x00, 0x88, 0xfa, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x52, 0x65, 0x73, 0x68,
|
||||
0x61, 0x70, 0x65, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x76, 0xfb, 0xff, 0xff, 0x00, 0x00, 0x00,
|
||||
0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x1c, 0x00,
|
||||
0x00, 0x00, 0xbc, 0xfa, 0xff, 0xff, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x77, 0x69,
|
||||
0x6e, 0x64, 0x6f, 0x77, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00,
|
||||
0xb6, 0xfb, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0xfc, 0xfa, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x43, 0x6f,
|
||||
0x6e, 0x73, 0x74, 0x5f, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe6, 0xfb, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14,
|
||||
0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00,
|
||||
0x2c, 0xfb, 0xff, 0xff, 0x06, 0x00, 0x00, 0x00, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x16, 0xfc, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x11, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0x5c, 0xfb, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x43,
|
||||
0x6f, 0x6e, 0x73, 0x74, 0x5f, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0xfc, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01,
|
||||
0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1c, 0x00, 0x00,
|
||||
0x00, 0x8c, 0xfb, 0xff, 0xff, 0x0d, 0x00, 0x00, 0x00, 0x52, 0x65, 0x73, 0x68, 0x61, 0x70, 0x65, 0x2f, 0x73, 0x68,
|
||||
0x61, 0x70, 0x65, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x82, 0xfc, 0xff, 0xff, 0x00,
|
||||
0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
|
||||
0x24, 0x00, 0x00, 0x00, 0xc8, 0xfb, 0xff, 0xff, 0x17, 0x00, 0x00, 0x00, 0x63, 0x6c, 0x69, 0x70, 0x5f, 0x62, 0x79,
|
||||
0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2f, 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x2f, 0x79, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0xc2, 0xfc, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0e,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x28, 0x00, 0x00, 0x00, 0x08, 0xfc, 0xff, 0xff, 0x18, 0x00, 0x00, 0x00,
|
||||
0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x2f,
|
||||
0x43, 0x6f, 0x6e, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x0a, 0xfd,
|
||||
0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x07, 0x28, 0x00, 0x00, 0x00, 0x50, 0xfc, 0xff, 0xff, 0x1a, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e,
|
||||
0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x2f, 0x43, 0x6f, 0x6e, 0x73,
|
||||
0x74, 0x5f, 0x31, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x52, 0xfd, 0xff, 0xff, 0x00, 0x00,
|
||||
0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x28,
|
||||
0x00, 0x00, 0x00, 0x98, 0xfc, 0xff, 0xff, 0x1a, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66,
|
||||
0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x5f, 0x32, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x9a, 0xfd, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00,
|
||||
0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x28, 0x00, 0x00, 0x00, 0xe0,
|
||||
0xfc, 0xff, 0xff, 0x1a, 0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65,
|
||||
0x72, 0x5f, 0x62, 0x61, 0x6e, 0x6b, 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x5f, 0x33, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x29, 0x00, 0x00, 0x00, 0xe2, 0xfd, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00,
|
||||
0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x28, 0x00, 0x00, 0x00, 0x28, 0xfd, 0xff, 0xff, 0x1a,
|
||||
0x00, 0x00, 0x00, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x62, 0x61,
|
||||
0x6e, 0x6b, 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x5f, 0x34, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00,
|
||||
0x00, 0x2a, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x09, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x20, 0x00, 0x00, 0x00, 0x70, 0xfd, 0xff, 0xff, 0x11, 0x00, 0x00, 0x00, 0x73,
|
||||
0x69, 0x67, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x63, 0x61, 0x6e, 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x6a, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00,
|
||||
0x00, 0x14, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x00, 0xb0, 0xfd,
|
||||
0xff, 0xff, 0x13, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x69, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x6c, 0x69, 0x63, 0x65,
|
||||
0x2f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xaa, 0xfe, 0xff, 0xff,
|
||||
0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x02, 0x24, 0x00, 0x00, 0x00, 0xf0, 0xfd, 0xff, 0xff, 0x15, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x69, 0x64, 0x65,
|
||||
0x64, 0x5f, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x2f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x31, 0x00, 0x00, 0x00, 0x01,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xee, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00,
|
||||
0x14, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x24, 0x00, 0x00, 0x00, 0x34, 0xfe, 0xff,
|
||||
0xff, 0x15, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x69, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x2f,
|
||||
0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x32, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x32,
|
||||
0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0x78, 0xfe, 0xff, 0xff, 0x06, 0x00, 0x00, 0x00, 0x43, 0x61, 0x73,
|
||||
0x74, 0x5f, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00,
|
||||
0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0xa8,
|
||||
0xfe, 0xff, 0xff, 0x05, 0x00, 0x00, 0x00, 0x7a, 0x65, 0x72, 0x6f, 0x73, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0x05, 0x00, 0x00, 0x00, 0x96, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00,
|
||||
0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x00, 0xdc, 0xfe, 0xff, 0xff, 0x07, 0x00,
|
||||
0x00, 0x00, 0x7a, 0x65, 0x72, 0x6f, 0x73, 0x5f, 0x31, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xca,
|
||||
0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x07, 0x14, 0x00, 0x00, 0x00, 0x10, 0xff, 0xff, 0xff, 0x05, 0x00, 0x00, 0x00, 0x43, 0x6f, 0x6e,
|
||||
0x73, 0x74, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x1c, 0x00,
|
||||
0x18, 0x00, 0x17, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x16,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x07, 0x2c, 0x00, 0x00, 0x00, 0x5c, 0xff, 0xff, 0xff, 0x1d, 0x00, 0x00, 0x00, 0x73, 0x65, 0x72,
|
||||
0x76, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x5f,
|
||||
0x66, 0x72, 0x61, 0x6d, 0x65, 0x3a, 0x30, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0,
|
||||
0x01, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00,
|
||||
0xa4, 0x01, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0x3c, 0x01, 0x00,
|
||||
0x00, 0x10, 0x01, 0x00, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x50, 0x00,
|
||||
0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x04,
|
||||
0x00, 0x00, 0x00, 0x50, 0xfe, 0xff, 0xff, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x5c, 0xfe, 0xff, 0xff,
|
||||
0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x68, 0xfe, 0xff, 0xff, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x2a, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x7c, 0xfe, 0xff, 0xff, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x12, 0x70, 0xfe, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x13,
|
||||
0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x61, 0x6e, 0x6b,
|
||||
0x4c, 0x6f, 0x67, 0x00, 0x98, 0xfe, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x20, 0x0a, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x50, 0x43, 0x41, 0x4e, 0x00, 0x00, 0xb8, 0xfe,
|
||||
0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x23, 0x00, 0x00, 0x00, 0x53,
|
||||
0x69, 0x67, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x61, 0x6e, 0x6b, 0x53, 0x70, 0x65, 0x63,
|
||||
0x74, 0x72, 0x61, 0x6c, 0x53, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0xf0, 0xfe, 0xff,
|
||||
0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x1a, 0x00, 0x00, 0x00, 0x53, 0x69,
|
||||
0x67, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x61, 0x6e, 0x6b, 0x53, 0x71, 0x75, 0x61, 0x72,
|
||||
0x65, 0x52, 0x6f, 0x6f, 0x74, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x20, 0x10, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x74, 0x65,
|
||||
0x72, 0x42, 0x61, 0x6e, 0x6b, 0x00, 0x00, 0x00, 0x00, 0x60, 0xff, 0xff, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x02, 0x6c, 0xff, 0xff, 0xff, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x0c, 0x00, 0x10, 0x00, 0x0f,
|
||||
0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x35, 0x7c, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x20, 0x0c, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x45, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x00, 0x00,
|
||||
0x00, 0x00, 0xa0, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0a,
|
||||
0x00, 0x00, 0x00, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x52, 0x66, 0x66, 0x74, 0x00, 0x00, 0xc0, 0xff, 0xff, 0xff,
|
||||
0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x12, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67,
|
||||
0x6e, 0x61, 0x6c, 0x46, 0x66, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x00, 0x00, 0x0c, 0x00,
|
||||
0x0c, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x16, 0x0c, 0x00, 0x10, 0x00, 0x0f, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x00, 0x00,
|
||||
0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0c, 0x00, 0x00, 0x00, 0x53, 0x69, 0x67,
|
||||
0x6e, 0x61, 0x6c, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
} // namespace micro_wake_word
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP_IDF
|
||||
503
esphome/components/micro_wake_word/micro_wake_word.cpp
Normal file
503
esphome/components/micro_wake_word/micro_wake_word.cpp
Normal file
@ -0,0 +1,503 @@
|
||||
#include "micro_wake_word.h"
|
||||
|
||||
/**
|
||||
* This is a workaround until we can figure out a way to get
|
||||
* the tflite-micro idf component code available in CI
|
||||
*
|
||||
* */
|
||||
//
|
||||
#ifndef CLANG_TIDY
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "audio_preprocessor_int8_model_data.h"
|
||||
|
||||
#include <tensorflow/lite/core/c/common.h>
|
||||
#include <tensorflow/lite/micro/micro_interpreter.h>
|
||||
#include <tensorflow/lite/micro/micro_mutable_op_resolver.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome {
|
||||
namespace micro_wake_word {
|
||||
|
||||
static const char *const TAG = "micro_wake_word";
|
||||
|
||||
static const size_t SAMPLE_RATE_HZ = 16000; // 16 kHz
|
||||
static const size_t BUFFER_LENGTH = 500; // 0.5 seconds
|
||||
static const size_t BUFFER_SIZE = SAMPLE_RATE_HZ / 1000 * BUFFER_LENGTH;
|
||||
static const size_t INPUT_BUFFER_SIZE = 32 * SAMPLE_RATE_HZ / 1000; // 32ms * 16kHz / 1000ms
|
||||
|
||||
float MicroWakeWord::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
static const LogString *micro_wake_word_state_to_string(State state) {
|
||||
switch (state) {
|
||||
case State::IDLE:
|
||||
return LOG_STR("IDLE");
|
||||
case State::START_MICROPHONE:
|
||||
return LOG_STR("START_MICROPHONE");
|
||||
case State::STARTING_MICROPHONE:
|
||||
return LOG_STR("STARTING_MICROPHONE");
|
||||
case State::DETECTING_WAKE_WORD:
|
||||
return LOG_STR("DETECTING_WAKE_WORD");
|
||||
case State::STOP_MICROPHONE:
|
||||
return LOG_STR("STOP_MICROPHONE");
|
||||
case State::STOPPING_MICROPHONE:
|
||||
return LOG_STR("STOPPING_MICROPHONE");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
void MicroWakeWord::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up Micro Wake Word...");
|
||||
|
||||
if (!this->initialize_models()) {
|
||||
ESP_LOGE(TAG, "Failed to initialize models");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
ExternalRAMAllocator<int16_t> allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
|
||||
this->input_buffer_ = allocator.allocate(NEW_SAMPLES_TO_GET);
|
||||
if (this->input_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate input buffer");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->ring_buffer_ = RingBuffer::create(BUFFER_SIZE * sizeof(int16_t));
|
||||
if (this->ring_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate ring buffer");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, "Micro Wake Word initialized");
|
||||
}
|
||||
|
||||
int MicroWakeWord::read_microphone_() {
|
||||
size_t bytes_read = this->microphone_->read(this->input_buffer_, NEW_SAMPLES_TO_GET * sizeof(int16_t));
|
||||
if (bytes_read == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t bytes_written = this->ring_buffer_->write((void *) this->input_buffer_, bytes_read);
|
||||
if (bytes_written != bytes_read) {
|
||||
ESP_LOGW(TAG, "Failed to write some data to ring buffer (written=%d, expected=%d)", bytes_written, bytes_read);
|
||||
}
|
||||
return bytes_written;
|
||||
}
|
||||
|
||||
void MicroWakeWord::loop() {
|
||||
switch (this->state_) {
|
||||
case State::IDLE:
|
||||
break;
|
||||
case State::START_MICROPHONE:
|
||||
ESP_LOGD(TAG, "Starting Microphone");
|
||||
this->microphone_->start();
|
||||
this->set_state_(State::STARTING_MICROPHONE);
|
||||
this->high_freq_.start();
|
||||
break;
|
||||
case State::STARTING_MICROPHONE:
|
||||
if (this->microphone_->is_running()) {
|
||||
this->set_state_(State::DETECTING_WAKE_WORD);
|
||||
}
|
||||
break;
|
||||
case State::DETECTING_WAKE_WORD:
|
||||
this->read_microphone_();
|
||||
if (this->detect_wake_word_()) {
|
||||
ESP_LOGD(TAG, "Wake Word Detected");
|
||||
this->detected_ = true;
|
||||
this->set_state_(State::STOP_MICROPHONE);
|
||||
}
|
||||
break;
|
||||
case State::STOP_MICROPHONE:
|
||||
ESP_LOGD(TAG, "Stopping Microphone");
|
||||
this->microphone_->stop();
|
||||
this->set_state_(State::STOPPING_MICROPHONE);
|
||||
this->high_freq_.stop();
|
||||
break;
|
||||
case State::STOPPING_MICROPHONE:
|
||||
if (this->microphone_->is_stopped()) {
|
||||
this->set_state_(State::IDLE);
|
||||
if (this->detected_) {
|
||||
this->detected_ = false;
|
||||
this->wake_word_detected_trigger_->trigger("");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MicroWakeWord::start() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGW(TAG, "Wake word component is marked as failed. Please check setup logs");
|
||||
return;
|
||||
}
|
||||
if (this->state_ != State::IDLE) {
|
||||
ESP_LOGW(TAG, "Wake word is already running");
|
||||
return;
|
||||
}
|
||||
this->set_state_(State::START_MICROPHONE);
|
||||
}
|
||||
|
||||
void MicroWakeWord::stop() {
|
||||
if (this->state_ == State::IDLE) {
|
||||
ESP_LOGW(TAG, "Wake word is already stopped");
|
||||
return;
|
||||
}
|
||||
if (this->state_ == State::STOPPING_MICROPHONE) {
|
||||
ESP_LOGW(TAG, "Wake word is already stopping");
|
||||
return;
|
||||
}
|
||||
this->set_state_(State::STOP_MICROPHONE);
|
||||
}
|
||||
|
||||
void MicroWakeWord::set_state_(State state) {
|
||||
ESP_LOGD(TAG, "State changed from %s to %s", LOG_STR_ARG(micro_wake_word_state_to_string(this->state_)),
|
||||
LOG_STR_ARG(micro_wake_word_state_to_string(state)));
|
||||
this->state_ = state;
|
||||
}
|
||||
|
||||
bool MicroWakeWord::initialize_models() {
|
||||
ExternalRAMAllocator<uint8_t> arena_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
ExternalRAMAllocator<int8_t> features_allocator(ExternalRAMAllocator<int8_t>::ALLOW_FAILURE);
|
||||
ExternalRAMAllocator<int16_t> audio_samples_allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
|
||||
|
||||
this->streaming_tensor_arena_ = arena_allocator.allocate(STREAMING_MODEL_ARENA_SIZE);
|
||||
if (this->streaming_tensor_arena_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->streaming_var_arena_ = arena_allocator.allocate(STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
if (this->streaming_var_arena_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate the streaming model variable's tensor arena.");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->preprocessor_tensor_arena_ = arena_allocator.allocate(PREPROCESSOR_ARENA_SIZE);
|
||||
if (this->preprocessor_tensor_arena_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate the audio preprocessor model's tensor arena.");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->new_features_data_ = features_allocator.allocate(PREPROCESSOR_FEATURE_SIZE);
|
||||
if (this->new_features_data_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate the audio features buffer.");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->preprocessor_audio_buffer_ = audio_samples_allocator.allocate(SAMPLE_DURATION_COUNT);
|
||||
if (this->preprocessor_audio_buffer_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate the audio preprocessor's buffer.");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->preprocessor_stride_buffer_ = audio_samples_allocator.allocate(HISTORY_SAMPLES_TO_KEEP);
|
||||
if (this->preprocessor_stride_buffer_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate the audio preprocessor's stride buffer.");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->preprocessor_model_ = tflite::GetModel(G_AUDIO_PREPROCESSOR_INT8_TFLITE);
|
||||
if (this->preprocessor_model_->version() != TFLITE_SCHEMA_VERSION) {
|
||||
ESP_LOGE(TAG, "Wake word's audio preprocessor model's schema is not supported");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->streaming_model_ = tflite::GetModel(this->model_start_);
|
||||
if (this->streaming_model_->version() != TFLITE_SCHEMA_VERSION) {
|
||||
ESP_LOGE(TAG, "Wake word's streaming model's schema is not supported");
|
||||
return false;
|
||||
}
|
||||
|
||||
static tflite::MicroMutableOpResolver<18> preprocessor_op_resolver;
|
||||
static tflite::MicroMutableOpResolver<14> streaming_op_resolver;
|
||||
|
||||
if (!this->register_preprocessor_ops_(preprocessor_op_resolver))
|
||||
return false;
|
||||
if (!this->register_streaming_ops_(streaming_op_resolver))
|
||||
return false;
|
||||
|
||||
tflite::MicroAllocator *ma =
|
||||
tflite::MicroAllocator::Create(this->streaming_var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
this->mrv_ = tflite::MicroResourceVariables::Create(ma, 15);
|
||||
|
||||
static tflite::MicroInterpreter static_preprocessor_interpreter(
|
||||
this->preprocessor_model_, preprocessor_op_resolver, this->preprocessor_tensor_arena_, PREPROCESSOR_ARENA_SIZE);
|
||||
|
||||
static tflite::MicroInterpreter static_streaming_interpreter(this->streaming_model_, streaming_op_resolver,
|
||||
this->streaming_tensor_arena_,
|
||||
STREAMING_MODEL_ARENA_SIZE, this->mrv_);
|
||||
|
||||
this->preprocessor_interperter_ = &static_preprocessor_interpreter;
|
||||
this->streaming_interpreter_ = &static_streaming_interpreter;
|
||||
|
||||
// Allocate tensors for each models.
|
||||
if (this->preprocessor_interperter_->AllocateTensors() != kTfLiteOk) {
|
||||
ESP_LOGE(TAG, "Failed to allocate tensors for the audio preprocessor");
|
||||
return false;
|
||||
}
|
||||
if (this->streaming_interpreter_->AllocateTensors() != kTfLiteOk) {
|
||||
ESP_LOGE(TAG, "Failed to allocate tensors for the streaming model");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify input tensor matches expected values
|
||||
TfLiteTensor *input = this->streaming_interpreter_->input(0);
|
||||
if ((input->dims->size != 3) || (input->dims->data[0] != 1) || (input->dims->data[0] != 1) ||
|
||||
(input->dims->data[1] != 1) || (input->dims->data[2] != PREPROCESSOR_FEATURE_SIZE)) {
|
||||
ESP_LOGE(TAG, "Wake word detection model tensor input dimensions is not 1x1x%u", input->dims->data[2]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input->type != kTfLiteInt8) {
|
||||
ESP_LOGE(TAG, "Wake word detection model tensor input is not int8.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify output tensor matches expected values
|
||||
TfLiteTensor *output = this->streaming_interpreter_->output(0);
|
||||
if ((output->dims->size != 2) || (output->dims->data[0] != 1) || (output->dims->data[1] != 1)) {
|
||||
ESP_LOGE(TAG, "Wake word detection model tensor output dimensions is not 1x1.");
|
||||
}
|
||||
|
||||
if (output->type != kTfLiteUInt8) {
|
||||
ESP_LOGE(TAG, "Wake word detection model tensor input is not uint8.");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->recent_streaming_probabilities_.resize(this->sliding_window_average_size_, 0.0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MicroWakeWord::update_features_() {
|
||||
// Verify we have enough samples for a feature slice
|
||||
if (!this->slice_available_()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retrieve strided audio samples
|
||||
int16_t *audio_samples = nullptr;
|
||||
if (!this->stride_audio_samples_(&audio_samples)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compute the features for the newest audio samples
|
||||
if (!this->generate_single_feature_(audio_samples, SAMPLE_DURATION_COUNT, this->new_features_data_)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
float MicroWakeWord::perform_streaming_inference_() {
|
||||
TfLiteTensor *input = this->streaming_interpreter_->input(0);
|
||||
|
||||
size_t bytes_to_copy = input->bytes;
|
||||
|
||||
memcpy((void *) (tflite::GetTensorData<int8_t>(input)), (const void *) (this->new_features_data_), bytes_to_copy);
|
||||
|
||||
uint32_t prior_invoke = millis();
|
||||
|
||||
TfLiteStatus invoke_status = this->streaming_interpreter_->Invoke();
|
||||
if (invoke_status != kTfLiteOk) {
|
||||
ESP_LOGW(TAG, "Streaming Interpreter Invoke failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Streaming Inference Latency=%u ms", (millis() - prior_invoke));
|
||||
|
||||
TfLiteTensor *output = this->streaming_interpreter_->output(0);
|
||||
|
||||
return static_cast<float>(output->data.uint8[0]) / 255.0;
|
||||
}
|
||||
|
||||
bool MicroWakeWord::detect_wake_word_() {
|
||||
// Preprocess the newest audio samples into features
|
||||
if (!this->update_features_()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform inference
|
||||
uint32_t streaming_size = micros();
|
||||
float streaming_prob = this->perform_streaming_inference_();
|
||||
|
||||
// Add the most recent probability to the sliding window
|
||||
this->recent_streaming_probabilities_[this->last_n_index_] = streaming_prob;
|
||||
++this->last_n_index_;
|
||||
if (this->last_n_index_ == this->sliding_window_average_size_)
|
||||
this->last_n_index_ = 0;
|
||||
|
||||
float sum = 0.0;
|
||||
for (auto &prob : this->recent_streaming_probabilities_) {
|
||||
sum += prob;
|
||||
}
|
||||
|
||||
float sliding_window_average = sum / static_cast<float>(this->sliding_window_average_size_);
|
||||
|
||||
// Ensure we have enough samples since the last positive detection
|
||||
this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0);
|
||||
if (this->ignore_windows_ < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Detect the wake word if the sliding window average is above the cutoff
|
||||
if (sliding_window_average > this->probability_cutoff_) {
|
||||
this->ignore_windows_ = -MIN_SLICES_BEFORE_DETECTION;
|
||||
for (auto &prob : this->recent_streaming_probabilities_) {
|
||||
prob = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void MicroWakeWord::set_sliding_window_average_size(size_t size) {
|
||||
this->sliding_window_average_size_ = size;
|
||||
this->recent_streaming_probabilities_.resize(this->sliding_window_average_size_, 0.0);
|
||||
}
|
||||
|
||||
bool MicroWakeWord::slice_available_() {
|
||||
size_t available = this->ring_buffer_->available();
|
||||
|
||||
return available > (NEW_SAMPLES_TO_GET * sizeof(int16_t));
|
||||
}
|
||||
|
||||
bool MicroWakeWord::stride_audio_samples_(int16_t **audio_samples) {
|
||||
// Copy 320 bytes (160 samples over 10 ms) into preprocessor_audio_buffer_ from history in
|
||||
// preprocessor_stride_buffer_
|
||||
memcpy((void *) (this->preprocessor_audio_buffer_), (void *) (this->preprocessor_stride_buffer_),
|
||||
HISTORY_SAMPLES_TO_KEEP * sizeof(int16_t));
|
||||
|
||||
if (this->ring_buffer_->available() < NEW_SAMPLES_TO_GET * sizeof(int16_t)) {
|
||||
ESP_LOGD(TAG, "Audio Buffer not full enough");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copy 640 bytes (320 samples over 20 ms) from the ring buffer
|
||||
// The first 320 bytes (160 samples over 10 ms) will be from history
|
||||
size_t bytes_read = this->ring_buffer_->read((void *) (this->preprocessor_audio_buffer_ + HISTORY_SAMPLES_TO_KEEP),
|
||||
NEW_SAMPLES_TO_GET * sizeof(int16_t), pdMS_TO_TICKS(200));
|
||||
|
||||
if (bytes_read == 0) {
|
||||
ESP_LOGE(TAG, "Could not read data from Ring Buffer");
|
||||
} else if (bytes_read < NEW_SAMPLES_TO_GET * sizeof(int16_t)) {
|
||||
ESP_LOGD(TAG, "Partial Read of Data by Model");
|
||||
ESP_LOGD(TAG, "Could only read %d bytes when required %d bytes ", bytes_read,
|
||||
(int) (NEW_SAMPLES_TO_GET * sizeof(int16_t)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copy the last 320 bytes (160 samples over 10 ms) from the audio buffer into history stride buffer for the next
|
||||
// iteration
|
||||
memcpy((void *) (this->preprocessor_stride_buffer_), (void *) (this->preprocessor_audio_buffer_ + NEW_SAMPLES_TO_GET),
|
||||
HISTORY_SAMPLES_TO_KEEP * sizeof(int16_t));
|
||||
|
||||
*audio_samples = this->preprocessor_audio_buffer_;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MicroWakeWord::generate_single_feature_(const int16_t *audio_data, const int audio_data_size,
|
||||
int8_t feature_output[PREPROCESSOR_FEATURE_SIZE]) {
|
||||
TfLiteTensor *input = this->preprocessor_interperter_->input(0);
|
||||
TfLiteTensor *output = this->preprocessor_interperter_->output(0);
|
||||
std::copy_n(audio_data, audio_data_size, tflite::GetTensorData<int16_t>(input));
|
||||
|
||||
if (this->preprocessor_interperter_->Invoke() != kTfLiteOk) {
|
||||
ESP_LOGE(TAG, "Failed to preprocess audio for local wake word.");
|
||||
return false;
|
||||
}
|
||||
std::memcpy(feature_output, tflite::GetTensorData<int8_t>(output), PREPROCESSOR_FEATURE_SIZE * sizeof(int8_t));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MicroWakeWord::register_preprocessor_ops_(tflite::MicroMutableOpResolver<18> &op_resolver) {
|
||||
if (op_resolver.AddReshape() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddCast() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddStridedSlice() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddConcatenation() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddMul() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddAdd() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddDiv() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddMinimum() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddMaximum() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddWindow() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddFftAutoScale() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddRfft() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddEnergy() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddFilterBank() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddFilterBankSquareRoot() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddFilterBankSpectralSubtraction() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddPCAN() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddFilterBankLog() != kTfLiteOk)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MicroWakeWord::register_streaming_ops_(tflite::MicroMutableOpResolver<14> &op_resolver) {
|
||||
if (op_resolver.AddCallOnce() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddVarHandle() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddReshape() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddReadVariable() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddStridedSlice() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddConcatenation() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddAssignVariable() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddConv2D() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddMul() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddAdd() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddMean() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddFullyConnected() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddLogistic() != kTfLiteOk)
|
||||
return false;
|
||||
if (op_resolver.AddQuantize() != kTfLiteOk)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace micro_wake_word
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP_IDF
|
||||
|
||||
#endif // CLANG_TIDY
|
||||
204
esphome/components/micro_wake_word/micro_wake_word.h
Normal file
204
esphome/components/micro_wake_word/micro_wake_word.h
Normal file
@ -0,0 +1,204 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* This is a workaround until we can figure out a way to get
|
||||
* the tflite-micro idf component code available in CI
|
||||
*
|
||||
* */
|
||||
//
|
||||
#ifndef CLANG_TIDY
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/ring_buffer.h"
|
||||
|
||||
#include "esphome/components/microphone/microphone.h"
|
||||
|
||||
#include <tensorflow/lite/core/c/common.h>
|
||||
#include <tensorflow/lite/micro/micro_interpreter.h>
|
||||
#include <tensorflow/lite/micro/micro_mutable_op_resolver.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace micro_wake_word {
|
||||
|
||||
// The following are dictated by the preprocessor model
|
||||
//
|
||||
// The number of features the audio preprocessor generates per slice
|
||||
static const uint8_t PREPROCESSOR_FEATURE_SIZE = 40;
|
||||
// How frequently the preprocessor generates a new set of features
|
||||
static const uint8_t FEATURE_STRIDE_MS = 20;
|
||||
// Duration of each slice used as input into the preprocessor
|
||||
static const uint8_t FEATURE_DURATION_MS = 30;
|
||||
// Audio sample frequency in hertz
|
||||
static const uint16_t AUDIO_SAMPLE_FREQUENCY = 16000;
|
||||
// The number of old audio samples that are saved to be part of the next feature window
|
||||
static const uint16_t HISTORY_SAMPLES_TO_KEEP =
|
||||
((FEATURE_DURATION_MS - FEATURE_STRIDE_MS) * (AUDIO_SAMPLE_FREQUENCY / 1000));
|
||||
// The number of new audio samples to receive to be included with the next feature window
|
||||
static const uint16_t NEW_SAMPLES_TO_GET = (FEATURE_STRIDE_MS * (AUDIO_SAMPLE_FREQUENCY / 1000));
|
||||
// The total number of audio samples included in the feature window
|
||||
static const uint16_t SAMPLE_DURATION_COUNT = FEATURE_DURATION_MS * AUDIO_SAMPLE_FREQUENCY / 1000;
|
||||
// Number of bytes in memory needed for the preprocessor arena
|
||||
static const uint32_t PREPROCESSOR_ARENA_SIZE = 9528;
|
||||
|
||||
// The following configure the streaming wake word model
|
||||
//
|
||||
// The number of audio slices to process before accepting a positive detection
|
||||
static const uint8_t MIN_SLICES_BEFORE_DETECTION = 74;
|
||||
|
||||
// Number of bytes in memory needed for the streaming wake word model
|
||||
static const uint32_t STREAMING_MODEL_ARENA_SIZE = 64000;
|
||||
static const uint32_t STREAMING_MODEL_VARIABLE_ARENA_SIZE = 1024;
|
||||
|
||||
enum State {
|
||||
IDLE,
|
||||
START_MICROPHONE,
|
||||
STARTING_MICROPHONE,
|
||||
DETECTING_WAKE_WORD,
|
||||
STOP_MICROPHONE,
|
||||
STOPPING_MICROPHONE,
|
||||
};
|
||||
|
||||
class MicroWakeWord : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
bool is_running() const { return this->state_ != State::IDLE; }
|
||||
|
||||
bool initialize_models();
|
||||
|
||||
// Increasing either of these will reduce the rate of false acceptances while increasing the false rejection rate
|
||||
void set_probability_cutoff(float probability_cutoff) { this->probability_cutoff_ = probability_cutoff; }
|
||||
void set_sliding_window_average_size(size_t size);
|
||||
|
||||
void set_microphone(microphone::Microphone *microphone) { this->microphone_ = microphone; }
|
||||
|
||||
Trigger<std::string> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; }
|
||||
|
||||
void set_model_start(const uint8_t *model_start) { this->model_start_ = model_start; }
|
||||
void set_wake_word(const std::string &wake_word) { this->wake_word_ = wake_word; }
|
||||
|
||||
protected:
|
||||
void set_state_(State state);
|
||||
int read_microphone_();
|
||||
|
||||
const uint8_t *model_start_;
|
||||
std::string wake_word_;
|
||||
|
||||
microphone::Microphone *microphone_{nullptr};
|
||||
Trigger<std::string> *wake_word_detected_trigger_ = new Trigger<std::string>();
|
||||
State state_{State::IDLE};
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
|
||||
std::unique_ptr<RingBuffer> ring_buffer_;
|
||||
|
||||
int16_t *input_buffer_;
|
||||
|
||||
const tflite::Model *preprocessor_model_{nullptr};
|
||||
const tflite::Model *streaming_model_{nullptr};
|
||||
tflite::MicroInterpreter *streaming_interpreter_{nullptr};
|
||||
tflite::MicroInterpreter *preprocessor_interperter_{nullptr};
|
||||
|
||||
std::vector<float> recent_streaming_probabilities_;
|
||||
size_t last_n_index_{0};
|
||||
|
||||
float probability_cutoff_{0.5};
|
||||
size_t sliding_window_average_size_{10};
|
||||
|
||||
// When the wake word detection first starts or after the word has been detected once, we ignore this many audio
|
||||
// feature slices before accepting a positive detection again
|
||||
int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION};
|
||||
|
||||
uint8_t *streaming_var_arena_{nullptr};
|
||||
uint8_t *streaming_tensor_arena_{nullptr};
|
||||
uint8_t *preprocessor_tensor_arena_{nullptr};
|
||||
int8_t *new_features_data_{nullptr};
|
||||
|
||||
tflite::MicroResourceVariables *mrv_{nullptr};
|
||||
|
||||
// Stores audio fed into feature generator preprocessor
|
||||
int16_t *preprocessor_audio_buffer_;
|
||||
int16_t *preprocessor_stride_buffer_;
|
||||
|
||||
bool detected_{false};
|
||||
|
||||
/** Detects if wake word has been said
|
||||
*
|
||||
* If enough audio samples are available, it will generate one slice of new features.
|
||||
* If the streaming model predicts the wake word, then the nonstreaming model confirms it.
|
||||
* @param ring_Buffer Ring buffer containing raw audio samples
|
||||
* @return True if the wake word is detected, false otherwise
|
||||
*/
|
||||
bool detect_wake_word_();
|
||||
|
||||
/// @brief Returns true if there are enough audio samples in the buffer to generate another slice of features
|
||||
bool slice_available_();
|
||||
|
||||
/** Shifts previous feature slices over by one and generates a new slice of features
|
||||
*
|
||||
* @param ring_buffer ring buffer containing raw audio samples
|
||||
* @return True if a new slice of features was generated, false otherwise
|
||||
*/
|
||||
bool update_features_();
|
||||
|
||||
/** Generates features from audio samples
|
||||
*
|
||||
* Adapted from TFLite micro speech example
|
||||
* @param audio_data Pointer to array with the audio samples
|
||||
* @param audio_data_size The number of samples to use as input to the preprocessor model
|
||||
* @param feature_output Array that will store the features
|
||||
* @return True if successful, false otherwise.
|
||||
*/
|
||||
bool generate_single_feature_(const int16_t *audio_data, int audio_data_size,
|
||||
int8_t feature_output[PREPROCESSOR_FEATURE_SIZE]);
|
||||
|
||||
/** Performs inference over the most recent feature slice with the streaming model
|
||||
*
|
||||
* @return Probability of the wake word between 0.0 and 1.0
|
||||
*/
|
||||
float perform_streaming_inference_();
|
||||
|
||||
/** Strides the audio samples by keeping the last 10 ms of the previous slice
|
||||
*
|
||||
* Adapted from the TFLite micro speech example
|
||||
* @param ring_buffer Ring buffer containing raw audio samples
|
||||
* @param audio_samples Pointer to an array that will store the strided audio samples
|
||||
* @return True if successful, false otherwise
|
||||
*/
|
||||
bool stride_audio_samples_(int16_t **audio_samples);
|
||||
|
||||
/// @brief Returns true if successfully registered the preprocessor's TensorFlow operations
|
||||
bool register_preprocessor_ops_(tflite::MicroMutableOpResolver<18> &op_resolver);
|
||||
|
||||
/// @brief Returns true if successfully registered the streaming model's TensorFlow operations
|
||||
bool register_streaming_ops_(tflite::MicroMutableOpResolver<14> &op_resolver);
|
||||
};
|
||||
|
||||
template<typename... Ts> class StartAction : public Action<Ts...>, public Parented<MicroWakeWord> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->start(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class StopAction : public Action<Ts...>, public Parented<MicroWakeWord> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->stop(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class IsRunningCondition : public Condition<Ts...>, public Parented<MicroWakeWord> {
|
||||
public:
|
||||
bool check(Ts... x) override { return this->parent_->is_running(); }
|
||||
};
|
||||
|
||||
} // namespace micro_wake_word
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP_IDF
|
||||
|
||||
#endif // CLANG_TIDY
|
||||
@ -10,6 +10,8 @@ from esphome.const import (
|
||||
CONF_BIRTH_MESSAGE,
|
||||
CONF_BROKER,
|
||||
CONF_CERTIFICATE_AUTHORITY,
|
||||
CONF_CLIENT_CERTIFICATE,
|
||||
CONF_CLIENT_CERTIFICATE_KEY,
|
||||
CONF_CLIENT_ID,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_COMMAND_RETAIN,
|
||||
@ -199,6 +201,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_CERTIFICATE_AUTHORITY): cv.All(
|
||||
cv.string, cv.only_with_esp_idf
|
||||
),
|
||||
cv.Inclusive(CONF_CLIENT_CERTIFICATE, "cert-key-pair"): cv.All(
|
||||
cv.string, cv.only_on_esp32
|
||||
),
|
||||
cv.Inclusive(CONF_CLIENT_CERTIFICATE_KEY, "cert-key-pair"): cv.All(
|
||||
cv.string, cv.only_on_esp32
|
||||
),
|
||||
cv.SplitDefault(CONF_SKIP_CERT_CN_CHECK, esp32_idf=False): cv.All(
|
||||
cv.boolean, cv.only_with_esp_idf
|
||||
),
|
||||
@ -378,6 +386,9 @@ async def to_code(config):
|
||||
if CONF_CERTIFICATE_AUTHORITY in config:
|
||||
cg.add(var.set_ca_certificate(config[CONF_CERTIFICATE_AUTHORITY]))
|
||||
cg.add(var.set_skip_cert_cn_check(config[CONF_SKIP_CERT_CN_CHECK]))
|
||||
if CONF_CLIENT_CERTIFICATE in config:
|
||||
cg.add(var.set_cl_certificate(config[CONF_CLIENT_CERTIFICATE]))
|
||||
cg.add(var.set_cl_key(config[CONF_CLIENT_CERTIFICATE_KEY]))
|
||||
|
||||
# prevent error -0x428e
|
||||
# See https://github.com/espressif/esp-idf/issues/139
|
||||
|
||||
@ -45,6 +45,11 @@ bool MQTTBackendESP32::initialize_() {
|
||||
mqtt_cfg_.cert_pem = ca_certificate_.value().c_str();
|
||||
mqtt_cfg_.skip_cert_common_name_check = skip_cert_cn_check_;
|
||||
mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_SSL;
|
||||
|
||||
if (this->cl_certificate_.has_value() && this->cl_key_.has_value()) {
|
||||
mqtt_cfg_.client_cert_pem = this->cl_certificate_.value().c_str();
|
||||
mqtt_cfg_.client_key_pem = this->cl_key_.value().c_str();
|
||||
}
|
||||
} else {
|
||||
mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_TCP;
|
||||
}
|
||||
@ -79,6 +84,11 @@ bool MQTTBackendESP32::initialize_() {
|
||||
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;
|
||||
|
||||
if (this->cl_certificate_.has_value() && this->cl_key_.has_value()) {
|
||||
mqtt_cfg_.credentials.authentication.certificate = this->cl_certificate_.value().c_str();
|
||||
mqtt_cfg_.credentials.authentication.key = this->cl_key_.value().c_str();
|
||||
}
|
||||
} else {
|
||||
mqtt_cfg_.broker.address.transport = MQTT_TRANSPORT_OVER_TCP;
|
||||
}
|
||||
|
||||
@ -124,6 +124,8 @@ class MQTTBackendESP32 final : public MQTTBackend {
|
||||
void loop() final;
|
||||
|
||||
void set_ca_certificate(const std::string &cert) { ca_certificate_ = cert; }
|
||||
void set_cl_certificate(const std::string &cert) { cl_certificate_ = cert; }
|
||||
void set_cl_key(const std::string &key) { cl_key_ = key; }
|
||||
void set_skip_cert_cn_check(bool skip_check) { skip_cert_cn_check_ = skip_check; }
|
||||
|
||||
protected:
|
||||
@ -154,6 +156,8 @@ class MQTTBackendESP32 final : public MQTTBackend {
|
||||
uint16_t keep_alive_;
|
||||
bool clean_session_;
|
||||
optional<std::string> ca_certificate_;
|
||||
optional<std::string> cl_certificate_;
|
||||
optional<std::string> cl_key_;
|
||||
bool skip_cert_cn_check_{false};
|
||||
|
||||
// callbacks
|
||||
|
||||
@ -25,7 +25,7 @@ void MQTTBinarySensorComponent::dump_config() {
|
||||
MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor)
|
||||
: binary_sensor_(binary_sensor) {
|
||||
if (this->binary_sensor_->is_status_binary_sensor()) {
|
||||
this->set_custom_state_topic(mqtt::global_mqtt_client->get_availability().topic);
|
||||
this->set_custom_state_topic(mqtt::global_mqtt_client->get_availability().topic.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -146,6 +146,8 @@ class MQTTClientComponent : public Component {
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); }
|
||||
void set_cl_certificate(const char *cert) { this->mqtt_backend_.set_cl_certificate(cert); }
|
||||
void set_cl_key(const char *key) { this->mqtt_backend_.set_cl_key(key); }
|
||||
void set_skip_cert_cn_check(bool skip_check) { this->mqtt_backend_.set_skip_cert_cn_check(skip_check); }
|
||||
#endif
|
||||
const Availability &get_availability();
|
||||
|
||||
@ -34,13 +34,13 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con
|
||||
|
||||
std::string MQTTComponent::get_state_topic_() const {
|
||||
if (this->has_custom_state_topic_)
|
||||
return this->custom_state_topic_;
|
||||
return this->custom_state_topic_.str();
|
||||
return this->get_default_topic_for_("state");
|
||||
}
|
||||
|
||||
std::string MQTTComponent::get_command_topic_() const {
|
||||
if (this->has_custom_command_topic_)
|
||||
return this->custom_command_topic_;
|
||||
return this->custom_command_topic_.str();
|
||||
return this->get_default_topic_for_("command");
|
||||
}
|
||||
|
||||
@ -180,12 +180,12 @@ MQTTComponent::MQTTComponent() = default;
|
||||
|
||||
float MQTTComponent::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
void MQTTComponent::disable_discovery() { this->discovery_enabled_ = false; }
|
||||
void MQTTComponent::set_custom_state_topic(const std::string &custom_state_topic) {
|
||||
this->custom_state_topic_ = custom_state_topic;
|
||||
void MQTTComponent::set_custom_state_topic(const char *custom_state_topic) {
|
||||
this->custom_state_topic_ = StringRef(custom_state_topic);
|
||||
this->has_custom_state_topic_ = true;
|
||||
}
|
||||
void MQTTComponent::set_custom_command_topic(const std::string &custom_command_topic) {
|
||||
this->custom_command_topic_ = custom_command_topic;
|
||||
void MQTTComponent::set_custom_command_topic(const char *custom_command_topic) {
|
||||
this->custom_command_topic_ = StringRef(custom_command_topic);
|
||||
this->has_custom_command_topic_ = true;
|
||||
}
|
||||
void MQTTComponent::set_command_retain(bool command_retain) { this->command_retain_ = command_retain; }
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "mqtt_client.h"
|
||||
|
||||
namespace esphome {
|
||||
@ -88,9 +89,9 @@ class MQTTComponent : public Component {
|
||||
virtual std::string component_type() const = 0;
|
||||
|
||||
/// Set a custom state topic. Set to "" for default behavior.
|
||||
void set_custom_state_topic(const std::string &custom_state_topic);
|
||||
void set_custom_state_topic(const char *custom_state_topic);
|
||||
/// Set a custom command topic. Set to "" for default behavior.
|
||||
void set_custom_command_topic(const std::string &custom_command_topic);
|
||||
void set_custom_command_topic(const char *custom_command_topic);
|
||||
/// Set whether command message should be retained.
|
||||
void set_command_retain(bool command_retain);
|
||||
|
||||
@ -188,15 +189,17 @@ class MQTTComponent : public Component {
|
||||
/// Generate the Home Assistant MQTT discovery object id by automatically transforming the friendly name.
|
||||
std::string get_default_object_id_() const;
|
||||
|
||||
std::string custom_state_topic_{};
|
||||
std::string custom_command_topic_{};
|
||||
StringRef custom_state_topic_{};
|
||||
StringRef custom_command_topic_{};
|
||||
|
||||
std::unique_ptr<Availability> availability_;
|
||||
|
||||
bool has_custom_state_topic_{false};
|
||||
bool has_custom_command_topic_{false};
|
||||
|
||||
bool command_retain_{false};
|
||||
bool retain_{true};
|
||||
bool discovery_enabled_{true};
|
||||
std::unique_ptr<Availability> availability_;
|
||||
bool resend_state_{false};
|
||||
};
|
||||
|
||||
|
||||
@ -14,6 +14,13 @@
|
||||
#include <IPAddress.h>
|
||||
#endif /* USE_ADRDUINO */
|
||||
|
||||
#ifdef USE_HOST
|
||||
#include <arpa/inet.h>
|
||||
using ip_addr_t = in_addr;
|
||||
using ip4_addr_t = in_addr;
|
||||
#define ipaddr_aton(x, y) inet_aton((x), (y))
|
||||
#endif
|
||||
|
||||
#if USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#define arduino_ns Arduino_h
|
||||
#elif USE_LIBRETINY
|
||||
@ -32,6 +39,14 @@ namespace network {
|
||||
|
||||
struct IPAddress {
|
||||
public:
|
||||
#ifdef USE_HOST
|
||||
IPAddress() { ip_addr_.s_addr = 0; }
|
||||
IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) {
|
||||
this->ip_addr_.s_addr = htonl((first << 24) | (second << 16) | (third << 8) | fourth);
|
||||
}
|
||||
IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); }
|
||||
IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; }
|
||||
#else
|
||||
IPAddress() { ip_addr_set_zero(&ip_addr_); }
|
||||
IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) {
|
||||
IP_ADDR4(&ip_addr_, first, second, third, fourth);
|
||||
@ -107,6 +122,7 @@ struct IPAddress {
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
ip_addr_t ip_addr_;
|
||||
|
||||
@ -33,14 +33,14 @@ CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start"
|
||||
|
||||
|
||||
def NextionName(value):
|
||||
valid_chars = f"{ascii_letters + digits}."
|
||||
valid_chars = f"{ascii_letters + digits + '_'}."
|
||||
if not isinstance(value, str) or len(value) > 29:
|
||||
raise cv.Invalid("Must be a string less than 29 characters")
|
||||
|
||||
for char in value:
|
||||
if char not in valid_chars:
|
||||
raise cv.Invalid(
|
||||
f"Must only consist of upper/lowercase characters, numbers and the period '.'. The character '{char}' cannot be used."
|
||||
f"Must only consist of upper/lowercase characters, numbers, the underscore '_', and the period '.'. The character '{char}' cannot be used."
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome.components import display, uart
|
||||
from esphome.components import esp32
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
@ -96,6 +97,11 @@ async def to_code(config):
|
||||
if CORE.is_esp32 and CORE.using_arduino:
|
||||
cg.add_library("WiFiClientSecure", None)
|
||||
cg.add_library("HTTPClient", None)
|
||||
elif CORE.is_esp32 and CORE.using_esp_idf:
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True)
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True
|
||||
)
|
||||
elif CORE.is_esp8266 and CORE.using_arduino:
|
||||
cg.add_library("ESP8266HTTPClient", None)
|
||||
|
||||
|
||||
@ -750,6 +750,50 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
*/
|
||||
void filled_circle(int center_x, int center_y, int radius, Color color);
|
||||
|
||||
/**
|
||||
* Draws a QR code in the screen
|
||||
* @param x1 The top left x coordinate to start the QR code.
|
||||
* @param y1 The top left y coordinate to start the QR code.
|
||||
* @param content The content of the QR code (as a plain text - Nextion will generate the QR code).
|
||||
* @param size The size (in pixels) for the QR code. Defaults to 200px.
|
||||
* @param background_color The background color to draw with (as rgb565 integer). Defaults to 65535 (white).
|
||||
* @param foreground_color The foreground color to draw with (as rgb565 integer). Defaults to 0 (black).
|
||||
* @param logo_pic The picture id for the logo in the center of the QR code. Defaults to -1 (no logo).
|
||||
* @param border_width The border width (in pixels) for the QR code. Defaults to 8px.
|
||||
*
|
||||
* Example:
|
||||
* ```cpp
|
||||
* it.qrcode(25, 25, "WIFI:S:MySSID;T:WPA;P:MyPassW0rd;;");
|
||||
* ```
|
||||
*
|
||||
* Draws a QR code with a Wi-Fi network credentials starting at the given coordinates (25,25).
|
||||
*/
|
||||
void qrcode(int x1, int y1, const char *content, int size = 200, uint16_t background_color = 65535,
|
||||
uint16_t foreground_color = 0, int logo_pic = -1, uint8_t border_width = 8);
|
||||
/**
|
||||
* Draws a QR code in the screen
|
||||
* @param x1 The top left x coordinate to start the QR code.
|
||||
* @param y1 The top left y coordinate to start the QR code.
|
||||
* @param content The content of the QR code (as a plain text - Nextion will generate the QR code).
|
||||
* @param size The size (in pixels) for the QR code. Defaults to 200px.
|
||||
* @param background_color The background color to draw with (as Color). Defaults to 65535 (white).
|
||||
* @param foreground_color The foreground color to draw with (as Color). Defaults to 0 (black).
|
||||
* @param logo_pic The picture id for the logo in the center of the QR code. Defaults to -1 (no logo).
|
||||
* @param border_width The border width (in pixels) for the QR code. Defaults to 8px.
|
||||
*
|
||||
* Example:
|
||||
* ```cpp
|
||||
* auto blue = Color(0, 0, 255);
|
||||
* auto red = Color(255, 0, 0);
|
||||
* it.qrcode(25, 25, "WIFI:S:MySSID;T:WPA;P:MyPassW0rd;;", 150, blue, red);
|
||||
* ```
|
||||
*
|
||||
* Draws a QR code with a Wi-Fi network credentials starting at the given coordinates (25,25) with size of 150px in
|
||||
* red on a blue background.
|
||||
*/
|
||||
void qrcode(int x1, int y1, const char *content, int size, Color background_color = Color(255, 255, 255),
|
||||
Color foreground_color = Color(0, 0, 0), int logo_pic = -1, uint8_t border_width = 8);
|
||||
|
||||
/** Set the brightness of the backlight.
|
||||
*
|
||||
* @param brightness The brightness percentage from 0 to 1.0.
|
||||
@ -960,6 +1004,21 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
this->exit_reparse_on_start_ = exit_reparse_on_start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Retrieves the number of commands pending in the Nextion command queue.
|
||||
*
|
||||
* This function returns the current count of commands that have been queued but not yet processed
|
||||
* for the Nextion display. The Nextion command queue is used to store commands that are sent to
|
||||
* the Nextion display for various operations like updating the display, changing interface elements,
|
||||
* or other interactive features. A larger queue size might indicate a higher processing time or potential
|
||||
* delays in command execution. This function is useful for monitoring the command flow and managing
|
||||
* the execution efficiency of the Nextion display interface.
|
||||
*
|
||||
* @return size_t The number of commands currently in the Nextion queue. This count includes all commands
|
||||
* that have been added to the queue and are awaiting processing.
|
||||
*/
|
||||
size_t queue_size() { return this->nextion_queue_.size(); }
|
||||
|
||||
protected:
|
||||
std::deque<NextionQueue *> nextion_queue_;
|
||||
std::deque<NextionQueue *> waveform_queue_;
|
||||
|
||||
@ -294,6 +294,19 @@ void Nextion::filled_circle(int center_x, int center_y, int radius, Color color)
|
||||
display::ColorUtil::color_to_565(color));
|
||||
}
|
||||
|
||||
void Nextion::qrcode(int x1, int y1, const char *content, int size, uint16_t background_color,
|
||||
uint16_t foreground_color, int logo_pic, uint8_t border_width) {
|
||||
this->add_no_result_to_queue_with_printf_("qrcode", "qrcode %d,%d,%d,%d,%d,%d,%d,\"%s\"", x1, y1, size,
|
||||
background_color, foreground_color, logo_pic, border_width, content);
|
||||
}
|
||||
|
||||
void Nextion::qrcode(int x1, int y1, const char *content, int size, Color background_color, Color foreground_color,
|
||||
int logo_pic, uint8_t border_width) {
|
||||
this->add_no_result_to_queue_with_printf_(
|
||||
"qrcode", "qrcode %d,%d,%d,%d,%d,%d,%d,\"%s\"", x1, y1, size, display::ColorUtil::color_to_565(background_color),
|
||||
display::ColorUtil::color_to_565(foreground_color), logo_pic, border_width, content);
|
||||
}
|
||||
|
||||
void Nextion::set_nextion_rtc_time(ESPTime time) {
|
||||
this->add_no_result_to_queue_with_printf_("rtc0", "rtc0=%u", time.year);
|
||||
this->add_no_result_to_queue_with_printf_("rtc1", "rtc1=%u", time.month);
|
||||
|
||||
@ -24,7 +24,7 @@ int Nextion::upload_range(const std::string &url, int range_start) {
|
||||
ESP_LOGVV(TAG, "url: %s", url.c_str());
|
||||
uint range_size = this->tft_size_ - range_start;
|
||||
ESP_LOGVV(TAG, "tft_size_: %i", this->tft_size_);
|
||||
ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size());
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
int range_end = (range_start == 0) ? std::min(this->tft_size_, 16383) : this->tft_size_;
|
||||
if (range_size <= 0 or range_end <= range_start) {
|
||||
ESP_LOGE(TAG, "Invalid range");
|
||||
@ -37,6 +37,8 @@ int Nextion::upload_range(const std::string &url, int range_start) {
|
||||
esp_http_client_config_t config = {
|
||||
.url = url.c_str(),
|
||||
.cert_pem = nullptr,
|
||||
.disable_auto_redirect = false,
|
||||
.max_redirection_count = 10,
|
||||
};
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
|
||||
@ -44,7 +46,7 @@ int Nextion::upload_range(const std::string &url, int range_start) {
|
||||
sprintf(range_header, "bytes=%d-%d", range_start, range_end);
|
||||
ESP_LOGV(TAG, "Requesting range: %s", range_header);
|
||||
esp_http_client_set_header(client, "Range", range_header);
|
||||
ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size());
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
|
||||
ESP_LOGV(TAG, "Opening http connetion");
|
||||
esp_err_t err;
|
||||
@ -65,18 +67,19 @@ int Nextion::upload_range(const std::string &url, int range_start) {
|
||||
|
||||
int total_read_len = 0, read_len;
|
||||
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
ESP_LOGV(TAG, "Allocate buffer");
|
||||
uint8_t *buffer = new uint8_t[4096];
|
||||
std::string recv_string;
|
||||
if (buffer == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for buffer");
|
||||
ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size());
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Memory for buffer allocated successfully");
|
||||
|
||||
while (true) {
|
||||
App.feed_wdt();
|
||||
ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size());
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
int read_len = esp_http_client_read(client, reinterpret_cast<char *>(buffer), 4096);
|
||||
ESP_LOGVV(TAG, "Read %d bytes from HTTP client, writing to UART", read_len);
|
||||
if (read_len > 0) {
|
||||
@ -84,15 +87,14 @@ int Nextion::upload_range(const std::string &url, int range_start) {
|
||||
ESP_LOGVV(TAG, "Write to UART successful");
|
||||
this->recv_ret_string_(recv_string, 5000, true);
|
||||
this->content_length_ -= read_len;
|
||||
ESP_LOGD(TAG, "Uploaded %0.2f %%, remaining %d bytes",
|
||||
100.0 * (this->tft_size_ - this->content_length_) / this->tft_size_, this->content_length_);
|
||||
if (recv_string[0] != 0x05) { // 0x05 == "ok"
|
||||
ESP_LOGD(TAG, "Uploaded %0.2f %%, remaining %d bytes, heap is %" PRIu32 " bytes",
|
||||
100.0 * (this->tft_size_ - this->content_length_) / this->tft_size_, this->content_length_,
|
||||
esp_get_free_heap_size());
|
||||
|
||||
if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request
|
||||
ESP_LOGD(
|
||||
TAG, "recv_string [%s]",
|
||||
format_hex_pretty(reinterpret_cast<const uint8_t *>(recv_string.data()), recv_string.size()).c_str());
|
||||
}
|
||||
// handle partial upload request
|
||||
if (recv_string[0] == 0x08 && recv_string.size() == 5) {
|
||||
uint32_t result = 0;
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
result += static_cast<uint8_t>(recv_string[j + 1]) << (8 * j);
|
||||
@ -101,13 +103,37 @@ int Nextion::upload_range(const std::string &url, int range_start) {
|
||||
ESP_LOGI(TAG, "Nextion reported new range %" PRIu32, result);
|
||||
this->content_length_ = this->tft_size_ - result;
|
||||
// Deallocate the buffer when done
|
||||
ESP_LOGV(TAG, "Deallocate buffer");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
delete[] buffer;
|
||||
ESP_LOGVV(TAG, "Memory for buffer deallocated");
|
||||
esp_http_client_cleanup(client);
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
ESP_LOGV(TAG, "Close http client");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
ESP_LOGVV(TAG, "Client closed");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
return result;
|
||||
}
|
||||
} else if (recv_string[0] != 0x05) { // 0x05 == "ok"
|
||||
ESP_LOGE(
|
||||
TAG, "Invalid response from Nextion: [%s]",
|
||||
format_hex_pretty(reinterpret_cast<const uint8_t *>(recv_string.data()), recv_string.size()).c_str());
|
||||
ESP_LOGV(TAG, "Deallocate buffer");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
delete[] buffer;
|
||||
ESP_LOGVV(TAG, "Memory for buffer deallocated");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
ESP_LOGV(TAG, "Close http client");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
ESP_LOGVV(TAG, "Client closed");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
recv_string.clear();
|
||||
} else if (read_len == 0) {
|
||||
ESP_LOGV(TAG, "End of HTTP response reached");
|
||||
@ -119,11 +145,18 @@ int Nextion::upload_range(const std::string &url, int range_start) {
|
||||
}
|
||||
|
||||
// Deallocate the buffer when done
|
||||
ESP_LOGV(TAG, "Deallocate buffer");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
delete[] buffer;
|
||||
ESP_LOGVV(TAG, "Memory for buffer deallocated");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
}
|
||||
esp_http_client_cleanup(client);
|
||||
ESP_LOGV(TAG, "Close http client");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
ESP_LOGVV(TAG, "Client closed");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
return range_end + 1;
|
||||
}
|
||||
|
||||
@ -145,17 +178,19 @@ bool Nextion::upload_tft() {
|
||||
|
||||
// Define the configuration for the HTTP client
|
||||
ESP_LOGV(TAG, "Establishing connection to HTTP server");
|
||||
ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size());
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
esp_http_client_config_t config = {
|
||||
.url = this->tft_url_.c_str(),
|
||||
.cert_pem = nullptr,
|
||||
.method = HTTP_METHOD_HEAD,
|
||||
.timeout_ms = 15000,
|
||||
.disable_auto_redirect = false,
|
||||
.max_redirection_count = 10,
|
||||
};
|
||||
|
||||
// Initialize the HTTP client with the configuration
|
||||
ESP_LOGV(TAG, "Initializing HTTP client");
|
||||
ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size());
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
esp_http_client_handle_t http = esp_http_client_init(&config);
|
||||
if (!http) {
|
||||
ESP_LOGE(TAG, "Failed to initialize HTTP client.");
|
||||
@ -164,7 +199,7 @@ bool Nextion::upload_tft() {
|
||||
|
||||
// Perform the HTTP request
|
||||
ESP_LOGV(TAG, "Check if the client could connect");
|
||||
ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size());
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
esp_err_t err = esp_http_client_perform(http);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err));
|
||||
@ -173,14 +208,22 @@ bool Nextion::upload_tft() {
|
||||
}
|
||||
|
||||
// Check the HTTP Status Code
|
||||
ESP_LOGV(TAG, "Check the HTTP Status Code");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
int status_code = esp_http_client_get_status_code(http);
|
||||
ESP_LOGV(TAG, "HTTP Status Code: %d", status_code);
|
||||
size_t tft_file_size = esp_http_client_get_content_length(http);
|
||||
ESP_LOGD(TAG, "TFT file size: %zu", tft_file_size);
|
||||
|
||||
ESP_LOGD(TAG, "Close HTTP connection");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
esp_http_client_close(http);
|
||||
esp_http_client_cleanup(http);
|
||||
ESP_LOGVV(TAG, "Connection closed");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
|
||||
if (tft_file_size < 4096) {
|
||||
ESP_LOGE(TAG, "File size check failed. Size: %zu", tft_file_size);
|
||||
esp_http_client_cleanup(http);
|
||||
return this->upload_end(false);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "File size check passed. Proceeding...");
|
||||
@ -189,8 +232,10 @@ bool Nextion::upload_tft() {
|
||||
this->tft_size_ = tft_file_size;
|
||||
|
||||
ESP_LOGD(TAG, "Updating Nextion");
|
||||
// The Nextion will ignore the update command if it is sleeping
|
||||
|
||||
// The Nextion will ignore the update command if it is sleeping
|
||||
ESP_LOGV(TAG, "Wake-up Nextion");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
this->send_command_("sleep=0");
|
||||
this->set_backlight_brightness(1.0);
|
||||
vTaskDelay(pdMS_TO_TICKS(250)); // NOLINT
|
||||
@ -203,26 +248,31 @@ bool Nextion::upload_tft() {
|
||||
sprintf(command, "whmi-wris %d,%" PRIu32 ",1", this->content_length_, this->parent_->get_baud_rate());
|
||||
|
||||
// Clear serial receive buffer
|
||||
ESP_LOGV(TAG, "Clear serial receive buffer");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
uint8_t d;
|
||||
while (this->available()) {
|
||||
this->read_byte(&d);
|
||||
};
|
||||
|
||||
ESP_LOGV(TAG, "Send update instruction: %s", command);
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
this->send_command_(command);
|
||||
|
||||
std::string response;
|
||||
ESP_LOGV(TAG, "Waiting for upgrade response");
|
||||
this->recv_ret_string_(response, 2048, true); // This can take some time to return
|
||||
this->recv_ret_string_(response, 5000, true); // This can take some time to return
|
||||
|
||||
// The Nextion display will, if it's ready to accept data, send a 0x05 byte.
|
||||
ESP_LOGD(TAG, "Upgrade response is [%s]",
|
||||
format_hex_pretty(reinterpret_cast<const uint8_t *>(response.data()), response.size()).c_str());
|
||||
ESP_LOGD(TAG, "Upgrade response is [%s] - %zu bytes",
|
||||
format_hex_pretty(reinterpret_cast<const uint8_t *>(response.data()), response.size()).c_str(),
|
||||
response.length());
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
|
||||
if (response.find(0x05) != std::string::npos) {
|
||||
ESP_LOGV(TAG, "Preparation for tft update done");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Preparation for tft update failed %d \"%s\"", response[0], response.c_str());
|
||||
esp_http_client_cleanup(http);
|
||||
return this->upload_end(false);
|
||||
}
|
||||
|
||||
@ -230,12 +280,12 @@ bool Nextion::upload_tft() {
|
||||
content_length_, esp_get_free_heap_size());
|
||||
|
||||
ESP_LOGV(TAG, "Starting transfer by chunks loop");
|
||||
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
|
||||
int result = 0;
|
||||
while (content_length_ > 0) {
|
||||
result = upload_range(this->tft_url_.c_str(), result);
|
||||
if (result < 0) {
|
||||
ESP_LOGE(TAG, "Error updating Nextion!");
|
||||
esp_http_client_cleanup(http);
|
||||
return this->upload_end(false);
|
||||
}
|
||||
App.feed_wdt();
|
||||
@ -244,9 +294,6 @@ bool Nextion::upload_tft() {
|
||||
|
||||
ESP_LOGD(TAG, "Successfully updated Nextion!");
|
||||
|
||||
ESP_LOGD(TAG, "Close HTTP connection");
|
||||
esp_http_client_close(http);
|
||||
esp_http_client_cleanup(http);
|
||||
return upload_end(true);
|
||||
}
|
||||
|
||||
@ -256,7 +303,7 @@ bool Nextion::upload_end(bool successful) {
|
||||
this->soft_reset();
|
||||
vTaskDelay(pdMS_TO_TICKS(1500)); // NOLINT
|
||||
if (successful) {
|
||||
ESP_LOGD(TAG, "Restarting esphome");
|
||||
ESP_LOGD(TAG, "Restarting ESPHome");
|
||||
esp_restart(); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
return successful;
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
CODEOWNERS = ["@jesserockz", "@kbx81"]
|
||||
|
||||
nfc_ns = cg.esphome_ns.namespace("nfc")
|
||||
|
||||
Nfcc = nfc_ns.class_("Nfcc")
|
||||
NfcTag = nfc_ns.class_("NfcTag")
|
||||
|
||||
NfcTagListener = nfc_ns.class_("NfcTagListener")
|
||||
NfcOnTagTrigger = nfc_ns.class_(
|
||||
"NfcOnTagTrigger", automation.Trigger.template(cg.std_string, NfcTag)
|
||||
)
|
||||
|
||||
72
esphome/components/nfc/binary_sensor/__init__.py
Normal file
72
esphome/components/nfc/binary_sensor/__init__.py
Normal file
@ -0,0 +1,72 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import binary_sensor
|
||||
from esphome.const import CONF_UID
|
||||
from esphome.core import HexInt
|
||||
from .. import nfc_ns, Nfcc, NfcTagListener
|
||||
|
||||
DEPENDENCIES = ["nfc"]
|
||||
|
||||
CONF_NDEF_CONTAINS = "ndef_contains"
|
||||
CONF_NFCC_ID = "nfcc_id"
|
||||
CONF_TAG_ID = "tag_id"
|
||||
|
||||
NfcTagBinarySensor = nfc_ns.class_(
|
||||
"NfcTagBinarySensor",
|
||||
binary_sensor.BinarySensor,
|
||||
cg.Component,
|
||||
NfcTagListener,
|
||||
cg.Parented.template(Nfcc),
|
||||
)
|
||||
|
||||
|
||||
def validate_uid(value):
|
||||
value = cv.string_strict(value)
|
||||
for x in value.split("-"):
|
||||
if len(x) != 2:
|
||||
raise cv.Invalid(
|
||||
"Each part (separated by '-') of the UID must be two characters "
|
||||
"long."
|
||||
)
|
||||
try:
|
||||
x = int(x, 16)
|
||||
except ValueError as err:
|
||||
raise cv.Invalid(
|
||||
"Valid characters for parts of a UID are 0123456789ABCDEF."
|
||||
) from err
|
||||
if x < 0 or x > 255:
|
||||
raise cv.Invalid(
|
||||
"Valid values for UID parts (separated by '-') are 00 to FF"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
binary_sensor.binary_sensor_schema(NfcTagBinarySensor)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_NFCC_ID): cv.use_id(Nfcc),
|
||||
cv.Optional(CONF_NDEF_CONTAINS): cv.string,
|
||||
cv.Optional(CONF_TAG_ID): cv.string,
|
||||
cv.Optional(CONF_UID): validate_uid,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_exactly_one_key(CONF_NDEF_CONTAINS, CONF_TAG_ID, CONF_UID),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_NFCC_ID])
|
||||
|
||||
hub = await cg.get_variable(config[CONF_NFCC_ID])
|
||||
cg.add(hub.register_listener(var))
|
||||
if CONF_NDEF_CONTAINS in config:
|
||||
cg.add(var.set_ndef_match_string(config[CONF_NDEF_CONTAINS]))
|
||||
if CONF_TAG_ID in config:
|
||||
cg.add(var.set_tag_name(config[CONF_TAG_ID]))
|
||||
elif CONF_UID in config:
|
||||
addr = [HexInt(int(x, 16)) for x in config[CONF_UID].split("-")]
|
||||
cg.add(var.set_uid(addr))
|
||||
114
esphome/components/nfc/binary_sensor/binary_sensor.cpp
Normal file
114
esphome/components/nfc/binary_sensor/binary_sensor.cpp
Normal file
@ -0,0 +1,114 @@
|
||||
#include "binary_sensor.h"
|
||||
#include "../nfc_helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace nfc {
|
||||
|
||||
static const char *const TAG = "nfc.binary_sensor";
|
||||
|
||||
void NfcTagBinarySensor::setup() {
|
||||
this->parent_->register_listener(this);
|
||||
this->publish_initial_state(false);
|
||||
}
|
||||
|
||||
void NfcTagBinarySensor::dump_config() {
|
||||
std::string match_str = "name";
|
||||
|
||||
LOG_BINARY_SENSOR("", "NFC Tag Binary Sensor", this);
|
||||
if (!this->match_string_.empty()) {
|
||||
if (!this->match_tag_name_) {
|
||||
match_str = "contains";
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Tag %s: %s", match_str.c_str(), this->match_string_.c_str());
|
||||
return;
|
||||
}
|
||||
if (!this->uid_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " Tag UID: %s", format_bytes(this->uid_).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void NfcTagBinarySensor::set_ndef_match_string(const std::string &str) {
|
||||
this->match_string_ = str;
|
||||
this->match_tag_name_ = false;
|
||||
}
|
||||
|
||||
void NfcTagBinarySensor::set_tag_name(const std::string &str) {
|
||||
this->match_string_ = str;
|
||||
this->match_tag_name_ = true;
|
||||
}
|
||||
|
||||
void NfcTagBinarySensor::set_uid(const std::vector<uint8_t> &uid) { this->uid_ = uid; }
|
||||
|
||||
bool NfcTagBinarySensor::tag_match_ndef_string(const std::shared_ptr<NdefMessage> &msg) {
|
||||
for (const auto &record : msg->get_records()) {
|
||||
if (record->get_payload().find(this->match_string_) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool NfcTagBinarySensor::tag_match_tag_name(const std::shared_ptr<NdefMessage> &msg) {
|
||||
for (const auto &record : msg->get_records()) {
|
||||
if (record->get_payload().find(HA_TAG_ID_PREFIX) != std::string::npos) {
|
||||
auto rec_substr = record->get_payload().substr(sizeof(HA_TAG_ID_PREFIX) - 1);
|
||||
if (rec_substr.find(this->match_string_) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool NfcTagBinarySensor::tag_match_uid(const std::vector<uint8_t> &data) {
|
||||
if (data.size() != this->uid_.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < data.size(); i++) {
|
||||
if (data[i] != this->uid_[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void NfcTagBinarySensor::tag_off(NfcTag &tag) {
|
||||
if (!this->match_string_.empty() && tag.has_ndef_message()) {
|
||||
if (this->match_tag_name_) {
|
||||
if (this->tag_match_tag_name(tag.get_ndef_message())) {
|
||||
this->publish_state(false);
|
||||
}
|
||||
} else {
|
||||
if (this->tag_match_ndef_string(tag.get_ndef_message())) {
|
||||
this->publish_state(false);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this->uid_.empty() && this->tag_match_uid(tag.get_uid())) {
|
||||
this->publish_state(false);
|
||||
}
|
||||
}
|
||||
|
||||
void NfcTagBinarySensor::tag_on(NfcTag &tag) {
|
||||
if (!this->match_string_.empty() && tag.has_ndef_message()) {
|
||||
if (this->match_tag_name_) {
|
||||
if (this->tag_match_tag_name(tag.get_ndef_message())) {
|
||||
this->publish_state(true);
|
||||
}
|
||||
} else {
|
||||
if (this->tag_match_ndef_string(tag.get_ndef_message())) {
|
||||
this->publish_state(true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this->uid_.empty() && this->tag_match_uid(tag.get_uid())) {
|
||||
this->publish_state(true);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace nfc
|
||||
} // namespace esphome
|
||||
38
esphome/components/nfc/binary_sensor/binary_sensor.h
Normal file
38
esphome/components/nfc/binary_sensor/binary_sensor.h
Normal file
@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/nfc/nfc.h"
|
||||
#include "esphome/components/nfc/nfc_tag.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace nfc {
|
||||
|
||||
class NfcTagBinarySensor : public binary_sensor::BinarySensor,
|
||||
public Component,
|
||||
public NfcTagListener,
|
||||
public Parented<Nfcc> {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_ndef_match_string(const std::string &str);
|
||||
void set_tag_name(const std::string &str);
|
||||
void set_uid(const std::vector<uint8_t> &uid);
|
||||
|
||||
bool tag_match_ndef_string(const std::shared_ptr<NdefMessage> &msg);
|
||||
bool tag_match_tag_name(const std::shared_ptr<NdefMessage> &msg);
|
||||
bool tag_match_uid(const std::vector<uint8_t> &data);
|
||||
|
||||
void tag_off(NfcTag &tag) override;
|
||||
void tag_on(NfcTag &tag) override;
|
||||
|
||||
protected:
|
||||
bool match_tag_name_{false};
|
||||
std::string match_string_;
|
||||
std::vector<uint8_t> uid_;
|
||||
};
|
||||
|
||||
} // namespace nfc
|
||||
} // namespace esphome
|
||||
@ -66,5 +66,19 @@ bool mifare_classic_is_trailer_block(uint8_t block_num);
|
||||
|
||||
uint32_t get_mifare_ultralight_buffer_size(uint32_t message_length);
|
||||
|
||||
class NfcTagListener {
|
||||
public:
|
||||
virtual void tag_off(NfcTag &tag) {}
|
||||
virtual void tag_on(NfcTag &tag) {}
|
||||
};
|
||||
|
||||
class Nfcc {
|
||||
public:
|
||||
void register_listener(NfcTagListener *listener) { this->tag_listeners_.push_back(listener); }
|
||||
|
||||
protected:
|
||||
std::vector<NfcTagListener *> tag_listeners_;
|
||||
};
|
||||
|
||||
} // namespace nfc
|
||||
} // namespace esphome
|
||||
|
||||
@ -62,6 +62,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
DEVICE_CLASS_VOLUME,
|
||||
DEVICE_CLASS_VOLUME_FLOW_RATE,
|
||||
DEVICE_CLASS_VOLUME_STORAGE,
|
||||
DEVICE_CLASS_WATER,
|
||||
DEVICE_CLASS_WEIGHT,
|
||||
@ -117,6 +118,7 @@ DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
DEVICE_CLASS_VOLUME,
|
||||
DEVICE_CLASS_VOLUME_FLOW_RATE,
|
||||
DEVICE_CLASS_VOLUME_STORAGE,
|
||||
DEVICE_CLASS_WATER,
|
||||
DEVICE_CLASS_WEIGHT,
|
||||
|
||||
@ -12,6 +12,7 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_OTA,
|
||||
KEY_PAST_SAFE_MODE,
|
||||
CONF_VERSION,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
@ -41,6 +42,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(OTAComponent),
|
||||
cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean,
|
||||
cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True),
|
||||
cv.SplitDefault(
|
||||
CONF_PORT,
|
||||
esp8266=8266,
|
||||
@ -93,6 +95,7 @@ async def to_code(config):
|
||||
if CONF_PASSWORD in config:
|
||||
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
|
||||
cg.add_define("USE_OTA_PASSWORD")
|
||||
cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
|
||||
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@ -20,8 +20,7 @@ namespace esphome {
|
||||
namespace ota {
|
||||
|
||||
static const char *const TAG = "ota";
|
||||
|
||||
static const uint8_t OTA_VERSION_1_0 = 1;
|
||||
static constexpr u_int16_t OTA_BLOCK_SIZE = 8192;
|
||||
|
||||
OTAComponent *global_ota_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@ -101,6 +100,7 @@ void OTAComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Using Password.");
|
||||
}
|
||||
#endif
|
||||
ESP_LOGCONFIG(TAG, " OTA version: %d.", USE_OTA_VERSION);
|
||||
if (this->has_safe_mode_ && this->safe_mode_rtc_value_ > 1 &&
|
||||
this->safe_mode_rtc_value_ != esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC) {
|
||||
ESP_LOGW(TAG, "Last Boot was an unhandled reset, will proceed to safe mode in %" PRIu32 " restarts",
|
||||
@ -132,6 +132,9 @@ void OTAComponent::handle_() {
|
||||
uint8_t ota_features;
|
||||
std::unique_ptr<OTABackend> backend;
|
||||
(void) ota_features;
|
||||
#if USE_OTA_VERSION == 2
|
||||
size_t size_acknowledged = 0;
|
||||
#endif
|
||||
|
||||
if (client_ == nullptr) {
|
||||
struct sockaddr_storage source_addr;
|
||||
@ -168,7 +171,7 @@ void OTAComponent::handle_() {
|
||||
|
||||
// Send OK and version - 2 bytes
|
||||
buf[0] = OTA_RESPONSE_OK;
|
||||
buf[1] = OTA_VERSION_1_0;
|
||||
buf[1] = USE_OTA_VERSION;
|
||||
this->writeall_(buf, 2);
|
||||
|
||||
backend = make_ota_backend();
|
||||
@ -312,6 +315,13 @@ void OTAComponent::handle_() {
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
total += read;
|
||||
#if USE_OTA_VERSION == 2
|
||||
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
|
||||
buf[0] = OTA_RESPONSE_CHUNK_OK;
|
||||
this->writeall_(buf, 1);
|
||||
size_acknowledged += OTA_BLOCK_SIZE;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint32_t now = millis();
|
||||
if (now - last_progress > 1000) {
|
||||
|
||||
@ -10,31 +10,32 @@ namespace esphome {
|
||||
namespace ota {
|
||||
|
||||
enum OTAResponseTypes {
|
||||
OTA_RESPONSE_OK = 0,
|
||||
OTA_RESPONSE_REQUEST_AUTH = 1,
|
||||
OTA_RESPONSE_OK = 0x00,
|
||||
OTA_RESPONSE_REQUEST_AUTH = 0x01,
|
||||
|
||||
OTA_RESPONSE_HEADER_OK = 64,
|
||||
OTA_RESPONSE_AUTH_OK = 65,
|
||||
OTA_RESPONSE_UPDATE_PREPARE_OK = 66,
|
||||
OTA_RESPONSE_BIN_MD5_OK = 67,
|
||||
OTA_RESPONSE_RECEIVE_OK = 68,
|
||||
OTA_RESPONSE_UPDATE_END_OK = 69,
|
||||
OTA_RESPONSE_SUPPORTS_COMPRESSION = 70,
|
||||
OTA_RESPONSE_HEADER_OK = 0x40,
|
||||
OTA_RESPONSE_AUTH_OK = 0x41,
|
||||
OTA_RESPONSE_UPDATE_PREPARE_OK = 0x42,
|
||||
OTA_RESPONSE_BIN_MD5_OK = 0x43,
|
||||
OTA_RESPONSE_RECEIVE_OK = 0x44,
|
||||
OTA_RESPONSE_UPDATE_END_OK = 0x45,
|
||||
OTA_RESPONSE_SUPPORTS_COMPRESSION = 0x46,
|
||||
OTA_RESPONSE_CHUNK_OK = 0x47,
|
||||
|
||||
OTA_RESPONSE_ERROR_MAGIC = 128,
|
||||
OTA_RESPONSE_ERROR_UPDATE_PREPARE = 129,
|
||||
OTA_RESPONSE_ERROR_AUTH_INVALID = 130,
|
||||
OTA_RESPONSE_ERROR_WRITING_FLASH = 131,
|
||||
OTA_RESPONSE_ERROR_UPDATE_END = 132,
|
||||
OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 133,
|
||||
OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 134,
|
||||
OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 135,
|
||||
OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 136,
|
||||
OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 137,
|
||||
OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 138,
|
||||
OTA_RESPONSE_ERROR_MD5_MISMATCH = 139,
|
||||
OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 140,
|
||||
OTA_RESPONSE_ERROR_UNKNOWN = 255,
|
||||
OTA_RESPONSE_ERROR_MAGIC = 0x80,
|
||||
OTA_RESPONSE_ERROR_UPDATE_PREPARE = 0x81,
|
||||
OTA_RESPONSE_ERROR_AUTH_INVALID = 0x82,
|
||||
OTA_RESPONSE_ERROR_WRITING_FLASH = 0x83,
|
||||
OTA_RESPONSE_ERROR_UPDATE_END = 0x84,
|
||||
OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 0x85,
|
||||
OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 0x86,
|
||||
OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 0x87,
|
||||
OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 0x88,
|
||||
OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 0x89,
|
||||
OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A,
|
||||
OTA_RESPONSE_ERROR_MD5_MISMATCH = 0x8B,
|
||||
OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C,
|
||||
OTA_RESPONSE_ERROR_UNKNOWN = 0xFF,
|
||||
};
|
||||
|
||||
enum OTAState { OTA_COMPLETED = 0, OTA_STARTED, OTA_IN_PROGRESS, OTA_ERROR };
|
||||
|
||||
@ -92,66 +92,78 @@ CONFIG_SCHEMA = (
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_PM1,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_2_5_STD): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_10_0_STD): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_PM10,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_1_0): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_PM1,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_PM10,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_0_3UM): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNT_DECILITRE,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_0_5UM): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNT_DECILITRE,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_1_0UM): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNT_DECILITRE,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_2_5UM): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNT_DECILITRE,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_5_0UM): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNT_DECILITRE,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_10_0UM): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNT_DECILITRE,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
|
||||
@ -34,7 +34,7 @@ CONF_TAG_TTL = "tag_ttl"
|
||||
CONF_VEN_PIN = "ven_pin"
|
||||
|
||||
pn7150_ns = cg.esphome_ns.namespace("pn7150")
|
||||
PN7150 = pn7150_ns.class_("PN7150", cg.Component)
|
||||
PN7150 = pn7150_ns.class_("PN7150", nfc.Nfcc, cg.Component)
|
||||
|
||||
EmulationOffAction = pn7150_ns.class_("EmulationOffAction", automation.Action)
|
||||
EmulationOnAction = pn7150_ns.class_("EmulationOnAction", automation.Action)
|
||||
|
||||
@ -566,6 +566,9 @@ void PN7150::erase_tag_(const uint8_t tag_index) {
|
||||
for (auto *trigger : this->triggers_ontagremoved_) {
|
||||
trigger->process(this->discovered_endpoint_[tag_index].tag);
|
||||
}
|
||||
for (auto *listener : this->tag_listeners_) {
|
||||
listener->tag_off(*this->discovered_endpoint_[tag_index].tag);
|
||||
}
|
||||
ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str());
|
||||
this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index);
|
||||
}
|
||||
@ -881,6 +884,9 @@ void PN7150::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoi
|
||||
for (auto *trigger : this->triggers_ontag_) {
|
||||
trigger->process(working_endpoint.tag);
|
||||
}
|
||||
for (auto *listener : this->tag_listeners_) {
|
||||
listener->tag_on(*working_endpoint.tag);
|
||||
}
|
||||
working_endpoint.trig_called = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -142,7 +142,7 @@ struct DiscoveredEndpoint {
|
||||
bool trig_called;
|
||||
};
|
||||
|
||||
class PN7150 : public Component {
|
||||
class PN7150 : public nfc::Nfcc, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
@ -36,7 +36,7 @@ CONF_VEN_PIN = "ven_pin"
|
||||
CONF_WKUP_REQ_PIN = "wkup_req_pin"
|
||||
|
||||
pn7160_ns = cg.esphome_ns.namespace("pn7160")
|
||||
PN7160 = pn7160_ns.class_("PN7160", cg.Component)
|
||||
PN7160 = pn7160_ns.class_("PN7160", nfc.Nfcc, cg.Component)
|
||||
|
||||
EmulationOffAction = pn7160_ns.class_("EmulationOffAction", automation.Action)
|
||||
EmulationOnAction = pn7160_ns.class_("EmulationOnAction", automation.Action)
|
||||
|
||||
@ -591,6 +591,9 @@ void PN7160::erase_tag_(const uint8_t tag_index) {
|
||||
for (auto *trigger : this->triggers_ontagremoved_) {
|
||||
trigger->process(this->discovered_endpoint_[tag_index].tag);
|
||||
}
|
||||
for (auto *listener : this->tag_listeners_) {
|
||||
listener->tag_off(*this->discovered_endpoint_[tag_index].tag);
|
||||
}
|
||||
ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str());
|
||||
this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index);
|
||||
}
|
||||
@ -905,6 +908,9 @@ void PN7160::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoi
|
||||
for (auto *trigger : this->triggers_ontag_) {
|
||||
trigger->process(working_endpoint.tag);
|
||||
}
|
||||
for (auto *listener : this->tag_listeners_) {
|
||||
listener->tag_on(*working_endpoint.tag);
|
||||
}
|
||||
working_endpoint.trig_called = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -157,7 +157,7 @@ struct DiscoveredEndpoint {
|
||||
bool trig_called;
|
||||
};
|
||||
|
||||
class PN7160 : public Component {
|
||||
class PN7160 : public nfc::Nfcc, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
@ -59,14 +59,14 @@ TYPES: dict[str, cv.Schema] = {
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
CONF_VOLTAGE_LOW: sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
),
|
||||
CONF_VOLTAGE_HIGH: sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
),
|
||||
CONF_COULOMB: sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
|
||||
@ -633,6 +633,62 @@ async def magiquest_action(var, config, args):
|
||||
cg.add(var.set_magnitude(template_))
|
||||
|
||||
|
||||
# Microchip HCS301 KeeLoq OOK
|
||||
(
|
||||
KeeloqData,
|
||||
KeeloqBinarySensor,
|
||||
KeeloqTrigger,
|
||||
KeeloqAction,
|
||||
KeeloqDumper,
|
||||
) = declare_protocol("Keeloq")
|
||||
KEELOQ_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ADDRESS): cv.All(cv.hex_int, cv.Range(min=0, max=0xFFFFFFF)),
|
||||
cv.Required(CONF_CODE): cv.All(cv.hex_int, cv.Range(min=0, max=0xFFFFFFFF)),
|
||||
cv.Optional(CONF_COMMAND, default=0x10): cv.All(
|
||||
cv.hex_int,
|
||||
cv.Range(min=0, max=0x10),
|
||||
),
|
||||
cv.Optional(CONF_LEVEL, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@register_binary_sensor("keeloq", KeeloqBinarySensor, KEELOQ_SCHEMA)
|
||||
def Keeloq_binary_sensor(var, config):
|
||||
cg.add(
|
||||
var.set_data(
|
||||
cg.StructInitializer(
|
||||
KeeloqData,
|
||||
("address", config[CONF_ADDRESS]),
|
||||
("command", config[CONF_COMMAND]),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@register_trigger("keeloq", KeeloqTrigger, KeeloqData)
|
||||
def keeloq_trigger(var, config):
|
||||
pass
|
||||
|
||||
|
||||
@register_dumper("keeloq", KeeloqDumper)
|
||||
def keeloq_dumper(var, config):
|
||||
pass
|
||||
|
||||
|
||||
@register_action("keeloq", KeeloqAction, KEELOQ_SCHEMA)
|
||||
async def keeloq_action(var, config, args):
|
||||
template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint32)
|
||||
cg.add(var.set_address(template_))
|
||||
template_ = await cg.templatable(config[CONF_CODE], args, cg.uint32)
|
||||
cg.add(var.set_encrypted(template_))
|
||||
template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8)
|
||||
cg.add(var.set_command(template_))
|
||||
template_ = await cg.templatable(config[CONF_LEVEL], args, bool)
|
||||
cg.add(var.set_vlow(template_))
|
||||
|
||||
|
||||
# NEC
|
||||
NECData, NECBinarySensor, NECTrigger, NECAction, NECDumper = declare_protocol("NEC")
|
||||
NEC_SCHEMA = cv.Schema(
|
||||
|
||||
@ -13,7 +13,8 @@ static const uint8_t NBITS_SYNC = 4;
|
||||
static const uint8_t NBITS_ADDRESS = 16;
|
||||
static const uint8_t NBITS_CHANNEL = 5;
|
||||
static const uint8_t NBITS_COMMAND = 7;
|
||||
static const uint8_t NBITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND;
|
||||
static const uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND;
|
||||
static const uint8_t MIN_RX_SRC = (NDATABITS * 2 + NBITS_SYNC / 2);
|
||||
|
||||
static const uint8_t CMD_ON = 0x41;
|
||||
static const uint8_t CMD_OFF = 0x02;
|
||||
@ -116,7 +117,7 @@ void DraytonProtocol::encode(RemoteTransmitData *dst, const DraytonData &data) {
|
||||
|
||||
ESP_LOGV(TAG, "Send Drayton: out_data %08" PRIx32, out_data);
|
||||
|
||||
for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) {
|
||||
for (uint32_t mask = 1UL << (NDATABITS - 1); mask != 0; mask >>= 1) {
|
||||
if (out_data & mask) {
|
||||
dst->mark(BIT_TIME_US);
|
||||
dst->space(BIT_TIME_US);
|
||||
@ -134,79 +135,96 @@ optional<DraytonData> DraytonProtocol::decode(RemoteReceiveData src) {
|
||||
.command = 0,
|
||||
};
|
||||
|
||||
if (src.size() < 45) {
|
||||
return {};
|
||||
}
|
||||
while (src.size() - src.get_index() > MIN_RX_SRC) {
|
||||
ESP_LOGVV(TAG,
|
||||
"Decode Drayton: %" PRId32 ", %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32
|
||||
" %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32
|
||||
" %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 "",
|
||||
src.size() - src.get_index(), src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4),
|
||||
src.peek(5), src.peek(6), src.peek(7), src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12),
|
||||
src.peek(13), src.peek(14), src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19));
|
||||
|
||||
ESP_LOGVV(TAG,
|
||||
"Decode Drayton: %" PRId32 ", %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32
|
||||
" %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32
|
||||
" %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 "",
|
||||
src.size(), src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4), src.peek(5), src.peek(6),
|
||||
src.peek(7), src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12), src.peek(13), src.peek(14),
|
||||
src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19));
|
||||
// If first preamble item is a space, skip it
|
||||
if (src.peek_space_at_least(1)) {
|
||||
src.advance(1);
|
||||
}
|
||||
|
||||
// If first preamble item is a space, skip it
|
||||
if (src.peek_space_at_least(1)) {
|
||||
src.advance(1);
|
||||
}
|
||||
// Look for sync pulse, after. If sucessful index points to space of sync symbol
|
||||
while (src.size() - src.get_index() >= NDATABITS) {
|
||||
ESP_LOGVV(TAG, "Decode Drayton: sync search %d, %" PRId32 " %" PRId32, src.size() - src.get_index(), src.peek(),
|
||||
src.peek(1));
|
||||
if (src.peek_mark(2 * BIT_TIME_US) &&
|
||||
(src.peek_space(2 * BIT_TIME_US, 1) || src.peek_space(3 * BIT_TIME_US, 1))) {
|
||||
src.advance(1);
|
||||
ESP_LOGVV(TAG, "Decode Drayton: Found SYNC, - %d", src.get_index());
|
||||
break;
|
||||
} else {
|
||||
src.advance(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Look for sync pulse, after. If sucessful index points to space of sync symbol
|
||||
for (uint16_t preamble = 0; preamble <= NBITS_PREAMBLE * 2; preamble += 2) {
|
||||
ESP_LOGVV(TAG, "Decode Drayton: preamble %d %" PRId32 " %" PRId32, preamble, src.peek(preamble),
|
||||
src.peek(preamble + 1));
|
||||
if (src.peek_mark(2 * BIT_TIME_US, preamble) &&
|
||||
(src.peek_space(2 * BIT_TIME_US, preamble + 1) || src.peek_space(3 * BIT_TIME_US, preamble + 1))) {
|
||||
src.advance(preamble + 1);
|
||||
// No point continuing if not enough samples remaining to complete a packet
|
||||
if (src.size() - src.get_index() < NDATABITS) {
|
||||
ESP_LOGV(TAG, "Decode Drayton: Fail 1, - %" PRIu32, src.get_index());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Read data. Index points to space of sync symbol
|
||||
// Extract first bit
|
||||
// Checks next bit to leave index pointing correctly
|
||||
uint32_t out_data = 0;
|
||||
uint8_t bit = NBITS_ADDRESS + NBITS_COMMAND + NBITS_CHANNEL - 1;
|
||||
if (src.expect_space(3 * BIT_TIME_US) && (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) {
|
||||
out_data |= 0 << bit;
|
||||
} else if (src.expect_space(2 * BIT_TIME_US) && src.expect_mark(BIT_TIME_US) &&
|
||||
(src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) {
|
||||
out_data |= 1 << bit;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Decode Drayton: Fail 1, - %" PRIu32, src.get_index());
|
||||
return {};
|
||||
}
|
||||
|
||||
// Before/after each bit is read the index points to the transition at the start of the bit period or,
|
||||
// if there is no transition at the start of the bit period, then the transition in the middle of
|
||||
// the previous bit period.
|
||||
while (--bit >= 1) {
|
||||
ESP_LOGVV(TAG, "Decode Drayton: Data, %2d %08" PRIx32, bit, out_data);
|
||||
if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) &&
|
||||
(src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) {
|
||||
// Read data. Index points to space of sync symbol
|
||||
// Extract first bit
|
||||
// Checks next bit to leave index pointing correctly
|
||||
uint32_t out_data = 0;
|
||||
uint8_t bit = NDATABITS - 1;
|
||||
ESP_LOGVV(TAG, "Decode Drayton: first bit %d %" PRId32 ", %" PRId32, src.peek(0), src.peek(1), src.peek(2));
|
||||
if (src.expect_space(3 * BIT_TIME_US) && (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) {
|
||||
out_data |= 0 << bit;
|
||||
} else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) &&
|
||||
} else if (src.expect_space(2 * BIT_TIME_US) && src.expect_mark(BIT_TIME_US) &&
|
||||
(src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) {
|
||||
out_data |= 1 << bit;
|
||||
} else {
|
||||
ESP_LOGVV(TAG, "Decode Drayton: Fail 2, %2d %08" PRIx32, bit, out_data);
|
||||
return {};
|
||||
ESP_LOGV(TAG, "Decode Drayton: Fail 2, - %d %d %d", src.peek(-1), src.peek(0), src.peek(1));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) {
|
||||
out_data |= 0;
|
||||
} else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) {
|
||||
out_data |= 1;
|
||||
}
|
||||
ESP_LOGV(TAG, "Decode Drayton: Data, %2d %08" PRIx32, bit, out_data);
|
||||
|
||||
out.channel = (uint8_t) (out_data & 0x1F);
|
||||
out_data >>= NBITS_CHANNEL;
|
||||
out.command = (uint8_t) (out_data & 0x7F);
|
||||
out_data >>= NBITS_COMMAND;
|
||||
out.address = (uint16_t) (out_data & 0xFFFF);
|
||||
// Before/after each bit is read the index points to the transition at the start of the bit period or,
|
||||
// if there is no transition at the start of the bit period, then the transition in the middle of
|
||||
// the previous bit period.
|
||||
while (--bit >= 1) {
|
||||
ESP_LOGVV(TAG, "Decode Drayton: Data, %2d %08" PRIx32, bit, out_data);
|
||||
if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) &&
|
||||
(src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) {
|
||||
out_data |= 0 << bit;
|
||||
} else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) &&
|
||||
(src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) {
|
||||
out_data |= 1 << bit;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
if (bit > 0) {
|
||||
ESP_LOGVV(TAG, "Decode Drayton: Fail 3, %d %" PRId32 " %" PRId32, src.peek(-1), src.peek(0), src.peek(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) {
|
||||
out_data |= 0;
|
||||
} else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) {
|
||||
out_data |= 1;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Decode Drayton: Data, %2d %08x", bit, out_data);
|
||||
|
||||
out.channel = (uint8_t) (out_data & 0x1F);
|
||||
out_data >>= NBITS_CHANNEL;
|
||||
out.command = (uint8_t) (out_data & 0x7F);
|
||||
out_data >>= NBITS_COMMAND;
|
||||
out.address = (uint16_t) (out_data & 0xFFFF);
|
||||
|
||||
return out;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
void DraytonProtocol::dump(const DraytonData &data) {
|
||||
ESP_LOGI(TAG, "Received Drayton: address=0x%04X (0x%04x), channel=0x%03x command=0x%03X", data.address,
|
||||
|
||||
@ -26,12 +26,11 @@ DECLARE_REMOTE_PROTOCOL(Haier)
|
||||
|
||||
template<typename... Ts> class HaierAction : public RemoteTransmitterActionBase<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, data)
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, code)
|
||||
|
||||
void set_code(const std::vector<uint8_t> &code) { data_ = code; }
|
||||
void encode(RemoteTransmitData *dst, Ts... x) override {
|
||||
HaierData data{};
|
||||
data.data = this->data_.value(x...);
|
||||
data.data = this->code_.value(x...);
|
||||
HaierProtocol().encode(dst, data);
|
||||
}
|
||||
};
|
||||
|
||||
188
esphome/components/remote_base/keeloq_protocol.cpp
Normal file
188
esphome/components/remote_base/keeloq_protocol.cpp
Normal file
@ -0,0 +1,188 @@
|
||||
#include "keeloq_protocol.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace remote_base {
|
||||
|
||||
static const char *const TAG = "remote.keeloq";
|
||||
|
||||
static const uint32_t BIT_TIME_US = 380;
|
||||
static const uint8_t NBITS_PREAMBLE = 12;
|
||||
static const uint8_t NBITS_REPEAT = 1;
|
||||
static const uint8_t NBITS_VLOW = 1;
|
||||
static const uint8_t NBITS_SERIAL = 28;
|
||||
static const uint8_t NBITS_BUTTONS = 4;
|
||||
static const uint8_t NBITS_DISC = 12;
|
||||
static const uint8_t NBITS_SYNC_CNT = 16;
|
||||
|
||||
static const uint8_t NBITS_FIXED_DATA = NBITS_REPEAT + NBITS_VLOW + NBITS_BUTTONS + NBITS_SERIAL;
|
||||
static const uint8_t NBITS_ENCRYPTED_DATA = NBITS_BUTTONS + NBITS_DISC + NBITS_SYNC_CNT;
|
||||
static const uint8_t NBITS_DATA = NBITS_FIXED_DATA + NBITS_ENCRYPTED_DATA;
|
||||
|
||||
/*
|
||||
KeeLoq Protocol
|
||||
|
||||
Coded using information from datasheet for Microchip HCS301 KeeLow Code Hopping Encoder
|
||||
|
||||
Encoder - Hopping code is generated at random.
|
||||
|
||||
Decoder - Hopping code is ignored and not checked when received. Serial number of
|
||||
transmitter and nutton command is decoded.
|
||||
|
||||
*/
|
||||
|
||||
void KeeloqProtocol::encode(RemoteTransmitData *dst, const KeeloqData &data) {
|
||||
uint32_t out_data = 0x0;
|
||||
|
||||
ESP_LOGD(TAG, "Send Keeloq: address=%07x command=%03x encrypted=%08x", data.address, data.command, data.encrypted);
|
||||
ESP_LOGV(TAG, "Send Keeloq: data bits (%d + %d)", NBITS_ENCRYPTED_DATA, NBITS_FIXED_DATA);
|
||||
|
||||
// Preamble = '01' x 12
|
||||
for (uint8_t cnt = NBITS_PREAMBLE; cnt; cnt--) {
|
||||
dst->space(BIT_TIME_US);
|
||||
dst->mark(BIT_TIME_US);
|
||||
}
|
||||
|
||||
// Header = 10 bit space
|
||||
dst->space(10 * BIT_TIME_US);
|
||||
|
||||
// Encrypted field
|
||||
out_data = data.encrypted;
|
||||
|
||||
ESP_LOGV(TAG, "Send Keeloq: Encrypted data %04x", out_data);
|
||||
|
||||
for (uint32_t mask = 1, cnt = 0; cnt < NBITS_ENCRYPTED_DATA; cnt++, mask <<= 1) {
|
||||
if (out_data & mask) {
|
||||
dst->mark(1 * BIT_TIME_US);
|
||||
dst->space(2 * BIT_TIME_US);
|
||||
} else {
|
||||
dst->mark(2 * BIT_TIME_US);
|
||||
dst->space(1 * BIT_TIME_US);
|
||||
}
|
||||
}
|
||||
|
||||
// first 32 bits of fixed portion
|
||||
out_data = (data.command & 0x0f);
|
||||
out_data <<= NBITS_SERIAL;
|
||||
out_data |= data.address;
|
||||
ESP_LOGV(TAG, "Send Keeloq: Fixed data %04x", out_data);
|
||||
|
||||
for (uint32_t mask = 1, cnt = 0; cnt < (NBITS_FIXED_DATA - 2); cnt++, mask <<= 1) {
|
||||
if (out_data & mask) {
|
||||
dst->mark(1 * BIT_TIME_US);
|
||||
dst->space(2 * BIT_TIME_US);
|
||||
} else {
|
||||
dst->mark(2 * BIT_TIME_US);
|
||||
dst->space(1 * BIT_TIME_US);
|
||||
}
|
||||
}
|
||||
|
||||
// low battery flag
|
||||
if (data.vlow) {
|
||||
dst->mark(1 * BIT_TIME_US);
|
||||
dst->space(2 * BIT_TIME_US);
|
||||
} else {
|
||||
dst->mark(2 * BIT_TIME_US);
|
||||
dst->space(1 * BIT_TIME_US);
|
||||
}
|
||||
|
||||
// repeat flag - always sent as a '1'
|
||||
dst->mark(1 * BIT_TIME_US);
|
||||
dst->space(2 * BIT_TIME_US);
|
||||
|
||||
// Guard time at end of packet
|
||||
dst->space(39 * BIT_TIME_US);
|
||||
}
|
||||
|
||||
optional<KeeloqData> KeeloqProtocol::decode(RemoteReceiveData src) {
|
||||
KeeloqData out{
|
||||
.encrypted = 0,
|
||||
.address = 0,
|
||||
.command = 0,
|
||||
.repeat = false,
|
||||
.vlow = false,
|
||||
|
||||
};
|
||||
|
||||
if (src.size() != (NBITS_PREAMBLE + NBITS_DATA) * 2) {
|
||||
return {};
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, "%2d: %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d", src.size(), src.peek(0),
|
||||
src.peek(1), src.peek(2), src.peek(3), src.peek(4), src.peek(5), src.peek(6), src.peek(7), src.peek(8),
|
||||
src.peek(9), src.peek(10), src.peek(11), src.peek(12), src.peek(13), src.peek(14), src.peek(15),
|
||||
src.peek(16), src.peek(17), src.peek(18), src.peek(19));
|
||||
|
||||
// Check preamble bits
|
||||
int8_t bit = NBITS_PREAMBLE - 1;
|
||||
while (--bit >= 0) {
|
||||
if (!src.expect_mark(BIT_TIME_US) || !src.expect_space(BIT_TIME_US)) {
|
||||
ESP_LOGV(TAG, "Decode KeeLoq: Fail 1, %d %d", bit + 1, src.peek());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (!src.expect_mark(BIT_TIME_US) || !src.expect_space(10 * BIT_TIME_US)) {
|
||||
ESP_LOGV(TAG, "Decode KeeLoq: Fail 1, %d %d", bit + 1, src.peek());
|
||||
return {};
|
||||
}
|
||||
|
||||
// Read encrypted bits
|
||||
uint32_t out_data = 0;
|
||||
for (bit = 0; bit < NBITS_ENCRYPTED_DATA; bit++) {
|
||||
if (src.expect_mark(2 * BIT_TIME_US) && src.expect_space(BIT_TIME_US)) {
|
||||
out_data |= 0 << bit;
|
||||
} else if (src.expect_mark(BIT_TIME_US) && src.expect_space(2 * BIT_TIME_US)) {
|
||||
out_data |= 1 << bit;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Decode KeeLoq: Fail 2, %d %d", src.get_index(), src.peek());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
ESP_LOGVV(TAG, "Decode KeeLoq: Data, %d %08x", bit, out_data);
|
||||
out.encrypted = out_data;
|
||||
|
||||
// Read Serial Number and Button Status
|
||||
out_data = 0;
|
||||
for (bit = 0; bit < NBITS_SERIAL + NBITS_BUTTONS; bit++) {
|
||||
if (src.expect_mark(2 * BIT_TIME_US) && src.expect_space(BIT_TIME_US)) {
|
||||
out_data |= 0 << bit;
|
||||
} else if (src.expect_mark(BIT_TIME_US) && src.expect_space(2 * BIT_TIME_US)) {
|
||||
out_data |= 1 << bit;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Decode KeeLoq: Fail 3, %d %d", src.get_index(), src.peek());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
ESP_LOGVV(TAG, "Decode KeeLoq: Data, %2d %08x", bit, out_data);
|
||||
out.command = (out_data >> 28) & 0xf;
|
||||
out.address = out_data & 0xfffffff;
|
||||
|
||||
// Read Vlow bit
|
||||
if (src.expect_mark(2 * BIT_TIME_US) && src.expect_space(BIT_TIME_US)) {
|
||||
out.vlow = false;
|
||||
} else if (src.expect_mark(BIT_TIME_US) && src.expect_space(2 * BIT_TIME_US)) {
|
||||
out.vlow = true;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Decode KeeLoq: Fail 4, %08x", src.peek());
|
||||
return {};
|
||||
}
|
||||
|
||||
// Read Repeat bit
|
||||
if (src.expect_mark(2 * BIT_TIME_US) && src.peek_space_at_least(BIT_TIME_US)) {
|
||||
out.repeat = false;
|
||||
} else if (src.expect_mark(BIT_TIME_US) && src.peek_space_at_least(2 * BIT_TIME_US)) {
|
||||
out.repeat = true;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Decode KeeLoq: Fail 5, %08x", src.peek());
|
||||
return {};
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void KeeloqProtocol::dump(const KeeloqData &data) {
|
||||
ESP_LOGD(TAG, "Received Keeloq: address=0x%08X, command=0x%02x", data.address, data.command);
|
||||
}
|
||||
|
||||
} // namespace remote_base
|
||||
} // namespace esphome
|
||||
53
esphome/components/remote_base/keeloq_protocol.h
Normal file
53
esphome/components/remote_base/keeloq_protocol.h
Normal file
@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "remote_base.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace remote_base {
|
||||
|
||||
struct KeeloqData {
|
||||
uint32_t encrypted; // 32 bit encrypted field
|
||||
uint32_t address; // 28 bit serial number
|
||||
uint8_t command; // Button Status S2-S1-S0-S3
|
||||
bool repeat; // Repeated command bit
|
||||
bool vlow; // Battery status bit
|
||||
|
||||
bool operator==(const KeeloqData &rhs) const {
|
||||
// Treat 0x10 as a special, wildcard button press
|
||||
// This allows us to match on just the address if wanted.
|
||||
if (address != rhs.address) {
|
||||
return false;
|
||||
}
|
||||
return (rhs.command == 0x10 || command == rhs.command);
|
||||
}
|
||||
};
|
||||
|
||||
class KeeloqProtocol : public RemoteProtocol<KeeloqData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const KeeloqData &data) override;
|
||||
optional<KeeloqData> decode(RemoteReceiveData src) override;
|
||||
void dump(const KeeloqData &data) override;
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Keeloq)
|
||||
|
||||
template<typename... Ts> class KeeloqAction : public RemoteTransmitterActionBase<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint32_t, address)
|
||||
TEMPLATABLE_VALUE(uint32_t, encrypted)
|
||||
TEMPLATABLE_VALUE(uint8_t, command)
|
||||
TEMPLATABLE_VALUE(bool, vlow)
|
||||
|
||||
void encode(RemoteTransmitData *dst, Ts... x) override {
|
||||
KeeloqData data{};
|
||||
data.address = this->address_.value(x...);
|
||||
data.encrypted = this->encrypted_.value(x...);
|
||||
data.command = this->command_.value(x...);
|
||||
data.vlow = this->vlow_.value(x...);
|
||||
KeeloqProtocol().encode(dst, data);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace remote_base
|
||||
} // namespace esphome
|
||||
@ -82,6 +82,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
DEVICE_CLASS_VOLUME,
|
||||
DEVICE_CLASS_VOLUME_FLOW_RATE,
|
||||
DEVICE_CLASS_VOLUME_STORAGE,
|
||||
DEVICE_CLASS_WATER,
|
||||
DEVICE_CLASS_WEIGHT,
|
||||
@ -141,6 +142,7 @@ DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
DEVICE_CLASS_VOLUME,
|
||||
DEVICE_CLASS_VOLUME_FLOW_RATE,
|
||||
DEVICE_CLASS_VOLUME_STORAGE,
|
||||
DEVICE_CLASS_WATER,
|
||||
DEVICE_CLASS_WEIGHT,
|
||||
|
||||
@ -25,6 +25,7 @@ namespace sntp {
|
||||
static const char *const TAG = "sntp";
|
||||
|
||||
void SNTPComponent::setup() {
|
||||
#ifndef USE_HOST
|
||||
ESP_LOGCONFIG(TAG, "Setting up SNTP...");
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
if (sntp_enabled()) {
|
||||
@ -48,6 +49,7 @@ void SNTPComponent::setup() {
|
||||
#endif
|
||||
|
||||
sntp_init();
|
||||
#endif
|
||||
}
|
||||
void SNTPComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "SNTP Time:");
|
||||
@ -57,7 +59,7 @@ void SNTPComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
|
||||
}
|
||||
void SNTPComponent::update() {
|
||||
#ifndef USE_ESP_IDF
|
||||
#if !defined(USE_ESP_IDF) && !defined(USE_HOST)
|
||||
// force resync
|
||||
if (sntp_enabled()) {
|
||||
sntp_stop();
|
||||
|
||||
@ -86,6 +86,13 @@ class BSDSocketImpl : public Socket {
|
||||
}
|
||||
int listen(int backlog) override { return ::listen(fd_, backlog); }
|
||||
ssize_t read(void *buf, size_t len) override { return ::read(fd_, buf, len); }
|
||||
ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override {
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len);
|
||||
#else
|
||||
return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len);
|
||||
#endif
|
||||
}
|
||||
ssize_t readv(const struct iovec *iov, int iovcnt) override {
|
||||
#if defined(USE_ESP32)
|
||||
return ::lwip_readv(fd_, iov, iovcnt);
|
||||
|
||||
@ -31,6 +31,9 @@ class Socket {
|
||||
virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0;
|
||||
virtual int listen(int backlog) = 0;
|
||||
virtual ssize_t read(void *buf, size_t len) = 0;
|
||||
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
|
||||
virtual ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) = 0;
|
||||
#endif
|
||||
virtual ssize_t readv(const struct iovec *iov, int iovcnt) = 0;
|
||||
virtual ssize_t write(const void *buf, size_t len) = 0;
|
||||
virtual ssize_t writev(const struct iovec *iov, int iovcnt) = 0;
|
||||
|
||||
@ -29,12 +29,15 @@ from esphome.const import (
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
CONF_ALLOW_OTHER_USES,
|
||||
CONF_DATA_PINS,
|
||||
)
|
||||
from esphome.core import coroutine_with_priority, CORE
|
||||
|
||||
CODEOWNERS = ["@esphome/core", "@clydebarrow"]
|
||||
spi_ns = cg.esphome_ns.namespace("spi")
|
||||
SPIComponent = spi_ns.class_("SPIComponent", cg.Component)
|
||||
QuadSPIComponent = spi_ns.class_("QuadSPIComponent", cg.Component)
|
||||
SPIDevice = spi_ns.class_("SPIDevice")
|
||||
SPIDataRate = spi_ns.enum("SPIDataRate")
|
||||
SPIMode = spi_ns.enum("SPIMode")
|
||||
@ -190,12 +193,9 @@ def get_hw_spi(config, available):
|
||||
def validate_spi_config(config):
|
||||
available = list(range(len(get_hw_interface_list())))
|
||||
for spi in config:
|
||||
# map pin number to schema
|
||||
spi[CONF_CLK_PIN] = pins.gpio_output_pin_schema(spi[CONF_CLK_PIN])
|
||||
interface = spi[CONF_INTERFACE]
|
||||
if spi[CONF_FORCE_SW]:
|
||||
if interface == "any":
|
||||
spi[CONF_INTERFACE] = interface = "software"
|
||||
elif interface != "software":
|
||||
raise cv.Invalid("force_sw is deprecated - use interface: software")
|
||||
if interface == "software":
|
||||
pass
|
||||
elif interface == "any":
|
||||
@ -229,6 +229,8 @@ def validate_spi_config(config):
|
||||
spi, spi[CONF_INTERFACE_INDEX]
|
||||
):
|
||||
raise cv.Invalid("Invalid pin selections for hardware SPI interface")
|
||||
if CONF_DATA_PINS in spi and CONF_INTERFACE_INDEX not in spi:
|
||||
raise cv.Invalid("Quad mode requires a hardware interface")
|
||||
|
||||
return config
|
||||
|
||||
@ -249,14 +251,26 @@ def get_spi_interface(index):
|
||||
return "new SPIClass(HSPI)"
|
||||
|
||||
|
||||
# Do not use a pin schema for the number, as that will trigger a pin reuse error due to duplication of the
|
||||
# clock pin in the standard and quad schemas.
|
||||
clk_pin_validator = cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_NUMBER): cv.Any(cv.int_, cv.string),
|
||||
cv.Optional(CONF_ALLOW_OTHER_USES): cv.boolean,
|
||||
},
|
||||
key=CONF_NUMBER,
|
||||
)
|
||||
|
||||
SPI_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SPIComponent),
|
||||
cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Required(CONF_CLK_PIN): clk_pin_validator,
|
||||
cv.Optional(CONF_MISO_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Optional(CONF_MOSI_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_FORCE_SW, default=False): cv.boolean,
|
||||
cv.Optional(CONF_FORCE_SW): cv.invalid(
|
||||
"force_sw is deprecated - use interface: software"
|
||||
),
|
||||
cv.Optional(CONF_INTERFACE, default="any"): cv.one_of(
|
||||
*sum(get_hw_interface_list(), ["software", "hardware", "any"]),
|
||||
lower=True,
|
||||
@ -267,8 +281,34 @@ SPI_SCHEMA = cv.All(
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]),
|
||||
)
|
||||
|
||||
SPI_QUAD_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(QuadSPIComponent),
|
||||
cv.Required(CONF_CLK_PIN): clk_pin_validator,
|
||||
cv.Required(CONF_DATA_PINS): cv.All(
|
||||
cv.ensure_list(pins.internal_gpio_output_pin_number),
|
||||
cv.Length(min=4, max=4),
|
||||
),
|
||||
cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of(
|
||||
*sum(get_hw_interface_list(), ["hardware"]),
|
||||
lower=True,
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.only_with_esp_idf,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.ensure_list(SPI_SCHEMA),
|
||||
# Order is important. SPI_SCHEMA is the default.
|
||||
cv.ensure_list(
|
||||
cv.Any(
|
||||
SPI_SCHEMA,
|
||||
SPI_QUAD_SCHEMA,
|
||||
msg="Standard SPI requires mosi_pin and/or miso_pin; quad SPI requires data_pins only."
|
||||
+ " A clock pin is always required",
|
||||
),
|
||||
),
|
||||
validate_spi_config,
|
||||
)
|
||||
|
||||
@ -277,43 +317,46 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(configs):
|
||||
cg.add_define("USE_SPI")
|
||||
cg.add_global(spi_ns.using)
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("SPI", None)
|
||||
for spi in configs:
|
||||
var = cg.new_Pvariable(spi[CONF_ID])
|
||||
await cg.register_component(var, spi)
|
||||
|
||||
clk = await cg.gpio_pin_expression(spi[CONF_CLK_PIN])
|
||||
cg.add(var.set_clk(clk))
|
||||
if CONF_MISO_PIN in spi:
|
||||
miso = await cg.gpio_pin_expression(spi[CONF_MISO_PIN])
|
||||
cg.add(var.set_miso(miso))
|
||||
if CONF_MOSI_PIN in spi:
|
||||
mosi = await cg.gpio_pin_expression(spi[CONF_MOSI_PIN])
|
||||
cg.add(var.set_mosi(mosi))
|
||||
if CONF_INTERFACE_INDEX in spi:
|
||||
index = spi[CONF_INTERFACE_INDEX]
|
||||
cg.add(var.set_interface(cg.RawExpression(get_spi_interface(index))))
|
||||
if miso := spi.get(CONF_MISO_PIN):
|
||||
cg.add(var.set_miso(await cg.gpio_pin_expression(miso)))
|
||||
if mosi := spi.get(CONF_MOSI_PIN):
|
||||
cg.add(var.set_mosi(await cg.gpio_pin_expression(mosi)))
|
||||
if data_pins := spi.get(CONF_DATA_PINS):
|
||||
cg.add(var.set_data_pins(data_pins))
|
||||
if (index := spi.get(CONF_INTERFACE_INDEX)) is not None:
|
||||
interface = get_spi_interface(index)
|
||||
cg.add(var.set_interface(cg.RawExpression(interface)))
|
||||
cg.add(
|
||||
var.set_interface_name(
|
||||
re.sub(
|
||||
r"\W", "", get_spi_interface(index).replace("new SPIClass", "")
|
||||
)
|
||||
re.sub(r"\W", "", interface.replace("new SPIClass", ""))
|
||||
)
|
||||
)
|
||||
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("SPI", None)
|
||||
|
||||
|
||||
def spi_device_schema(
|
||||
cs_pin_required=True, default_data_rate=cv.UNDEFINED, default_mode=cv.UNDEFINED
|
||||
cs_pin_required=True,
|
||||
default_data_rate=cv.UNDEFINED,
|
||||
default_mode=cv.UNDEFINED,
|
||||
quad=False,
|
||||
):
|
||||
"""Create a schema for an SPI device.
|
||||
:param cs_pin_required: If true, make the CS_PIN required in the config.
|
||||
:param default_data_rate: Optional data_rate to use as default
|
||||
:param default_mode Optional. The default SPI mode to use.
|
||||
:param quad If set, will require an SPI component configured as quad data bits.
|
||||
:return: The SPI device schema, `extend` this in your config schema.
|
||||
"""
|
||||
schema = {
|
||||
cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent),
|
||||
cv.GenerateID(CONF_SPI_ID): cv.use_id(
|
||||
QuadSPIComponent if quad else SPIComponent
|
||||
),
|
||||
cv.Optional(CONF_DATA_RATE, default=default_data_rate): SPI_DATA_RATE_SCHEMA,
|
||||
cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum(
|
||||
SPI_MODE_OPTIONS, upper=True
|
||||
|
||||
@ -49,7 +49,8 @@ void SPIComponent::setup() {
|
||||
}
|
||||
|
||||
if (this->using_hw_) {
|
||||
this->spi_bus_ = SPIComponent::get_bus(this->interface_, this->clk_pin_, this->sdo_pin_, this->sdi_pin_);
|
||||
this->spi_bus_ =
|
||||
SPIComponent::get_bus(this->interface_, this->clk_pin_, this->sdo_pin_, this->sdi_pin_, this->data_pins_);
|
||||
if (this->spi_bus_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Unable to allocate SPI interface");
|
||||
this->mark_failed();
|
||||
@ -68,6 +69,9 @@ void SPIComponent::dump_config() {
|
||||
LOG_PIN(" CLK Pin: ", this->clk_pin_)
|
||||
LOG_PIN(" SDI Pin: ", this->sdi_pin_)
|
||||
LOG_PIN(" SDO Pin: ", this->sdo_pin_)
|
||||
for (size_t i = 0; i != this->data_pins_.size(); i++) {
|
||||
ESP_LOGCONFIG(TAG, " Data pin %u: GPIO%d", i, this->data_pins_[i]);
|
||||
}
|
||||
if (this->spi_bus_->is_hw()) {
|
||||
ESP_LOGCONFIG(TAG, " Using HW SPI: %s", this->interface_name_);
|
||||
} else {
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
@ -208,6 +209,10 @@ class SPIDelegate {
|
||||
esph_log_e("spi_device", "variable length write not implemented");
|
||||
}
|
||||
|
||||
virtual void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address,
|
||||
const uint8_t *data, size_t length, uint8_t bus_width) {
|
||||
esph_log_e("spi_device", "write_cmd_addr_data not implemented");
|
||||
}
|
||||
// write 16 bits
|
||||
virtual void write16(uint16_t data) {
|
||||
if (this->bit_order_ == BIT_ORDER_MSB_FIRST) {
|
||||
@ -331,6 +336,7 @@ class SPIComponent : public Component {
|
||||
void set_miso(GPIOPin *sdi) { this->sdi_pin_ = sdi; }
|
||||
|
||||
void set_mosi(GPIOPin *sdo) { this->sdo_pin_ = sdo; }
|
||||
void set_data_pins(std::vector<uint8_t> pins) { this->data_pins_ = std::move(pins); }
|
||||
|
||||
void set_interface(SPIInterface interface) {
|
||||
this->interface_ = interface;
|
||||
@ -348,15 +354,19 @@ class SPIComponent : public Component {
|
||||
GPIOPin *clk_pin_{nullptr};
|
||||
GPIOPin *sdi_pin_{nullptr};
|
||||
GPIOPin *sdo_pin_{nullptr};
|
||||
std::vector<uint8_t> data_pins_{};
|
||||
|
||||
SPIInterface interface_{};
|
||||
bool using_hw_{false};
|
||||
const char *interface_name_{nullptr};
|
||||
SPIBus *spi_bus_{};
|
||||
std::map<SPIClient *, SPIDelegate *> devices_;
|
||||
|
||||
static SPIBus *get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi);
|
||||
static SPIBus *get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi,
|
||||
const std::vector<uint8_t> &data_pins);
|
||||
};
|
||||
|
||||
using QuadSPIComponent = SPIComponent;
|
||||
/**
|
||||
* Base class for SPIDevice, un-templated.
|
||||
*/
|
||||
@ -422,18 +432,49 @@ class SPIDevice : public SPIClient {
|
||||
|
||||
void read_array(uint8_t *data, size_t length) { return this->delegate_->read_array(data, length); }
|
||||
|
||||
/**
|
||||
* Write a single data item, up to 32 bits.
|
||||
* @param data The data
|
||||
* @param num_bits The number of bits to write. The lower num_bits of data will be sent.
|
||||
*/
|
||||
void write(uint16_t data, size_t num_bits) { this->delegate_->write(data, num_bits); };
|
||||
|
||||
/* Write command, address and data. Command and address will be written as single-bit SPI,
|
||||
* data phase can be multiple bit (currently only 1 or 4)
|
||||
* @param cmd_bits Number of bits to write in the command phase
|
||||
* @param cmd The command value to write
|
||||
* @param addr_bits Number of bits to write in addr phase
|
||||
* @param address Address data
|
||||
* @param data Plain data bytes
|
||||
* @param length Number of data bytes
|
||||
* @param bus_width The number of data lines to use for the data phase.
|
||||
*/
|
||||
void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address, const uint8_t *data,
|
||||
size_t length, uint8_t bus_width = 1) {
|
||||
this->delegate_->write_cmd_addr_data(cmd_bits, cmd, addr_bits, address, data, length, bus_width);
|
||||
}
|
||||
|
||||
void write_byte(uint8_t data) { this->delegate_->write_array(&data, 1); }
|
||||
|
||||
/**
|
||||
* Write the array data, replace with received data.
|
||||
* @param data
|
||||
* @param length
|
||||
*/
|
||||
void transfer_array(uint8_t *data, size_t length) { this->delegate_->transfer(data, length); }
|
||||
|
||||
uint8_t transfer_byte(uint8_t data) { return this->delegate_->transfer(data); }
|
||||
|
||||
// the driver will byte-swap if required.
|
||||
/** Write 16 bit data. The driver will byte-swap if required.
|
||||
*/
|
||||
void write_byte16(uint16_t data) { this->delegate_->write16(data); }
|
||||
|
||||
// avoid use of this if possible. It's inefficient and ugly.
|
||||
/**
|
||||
* Write an array of data as 16 bit values, byte-swapping if required. Use of this should be avoided as
|
||||
* it is horribly slow.
|
||||
* @param data
|
||||
* @param length
|
||||
*/
|
||||
void write_array16(const uint16_t *data, size_t length) { this->delegate_->write_array16(data, length); }
|
||||
|
||||
void enable() { this->delegate_->begin_transaction(); }
|
||||
|
||||
@ -85,7 +85,8 @@ class SPIBusHw : public SPIBus {
|
||||
bool is_hw() override { return true; }
|
||||
};
|
||||
|
||||
SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) {
|
||||
SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi,
|
||||
const std::vector<uint8_t> &data_pins) {
|
||||
return new SPIBusHw(clk, sdo, sdi, interface);
|
||||
}
|
||||
|
||||
|
||||
@ -104,6 +104,60 @@ class SPIDelegateHw : public SPIDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write command, address and data
|
||||
* @param cmd_bits Number of bits to write in the command phase
|
||||
* @param cmd The command value to write
|
||||
* @param addr_bits Number of bits to write in addr phase
|
||||
* @param address Address data
|
||||
* @param data Remaining data bytes
|
||||
* @param length Number of data bytes
|
||||
* @param bus_width The number of data lines to use
|
||||
*/
|
||||
void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address, const uint8_t *data,
|
||||
size_t length, uint8_t bus_width) override {
|
||||
spi_transaction_ext_t desc = {};
|
||||
if (length == 0 && cmd_bits == 0 && addr_bits == 0) {
|
||||
esph_log_w(TAG, "Nothing to transfer");
|
||||
return;
|
||||
}
|
||||
desc.base.flags = SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_DUMMY;
|
||||
if (bus_width == 4) {
|
||||
desc.base.flags |= SPI_TRANS_MODE_QIO;
|
||||
} else if (bus_width == 8) {
|
||||
desc.base.flags |= SPI_TRANS_MODE_OCT;
|
||||
}
|
||||
desc.command_bits = cmd_bits;
|
||||
desc.address_bits = addr_bits;
|
||||
desc.dummy_bits = 0;
|
||||
desc.base.rxlength = 0;
|
||||
desc.base.cmd = cmd;
|
||||
desc.base.addr = address;
|
||||
do {
|
||||
size_t chunk_size = std::min(length, MAX_TRANSFER_SIZE);
|
||||
if (data != nullptr && chunk_size != 0) {
|
||||
desc.base.length = chunk_size * 8;
|
||||
desc.base.tx_buffer = data;
|
||||
length -= chunk_size;
|
||||
data += chunk_size;
|
||||
} else {
|
||||
length = 0;
|
||||
desc.base.length = 0;
|
||||
}
|
||||
esp_err_t err = spi_device_polling_start(this->handle_, (spi_transaction_t *) &desc, portMAX_DELAY);
|
||||
if (err == ESP_OK) {
|
||||
err = spi_device_polling_end(this->handle_, portMAX_DELAY);
|
||||
}
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Transmit failed - err %X", err);
|
||||
return;
|
||||
}
|
||||
// if more data is to be sent, skip the command and address phases.
|
||||
desc.command_bits = 0;
|
||||
desc.address_bits = 0;
|
||||
} while (length != 0);
|
||||
}
|
||||
|
||||
void transfer(uint8_t *ptr, size_t length) override { this->transfer(ptr, ptr, length); }
|
||||
|
||||
uint8_t transfer(uint8_t data) override {
|
||||
@ -142,13 +196,27 @@ class SPIDelegateHw : public SPIDelegate {
|
||||
|
||||
class SPIBusHw : public SPIBus {
|
||||
public:
|
||||
SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel) : SPIBus(clk, sdo, sdi), channel_(channel) {
|
||||
SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel, std::vector<uint8_t> data_pins)
|
||||
: SPIBus(clk, sdo, sdi), channel_(channel) {
|
||||
spi_bus_config_t buscfg = {};
|
||||
buscfg.mosi_io_num = Utility::get_pin_no(sdo);
|
||||
buscfg.miso_io_num = Utility::get_pin_no(sdi);
|
||||
buscfg.sclk_io_num = Utility::get_pin_no(clk);
|
||||
buscfg.quadwp_io_num = -1;
|
||||
buscfg.quadhd_io_num = -1;
|
||||
buscfg.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_SCLK;
|
||||
if (data_pins.empty()) {
|
||||
buscfg.mosi_io_num = Utility::get_pin_no(sdo);
|
||||
buscfg.miso_io_num = Utility::get_pin_no(sdi);
|
||||
buscfg.quadwp_io_num = -1;
|
||||
buscfg.quadhd_io_num = -1;
|
||||
} else {
|
||||
buscfg.data0_io_num = data_pins[0];
|
||||
buscfg.data1_io_num = data_pins[1];
|
||||
buscfg.data2_io_num = data_pins[2];
|
||||
buscfg.data3_io_num = data_pins[3];
|
||||
buscfg.data4_io_num = -1;
|
||||
buscfg.data5_io_num = -1;
|
||||
buscfg.data6_io_num = -1;
|
||||
buscfg.data7_io_num = -1;
|
||||
buscfg.flags |= SPICOMMON_BUSFLAG_QUAD;
|
||||
}
|
||||
buscfg.max_transfer_sz = MAX_TRANSFER_SIZE;
|
||||
auto err = spi_bus_initialize(channel, &buscfg, SPI_DMA_CH_AUTO);
|
||||
if (err != ESP_OK)
|
||||
@ -166,8 +234,9 @@ class SPIBusHw : public SPIBus {
|
||||
bool is_hw() override { return true; }
|
||||
};
|
||||
|
||||
SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) {
|
||||
return new SPIBusHw(clk, sdo, sdi, interface);
|
||||
SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi,
|
||||
const std::vector<uint8_t> &data_pins) {
|
||||
return new SPIBusHw(clk, sdo, sdi, interface, data_pins);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
55
esphome/components/st7567_base/__init__.py
Normal file
55
esphome/components/st7567_base/__init__.py
Normal file
@ -0,0 +1,55 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import pins
|
||||
from esphome.components import display
|
||||
from esphome.const import (
|
||||
CONF_LAMBDA,
|
||||
CONF_RESET_PIN,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_TRANSFORM,
|
||||
CONF_INVERT_COLORS,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@latonita"]
|
||||
|
||||
st7567_base_ns = cg.esphome_ns.namespace("st7567_base")
|
||||
ST7567 = st7567_base_ns.class_("ST7567", cg.PollingComponent, display.DisplayBuffer)
|
||||
ST7567Model = st7567_base_ns.enum("ST7567Model")
|
||||
|
||||
# todo in future: reuse following constants from const.py when they are released
|
||||
|
||||
|
||||
ST7567_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean,
|
||||
cv.Optional(CONF_TRANSFORM): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MIRROR_X, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
}
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
|
||||
async def setup_st7567(var, config):
|
||||
await display.register_display(var, config)
|
||||
|
||||
if CONF_RESET_PIN in config:
|
||||
reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
|
||||
cg.add(var.set_reset_pin(reset))
|
||||
|
||||
cg.add(var.init_invert_colors(config[CONF_INVERT_COLORS]))
|
||||
|
||||
if CONF_TRANSFORM in config:
|
||||
transform = config[CONF_TRANSFORM]
|
||||
cg.add(var.init_mirror_x(transform[CONF_MIRROR_X]))
|
||||
cg.add(var.init_mirror_y(transform[CONF_MIRROR_Y]))
|
||||
|
||||
if CONF_LAMBDA in config:
|
||||
lambda_ = await cg.process_lambda(
|
||||
config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
152
esphome/components/st7567_base/st7567_base.cpp
Normal file
152
esphome/components/st7567_base/st7567_base.cpp
Normal file
@ -0,0 +1,152 @@
|
||||
#include "st7567_base.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace st7567_base {
|
||||
|
||||
static const char *const TAG = "st7567";
|
||||
|
||||
void ST7567::setup() {
|
||||
this->init_internal_(this->get_buffer_length_());
|
||||
this->display_init_();
|
||||
}
|
||||
|
||||
void ST7567::display_init_() {
|
||||
ESP_LOGD(TAG, "Initializing ST7567 display...");
|
||||
this->display_init_registers_();
|
||||
this->clear();
|
||||
this->write_display_data();
|
||||
this->command(ST7567_DISPLAY_ON);
|
||||
}
|
||||
|
||||
void ST7567::display_init_registers_() {
|
||||
this->command(ST7567_BIAS_9);
|
||||
this->command(this->mirror_x_ ? ST7567_SEG_REVERSE : ST7567_SEG_NORMAL);
|
||||
this->command(this->mirror_y_ ? ST7567_COM_NORMAL : ST7567_COM_REMAP);
|
||||
this->command(ST7567_POWER_CTL | 0x4);
|
||||
this->command(ST7567_POWER_CTL | 0x6);
|
||||
this->command(ST7567_POWER_CTL | 0x7);
|
||||
|
||||
this->set_brightness(this->brightness_);
|
||||
this->set_contrast(this->contrast_);
|
||||
|
||||
this->command(ST7567_INVERT_OFF | this->invert_colors_);
|
||||
|
||||
this->command(ST7567_BOOSTER_ON);
|
||||
this->command(ST7567_REGULATOR_ON);
|
||||
this->command(ST7567_POWER_ON);
|
||||
|
||||
this->command(ST7567_SCAN_START_LINE);
|
||||
this->command(ST7567_PIXELS_NORMAL | this->all_pixels_on_);
|
||||
}
|
||||
|
||||
void ST7567::display_sw_refresh_() {
|
||||
ESP_LOGD(TAG, "Performing refresh sequence...");
|
||||
this->command(ST7567_SW_REFRESH);
|
||||
this->display_init_registers_();
|
||||
}
|
||||
|
||||
void ST7567::request_refresh() {
|
||||
// as per datasheet: It is recommended to use the refresh sequence regularly in a specified interval.
|
||||
this->refresh_requested_ = true;
|
||||
}
|
||||
|
||||
void ST7567::update() {
|
||||
this->do_update_();
|
||||
if (this->refresh_requested_) {
|
||||
this->refresh_requested_ = false;
|
||||
this->display_sw_refresh_();
|
||||
}
|
||||
this->write_display_data();
|
||||
}
|
||||
|
||||
void ST7567::set_all_pixels_on(bool enable) {
|
||||
this->all_pixels_on_ = enable;
|
||||
this->command(ST7567_PIXELS_NORMAL | this->all_pixels_on_);
|
||||
}
|
||||
|
||||
void ST7567::set_invert_colors(bool invert_colors) {
|
||||
this->invert_colors_ = invert_colors;
|
||||
this->command(ST7567_INVERT_OFF | this->invert_colors_);
|
||||
}
|
||||
|
||||
void ST7567::set_contrast(uint8_t val) {
|
||||
this->contrast_ = val & 0b111111;
|
||||
// 0..63, 26 is normal
|
||||
|
||||
// two byte command
|
||||
// first byte 0x81
|
||||
// second byte 0-63
|
||||
|
||||
this->command(ST7567_SET_EV_CMD);
|
||||
this->command(this->contrast_);
|
||||
}
|
||||
|
||||
void ST7567::set_brightness(uint8_t val) {
|
||||
this->brightness_ = val & 0b111;
|
||||
// 0..7, 5 normal
|
||||
|
||||
//********Adjust display brightness********
|
||||
// 0x20-0x27 is the internal Rb/Ra resistance
|
||||
// adjustment setting of V5 voltage RR=4.5V
|
||||
|
||||
this->command(ST7567_RESISTOR_RATIO | this->brightness_);
|
||||
}
|
||||
|
||||
bool ST7567::is_on() { return this->is_on_; }
|
||||
|
||||
void ST7567::turn_on() {
|
||||
this->command(ST7567_DISPLAY_ON);
|
||||
this->is_on_ = true;
|
||||
}
|
||||
|
||||
void ST7567::turn_off() {
|
||||
this->command(ST7567_DISPLAY_OFF);
|
||||
this->is_on_ = false;
|
||||
}
|
||||
|
||||
void ST7567::set_scroll(uint8_t line) { this->start_line_ = line % this->get_height_internal(); }
|
||||
|
||||
int ST7567::get_width_internal() { return 128; }
|
||||
|
||||
int ST7567::get_height_internal() { return 64; }
|
||||
|
||||
// 128x64, but memory size 132x64, line starts from 0, but if mirrored then it starts from 131, not 127
|
||||
size_t ST7567::get_buffer_length_() {
|
||||
return size_t(this->get_width_internal() + 4) * size_t(this->get_height_internal()) / 8u;
|
||||
}
|
||||
|
||||
void HOT ST7567::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t pos = x + (y / 8) * this->get_width_internal();
|
||||
uint8_t subpos = y & 0x07;
|
||||
if (color.is_on()) {
|
||||
this->buffer_[pos] |= (1 << subpos);
|
||||
} else {
|
||||
this->buffer_[pos] &= ~(1 << subpos);
|
||||
}
|
||||
}
|
||||
|
||||
void ST7567::fill(Color color) { memset(buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); }
|
||||
|
||||
void ST7567::init_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(1);
|
||||
// Trigger Reset
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
// Wake up
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
|
||||
const char *ST7567::model_str_() { return "ST7567 128x64"; }
|
||||
|
||||
} // namespace st7567_base
|
||||
} // namespace esphome
|
||||
100
esphome/components/st7567_base/st7567_base.h
Normal file
100
esphome/components/st7567_base/st7567_base.h
Normal file
@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace st7567_base {
|
||||
|
||||
static const uint8_t ST7567_BOOSTER_ON = 0x2C; // internal power supply on
|
||||
static const uint8_t ST7567_REGULATOR_ON = 0x2E; // internal power supply on
|
||||
static const uint8_t ST7567_POWER_ON = 0x2F; // internal power supply on
|
||||
|
||||
static const uint8_t ST7567_DISPLAY_ON = 0xAF; // Display ON. Normal Display Mode.
|
||||
static const uint8_t ST7567_DISPLAY_OFF = 0xAE; // Display OFF. All SEGs/COMs output with VSS
|
||||
static const uint8_t ST7567_SET_START_LINE = 0x40;
|
||||
static const uint8_t ST7567_POWER_CTL = 0x28;
|
||||
static const uint8_t ST7567_SEG_NORMAL = 0xA0; //
|
||||
static const uint8_t ST7567_SEG_REVERSE = 0xA1; // mirror X axis (horizontal)
|
||||
static const uint8_t ST7567_COM_NORMAL = 0xC0; //
|
||||
static const uint8_t ST7567_COM_REMAP = 0xC8; // mirror Y axis (vertical)
|
||||
static const uint8_t ST7567_PIXELS_NORMAL = 0xA4; // display ram content
|
||||
static const uint8_t ST7567_PIXELS_ALL_ON = 0xA5; // all pixels on
|
||||
static const uint8_t ST7567_INVERT_OFF = 0xA6; // normal pixels
|
||||
static const uint8_t ST7567_INVERT_ON = 0xA7; // inverted pixels
|
||||
static const uint8_t ST7567_SCAN_START_LINE = 0x40; // scrolling = 0x40 + (0..63)
|
||||
static const uint8_t ST7567_COL_ADDR_H = 0x10; // x pos (0..95) 4 MSB
|
||||
static const uint8_t ST7567_COL_ADDR_L = 0x00; // x pos (0..95) 4 LSB
|
||||
static const uint8_t ST7567_PAGE_ADDR = 0xB0; // y pos, 8.5 rows (0..8)
|
||||
static const uint8_t ST7567_BIAS_9 = 0xA2;
|
||||
static const uint8_t ST7567_CONTRAST = 0x80; // 0x80 + (0..31)
|
||||
static const uint8_t ST7567_SET_EV_CMD = 0x81;
|
||||
static const uint8_t ST7567_SET_EV_PARAM = 0x00;
|
||||
static const uint8_t ST7567_RESISTOR_RATIO = 0x20;
|
||||
static const uint8_t ST7567_SW_REFRESH = 0xE2;
|
||||
|
||||
class ST7567 : public display::DisplayBuffer {
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
void update() override;
|
||||
|
||||
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void init_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; }
|
||||
void init_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; }
|
||||
void init_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; }
|
||||
|
||||
void set_invert_colors(bool invert_colors); // inversion of screen colors
|
||||
void set_contrast(uint8_t val); // 0..63, 27-30 normal
|
||||
void set_brightness(uint8_t val); // 0..7, 5 normal
|
||||
void set_all_pixels_on(bool enable); // turn on all pixels, this doesn't affect RAM
|
||||
void set_scroll(uint8_t line); // set display start line: for screen scrolling w/o affecting RAM
|
||||
|
||||
bool is_on();
|
||||
void turn_on();
|
||||
void turn_off();
|
||||
|
||||
void request_refresh(); // from datasheet: It is recommended to use the refresh sequence regularly in a specified
|
||||
// interval.
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
void fill(Color color) override;
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
|
||||
|
||||
protected:
|
||||
virtual void command(uint8_t value) = 0;
|
||||
virtual void write_display_data() = 0;
|
||||
|
||||
void init_reset_();
|
||||
void display_init_();
|
||||
void display_init_registers_();
|
||||
void display_sw_refresh_();
|
||||
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
|
||||
int get_height_internal() override;
|
||||
int get_width_internal() override;
|
||||
size_t get_buffer_length_();
|
||||
|
||||
int get_offset_x_() { return mirror_x_ ? 4 : 0; };
|
||||
|
||||
const char *model_str_();
|
||||
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
bool is_on_{false};
|
||||
// float contrast_{1.0};
|
||||
// float brightness_{1.0};
|
||||
uint8_t contrast_{27};
|
||||
uint8_t brightness_{5};
|
||||
bool mirror_x_{true};
|
||||
bool mirror_y_{true};
|
||||
bool invert_colors_{false};
|
||||
bool all_pixels_on_{false};
|
||||
uint8_t start_line_{0};
|
||||
bool refresh_requested_{false};
|
||||
};
|
||||
|
||||
} // namespace st7567_base
|
||||
} // namespace esphome
|
||||
1
esphome/components/st7567_i2c/__init__.py
Normal file
1
esphome/components/st7567_i2c/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@latonita"]
|
||||
29
esphome/components/st7567_i2c/display.py
Normal file
29
esphome/components/st7567_i2c/display.py
Normal file
@ -0,0 +1,29 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import st7567_base, i2c
|
||||
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES
|
||||
|
||||
CODEOWNERS = ["@latonita"]
|
||||
|
||||
AUTO_LOAD = ["st7567_base"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
st7567_i2c = cg.esphome_ns.namespace("st7567_i2c")
|
||||
I2CST7567 = st7567_i2c.class_("I2CST7567", st7567_base.ST7567, i2c.I2CDevice)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
st7567_base.ST7567_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(I2CST7567),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x3F)),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await st7567_base.setup_st7567(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
60
esphome/components/st7567_i2c/st7567_i2c.cpp
Normal file
60
esphome/components/st7567_i2c/st7567_i2c.cpp
Normal file
@ -0,0 +1,60 @@
|
||||
#include "st7567_i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace st7567_i2c {
|
||||
|
||||
static const char *const TAG = "st7567_i2c";
|
||||
|
||||
void I2CST7567::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up I2C ST7567 display...");
|
||||
this->init_reset_();
|
||||
|
||||
auto err = this->write(nullptr, 0);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
ST7567::setup();
|
||||
}
|
||||
|
||||
void I2CST7567::dump_config() {
|
||||
LOG_DISPLAY("", "I2CST7567", this);
|
||||
LOG_I2C_DEVICE(this);
|
||||
ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_());
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Mirror X: %s", YESNO(this->mirror_x_));
|
||||
ESP_LOGCONFIG(TAG, " Mirror Y: %s", YESNO(this->mirror_y_));
|
||||
ESP_LOGCONFIG(TAG, " Invert Colors: %s", YESNO(this->invert_colors_));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
if (this->error_code_ == COMMUNICATION_FAILED) {
|
||||
ESP_LOGE(TAG, "Communication with I2C ST7567 failed!");
|
||||
}
|
||||
}
|
||||
|
||||
void I2CST7567::command(uint8_t value) { this->write_byte(0x00, value); }
|
||||
|
||||
void HOT I2CST7567::write_display_data() {
|
||||
// ST7567A has built-in RAM with 132x65 bit capacity which stores the display data.
|
||||
// but only first 128 pixels from each line are shown on screen
|
||||
// if screen got flipped horizontally then it shows last 128 pixels,
|
||||
// so we need to write x coordinate starting from column 4, not column 0
|
||||
this->command(esphome::st7567_base::ST7567_SET_START_LINE + this->start_line_);
|
||||
for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) {
|
||||
this->command(esphome::st7567_base::ST7567_PAGE_ADDR + y); // Set Page
|
||||
this->command(esphome::st7567_base::ST7567_COL_ADDR_H); // Set MSB Column address
|
||||
this->command(esphome::st7567_base::ST7567_COL_ADDR_L + this->get_offset_x_()); // Set LSB Column address
|
||||
|
||||
static const size_t BLOCK_SIZE = 64;
|
||||
for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x += BLOCK_SIZE) {
|
||||
this->write_register(esphome::st7567_base::ST7567_SET_START_LINE, &buffer_[y * this->get_width_internal() + x],
|
||||
this->get_width_internal() - x > BLOCK_SIZE ? BLOCK_SIZE : this->get_width_internal() - x,
|
||||
true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace st7567_i2c
|
||||
} // namespace esphome
|
||||
23
esphome/components/st7567_i2c/st7567_i2c.h
Normal file
23
esphome/components/st7567_i2c/st7567_i2c.h
Normal file
@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/st7567_base/st7567_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace st7567_i2c {
|
||||
|
||||
class I2CST7567 : public st7567_base::ST7567, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void command(uint8_t value) override;
|
||||
void write_display_data() override;
|
||||
|
||||
enum ErrorCode { NONE = 0, COMMUNICATION_FAILED } error_code_{NONE};
|
||||
};
|
||||
|
||||
} // namespace st7567_i2c
|
||||
} // namespace esphome
|
||||
1
esphome/components/st7567_spi/__init__.py
Normal file
1
esphome/components/st7567_spi/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@latonita"]
|
||||
34
esphome/components/st7567_spi/display.py
Normal file
34
esphome/components/st7567_spi/display.py
Normal file
@ -0,0 +1,34 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import pins
|
||||
from esphome.components import spi, st7567_base
|
||||
from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES
|
||||
|
||||
CODEOWNERS = ["@latonita"]
|
||||
|
||||
AUTO_LOAD = ["st7567_base"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
st7567_spi = cg.esphome_ns.namespace("st7567_spi")
|
||||
SPIST7567 = st7567_spi.class_("SPIST7567", st7567_base.ST7567, spi.SPIDevice)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
st7567_base.ST7567_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SPIST7567),
|
||||
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema()),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await st7567_base.setup_st7567(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
66
esphome/components/st7567_spi/st7567_spi.cpp
Normal file
66
esphome/components/st7567_spi/st7567_spi.cpp
Normal file
@ -0,0 +1,66 @@
|
||||
#include "st7567_spi.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace st7567_spi {
|
||||
|
||||
static const char *const TAG = "st7567_spi";
|
||||
|
||||
void SPIST7567::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up SPI ST7567 display...");
|
||||
this->spi_setup();
|
||||
this->dc_pin_->setup();
|
||||
if (this->cs_)
|
||||
this->cs_->setup();
|
||||
|
||||
this->init_reset_();
|
||||
ST7567::setup();
|
||||
}
|
||||
|
||||
void SPIST7567::dump_config() {
|
||||
LOG_DISPLAY("", "SPI ST7567", this);
|
||||
ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_());
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Mirror X: %s", YESNO(this->mirror_x_));
|
||||
ESP_LOGCONFIG(TAG, " Mirror Y: %s", YESNO(this->mirror_y_));
|
||||
ESP_LOGCONFIG(TAG, " Invert Colors: %s", YESNO(this->invert_colors_));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
void SPIST7567::command(uint8_t value) {
|
||||
if (this->cs_)
|
||||
this->cs_->digital_write(true);
|
||||
this->dc_pin_->digital_write(false);
|
||||
delay(1);
|
||||
this->enable();
|
||||
if (this->cs_)
|
||||
this->cs_->digital_write(false);
|
||||
this->write_byte(value);
|
||||
if (this->cs_)
|
||||
this->cs_->digital_write(true);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void HOT SPIST7567::write_display_data() {
|
||||
// ST7567A has built-in RAM with 132x65 bit capacity which stores the display data.
|
||||
// but only first 128 pixels from each line are shown on screen
|
||||
// if screen got flipped horizontally then it shows last 128 pixels,
|
||||
// so we need to write x coordinate starting from column 4, not column 0
|
||||
this->command(esphome::st7567_base::ST7567_SET_START_LINE + this->start_line_);
|
||||
for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) {
|
||||
this->dc_pin_->digital_write(false);
|
||||
this->command(esphome::st7567_base::ST7567_PAGE_ADDR + y); // Set Page
|
||||
this->command(esphome::st7567_base::ST7567_COL_ADDR_H); // Set MSB Column address
|
||||
this->command(esphome::st7567_base::ST7567_COL_ADDR_L + this->get_offset_x_()); // Set LSB Column address
|
||||
this->dc_pin_->digital_write(true);
|
||||
|
||||
this->enable();
|
||||
this->write_array(&this->buffer_[y * this->get_width_internal()], this->get_width_internal());
|
||||
this->disable();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace st7567_spi
|
||||
} // namespace esphome
|
||||
29
esphome/components/st7567_spi/st7567_spi.h
Normal file
29
esphome/components/st7567_spi/st7567_spi.h
Normal file
@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/st7567_base/st7567_base.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace st7567_spi {
|
||||
|
||||
class SPIST7567 : public st7567_base::ST7567,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
|
||||
spi::DATA_RATE_8MHZ> {
|
||||
public:
|
||||
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
|
||||
|
||||
void setup() override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void command(uint8_t value) override;
|
||||
|
||||
void write_display_data() override;
|
||||
|
||||
GPIOPin *dc_pin_;
|
||||
};
|
||||
|
||||
} // namespace st7567_spi
|
||||
} // namespace esphome
|
||||
@ -10,6 +10,7 @@ from esphome.const import (
|
||||
CONF_MODEL,
|
||||
CONF_RESET_PIN,
|
||||
CONF_PAGES,
|
||||
CONF_INVERT_COLORS,
|
||||
)
|
||||
from . import st7735_ns
|
||||
|
||||
@ -23,7 +24,6 @@ CONF_ROW_START = "row_start"
|
||||
CONF_COL_START = "col_start"
|
||||
CONF_EIGHT_BIT_COLOR = "eight_bit_color"
|
||||
CONF_USE_BGR = "use_bgr"
|
||||
CONF_INVERT_COLORS = "invert_colors"
|
||||
|
||||
SPIST7735 = st7735_ns.class_(
|
||||
"ST7735", cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice
|
||||
|
||||
@ -14,12 +14,12 @@ from esphome.const import (
|
||||
CONF_POWER_SUPPLY,
|
||||
CONF_ROTATION,
|
||||
CONF_CS_PIN,
|
||||
CONF_OFFSET_HEIGHT,
|
||||
CONF_OFFSET_WIDTH,
|
||||
)
|
||||
from . import st7789v_ns
|
||||
|
||||
CONF_EIGHTBITCOLOR = "eightbitcolor"
|
||||
CONF_OFFSET_HEIGHT = "offset_height"
|
||||
CONF_OFFSET_WIDTH = "offset_width"
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
|
||||
@ -97,6 +97,19 @@ MODELS = {
|
||||
CONF_BACKLIGHT_PIN: "GPIO15",
|
||||
}
|
||||
),
|
||||
"WAVESHARE_1.47IN_172X320": model_spec(
|
||||
presets={
|
||||
CONF_HEIGHT: 320,
|
||||
CONF_WIDTH: 172,
|
||||
CONF_OFFSET_HEIGHT: 34,
|
||||
CONF_OFFSET_WIDTH: 0,
|
||||
CONF_ROTATION: 90,
|
||||
CONF_CS_PIN: "GPIO21",
|
||||
CONF_DC_PIN: "GPIO22",
|
||||
CONF_RESET_PIN: "GPIO23",
|
||||
CONF_BACKLIGHT_PIN: "GPIO4",
|
||||
}
|
||||
),
|
||||
"CUSTOM": model_spec(),
|
||||
}
|
||||
|
||||
|
||||
@ -12,11 +12,13 @@ from esphome.const import (
|
||||
)
|
||||
from .. import template_ns
|
||||
|
||||
CODEOWNERS = ["@grahambrown11"]
|
||||
CODEOWNERS = ["@grahambrown11", "@hwstar"]
|
||||
|
||||
CONF_CODES = "codes"
|
||||
CONF_BYPASS_ARMED_HOME = "bypass_armed_home"
|
||||
CONF_BYPASS_ARMED_NIGHT = "bypass_armed_night"
|
||||
CONF_CHIME = "chime"
|
||||
CONF_TRIGGER_MODE = "trigger_mode"
|
||||
CONF_REQUIRES_CODE_TO_ARM = "requires_code_to_arm"
|
||||
CONF_ARMING_HOME_TIME = "arming_home_time"
|
||||
CONF_ARMING_NIGHT_TIME = "arming_night_time"
|
||||
@ -24,16 +26,20 @@ CONF_ARMING_AWAY_TIME = "arming_away_time"
|
||||
CONF_PENDING_TIME = "pending_time"
|
||||
CONF_TRIGGER_TIME = "trigger_time"
|
||||
|
||||
|
||||
FLAG_NORMAL = "normal"
|
||||
FLAG_BYPASS_ARMED_HOME = "bypass_armed_home"
|
||||
FLAG_BYPASS_ARMED_NIGHT = "bypass_armed_night"
|
||||
FLAG_CHIME = "chime"
|
||||
|
||||
BinarySensorFlags = {
|
||||
FLAG_NORMAL: 1 << 0,
|
||||
FLAG_BYPASS_ARMED_HOME: 1 << 1,
|
||||
FLAG_BYPASS_ARMED_NIGHT: 1 << 2,
|
||||
FLAG_CHIME: 1 << 3,
|
||||
}
|
||||
|
||||
|
||||
TemplateAlarmControlPanel = template_ns.class_(
|
||||
"TemplateAlarmControlPanel", alarm_control_panel.AlarmControlPanel, cg.Component
|
||||
)
|
||||
@ -46,6 +52,14 @@ RESTORE_MODES = {
|
||||
"RESTORE_DEFAULT_DISARMED": TemplateAlarmControlPanelRestoreMode.ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED,
|
||||
}
|
||||
|
||||
AlarmSensorType = template_ns.enum("AlarmSensorType")
|
||||
|
||||
ALARM_SENSOR_TYPES = {
|
||||
"DELAYED": AlarmSensorType.ALARM_SENSOR_TYPE_DELAYED,
|
||||
"INSTANT": AlarmSensorType.ALARM_SENSOR_TYPE_INSTANT,
|
||||
"DELAYED_FOLLOWER": AlarmSensorType.ALARM_SENSOR_TYPE_DELAYED_FOLLOWER,
|
||||
}
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
if config.get(CONF_REQUIRES_CODE_TO_ARM, False) and not config.get(CONF_CODES, []):
|
||||
@ -60,6 +74,10 @@ TEMPLATE_ALARM_CONTROL_PANEL_BINARY_SENSOR_SCHEMA = cv.maybe_simple_value(
|
||||
cv.Required(CONF_INPUT): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional(CONF_BYPASS_ARMED_HOME, default=False): cv.boolean,
|
||||
cv.Optional(CONF_BYPASS_ARMED_NIGHT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_CHIME, default=False): cv.boolean,
|
||||
cv.Optional(CONF_TRIGGER_MODE, default="DELAYED"): cv.enum(
|
||||
ALARM_SENSOR_TYPES, upper=True, space="_"
|
||||
),
|
||||
},
|
||||
key=CONF_INPUT,
|
||||
)
|
||||
@ -123,6 +141,7 @@ async def to_code(config):
|
||||
|
||||
for sensor in config.get(CONF_BINARY_SENSORS, []):
|
||||
bs = await cg.get_variable(sensor[CONF_INPUT])
|
||||
|
||||
flags = BinarySensorFlags[FLAG_NORMAL]
|
||||
if sensor[CONF_BYPASS_ARMED_HOME]:
|
||||
flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_HOME]
|
||||
@ -130,7 +149,9 @@ async def to_code(config):
|
||||
if sensor[CONF_BYPASS_ARMED_NIGHT]:
|
||||
flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_NIGHT]
|
||||
supports_arm_night = True
|
||||
cg.add(var.add_sensor(bs, flags))
|
||||
if sensor[CONF_CHIME]:
|
||||
flags |= BinarySensorFlags[FLAG_CHIME]
|
||||
cg.add(var.add_sensor(bs, flags, sensor[CONF_TRIGGER_MODE]))
|
||||
|
||||
cg.add(var.set_supports_arm_home(supports_arm_home))
|
||||
cg.add(var.set_supports_arm_night(supports_arm_night))
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
|
||||
#include "template_alarm_control_panel.h"
|
||||
#include <utility>
|
||||
#include "esphome/components/alarm_control_panel/alarm_control_panel.h"
|
||||
@ -15,8 +16,14 @@ static const char *const TAG = "template.alarm_control_panel";
|
||||
TemplateAlarmControlPanel::TemplateAlarmControlPanel(){};
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags) {
|
||||
this->sensor_map_[sensor] = flags;
|
||||
void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags, AlarmSensorType type) {
|
||||
// Save the flags and type. Assign a store index for the per sensor data type.
|
||||
SensorDataStore sd;
|
||||
sd.last_chime_state = false;
|
||||
this->sensor_map_[sensor].flags = flags;
|
||||
this->sensor_map_[sensor].type = type;
|
||||
this->sensor_data_.push_back(sd);
|
||||
this->sensor_map_[sensor].store_index = this->next_store_index_++;
|
||||
};
|
||||
#endif
|
||||
|
||||
@ -35,13 +42,27 @@ void TemplateAlarmControlPanel::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Trigger Time: %" PRIu32 "s", (this->trigger_time_ / 1000));
|
||||
ESP_LOGCONFIG(TAG, " Supported Features: %" PRIu32, this->get_supported_features());
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
for (auto sensor_pair : this->sensor_map_) {
|
||||
ESP_LOGCONFIG(TAG, " Binary Sesnsor:");
|
||||
ESP_LOGCONFIG(TAG, " Name: %s", sensor_pair.first->get_name().c_str());
|
||||
for (auto sensor_info : this->sensor_map_) {
|
||||
ESP_LOGCONFIG(TAG, " Binary Sensor:");
|
||||
ESP_LOGCONFIG(TAG, " Name: %s", sensor_info.first->get_name().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Armed home bypass: %s",
|
||||
TRUEFALSE(sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME));
|
||||
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME));
|
||||
ESP_LOGCONFIG(TAG, " Armed night bypass: %s",
|
||||
TRUEFALSE(sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT));
|
||||
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT));
|
||||
ESP_LOGCONFIG(TAG, " Chime mode: %s", TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME));
|
||||
const char *sensor_type;
|
||||
switch (sensor_info.second.type) {
|
||||
case ALARM_SENSOR_TYPE_INSTANT:
|
||||
sensor_type = "instant";
|
||||
break;
|
||||
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
|
||||
sensor_type = "delayed_follower";
|
||||
break;
|
||||
case ALARM_SENSOR_TYPE_DELAYED:
|
||||
default:
|
||||
sensor_type = "delayed";
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Sensor type: %s", sensor_type);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@ -92,31 +113,80 @@ void TemplateAlarmControlPanel::loop() {
|
||||
(millis() - this->last_update_) > this->trigger_time_) {
|
||||
future_state = this->desired_state_;
|
||||
}
|
||||
bool trigger = false;
|
||||
|
||||
bool delayed_sensor_not_ready = false;
|
||||
bool instant_sensor_not_ready = false;
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->is_state_armed(future_state)) {
|
||||
// TODO might be better to register change for each sensor in setup...
|
||||
for (auto sensor_pair : this->sensor_map_) {
|
||||
if (sensor_pair.first->state) {
|
||||
if (this->current_state_ == ACP_STATE_ARMED_HOME &&
|
||||
(sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) {
|
||||
continue;
|
||||
// Test all of the sensors in the list regardless of the alarm panel state
|
||||
for (auto sensor_info : this->sensor_map_) {
|
||||
// Check for chime zones
|
||||
if ((sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)) {
|
||||
// Look for the transition from closed to open
|
||||
if ((!this->sensor_data_[sensor_info.second.store_index].last_chime_state) && (sensor_info.first->state)) {
|
||||
// Must be disarmed to chime
|
||||
if (this->current_state_ == ACP_STATE_DISARMED) {
|
||||
this->chime_callback_.call();
|
||||
}
|
||||
if (this->current_state_ == ACP_STATE_ARMED_NIGHT &&
|
||||
(sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) {
|
||||
continue;
|
||||
}
|
||||
// Record the sensor state change
|
||||
this->sensor_data_[sensor_info.second.store_index].last_chime_state = sensor_info.first->state;
|
||||
}
|
||||
// Check for triggered sensors
|
||||
if (sensor_info.first->state) { // Sensor triggered?
|
||||
// Skip if bypass armed home
|
||||
if (this->current_state_ == ACP_STATE_ARMED_HOME &&
|
||||
(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) {
|
||||
continue;
|
||||
}
|
||||
// Skip if bypass armed night
|
||||
if (this->current_state_ == ACP_STATE_ARMED_NIGHT &&
|
||||
(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If sensor type is of type instant
|
||||
if (sensor_info.second.type == ALARM_SENSOR_TYPE_INSTANT) {
|
||||
instant_sensor_not_ready = true;
|
||||
break;
|
||||
}
|
||||
// If sensor type is of type interior follower
|
||||
if (sensor_info.second.type == ALARM_SENSOR_TYPE_DELAYED_FOLLOWER) {
|
||||
// Look to see if we are in the pending state
|
||||
if (this->current_state_ == ACP_STATE_PENDING) {
|
||||
delayed_sensor_not_ready = true;
|
||||
} else {
|
||||
instant_sensor_not_ready = true;
|
||||
}
|
||||
trigger = true;
|
||||
}
|
||||
// If sensor type is of type delayed
|
||||
if (sensor_info.second.type == ALARM_SENSOR_TYPE_DELAYED) {
|
||||
delayed_sensor_not_ready = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update all sensors not ready flag
|
||||
this->sensors_ready_ = ((!instant_sensor_not_ready) && (!delayed_sensor_not_ready));
|
||||
|
||||
// Call the ready state change callback if there was a change
|
||||
if (this->sensors_ready_ != this->sensors_ready_last_) {
|
||||
this->ready_callback_.call();
|
||||
this->sensors_ready_last_ = this->sensors_ready_;
|
||||
}
|
||||
|
||||
#endif
|
||||
if (trigger) {
|
||||
if (this->pending_time_ > 0 && this->current_state_ != ACP_STATE_TRIGGERED) {
|
||||
this->publish_state(ACP_STATE_PENDING);
|
||||
} else {
|
||||
if (this->is_state_armed(future_state) && (!this->sensors_ready_)) {
|
||||
// Instant sensors
|
||||
if (instant_sensor_not_ready) {
|
||||
this->publish_state(ACP_STATE_TRIGGERED);
|
||||
} else if (delayed_sensor_not_ready) {
|
||||
// Delayed sensors
|
||||
if ((this->pending_time_ > 0) && (this->current_state_ != ACP_STATE_TRIGGERED)) {
|
||||
this->publish_state(ACP_STATE_PENDING);
|
||||
} else {
|
||||
this->publish_state(ACP_STATE_TRIGGERED);
|
||||
}
|
||||
}
|
||||
} else if (future_state != this->current_state_) {
|
||||
this->publish_state(future_state);
|
||||
|
||||
@ -21,7 +21,15 @@ enum BinarySensorFlags : uint16_t {
|
||||
BINARY_SENSOR_MODE_NORMAL = 1 << 0,
|
||||
BINARY_SENSOR_MODE_BYPASS_ARMED_HOME = 1 << 1,
|
||||
BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT = 1 << 2,
|
||||
BINARY_SENSOR_MODE_CHIME = 1 << 3,
|
||||
};
|
||||
|
||||
enum AlarmSensorType : uint16_t {
|
||||
ALARM_SENSOR_TYPE_DELAYED = 0,
|
||||
ALARM_SENSOR_TYPE_INSTANT,
|
||||
ALARM_SENSOR_TYPE_DELAYED_FOLLOWER
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
enum TemplateAlarmControlPanelRestoreMode {
|
||||
@ -29,6 +37,16 @@ enum TemplateAlarmControlPanelRestoreMode {
|
||||
ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED,
|
||||
};
|
||||
|
||||
struct SensorDataStore {
|
||||
bool last_chime_state;
|
||||
};
|
||||
|
||||
struct SensorInfo {
|
||||
uint16_t flags;
|
||||
AlarmSensorType type;
|
||||
uint8_t store_index;
|
||||
};
|
||||
|
||||
class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, public Component {
|
||||
public:
|
||||
TemplateAlarmControlPanel();
|
||||
@ -38,6 +56,7 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
|
||||
uint32_t get_supported_features() const override;
|
||||
bool get_requires_code() const override;
|
||||
bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; }
|
||||
bool get_all_sensors_ready() { return this->sensors_ready_; };
|
||||
void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@ -46,7 +65,8 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
|
||||
* @param sensor The BinarySensor instance.
|
||||
* @param ignore_when_home if this should be ignored when armed_home mode
|
||||
*/
|
||||
void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0);
|
||||
void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0,
|
||||
AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED);
|
||||
#endif
|
||||
|
||||
/** add a code
|
||||
@ -98,8 +118,9 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
|
||||
protected:
|
||||
void control(const alarm_control_panel::AlarmControlPanelCall &call) override;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// the map of binary sensors that the alarm_panel monitors with their modes
|
||||
std::map<binary_sensor::BinarySensor *, uint16_t> sensor_map_;
|
||||
// This maps a binary sensor to its type and attribute bits
|
||||
std::map<binary_sensor::BinarySensor *, SensorInfo> sensor_map_;
|
||||
|
||||
#endif
|
||||
TemplateAlarmControlPanelRestoreMode restore_mode_{};
|
||||
|
||||
@ -115,10 +136,15 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
|
||||
uint32_t trigger_time_;
|
||||
// a list of codes
|
||||
std::vector<std::string> codes_;
|
||||
// Per sensor data store
|
||||
std::vector<SensorDataStore> sensor_data_;
|
||||
// requires a code to arm
|
||||
bool requires_code_to_arm_ = false;
|
||||
bool supports_arm_home_ = false;
|
||||
bool supports_arm_night_ = false;
|
||||
bool sensors_ready_ = false;
|
||||
bool sensors_ready_last_ = false;
|
||||
uint8_t next_store_index_ = 0;
|
||||
// check if the code is valid
|
||||
bool is_code_valid_(optional<std::string> code);
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ class Filter {
|
||||
* @param value The new value.
|
||||
* @return An optional string, the new value that should be pushed out.
|
||||
*/
|
||||
virtual optional<std::string> new_value(std::string value);
|
||||
virtual optional<std::string> new_value(std::string value) = 0;
|
||||
|
||||
/// Initialize this filter, please note this can be called more than once.
|
||||
virtual void initialize(TextSensor *parent, Filter *next);
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
#include "real_time_clock.h"
|
||||
#include "esphome/core/log.h"
|
||||
#ifdef USE_HOST
|
||||
#include <sys/time.h>
|
||||
#else
|
||||
#include "lwip/opt.h"
|
||||
#endif
|
||||
#ifdef USE_ESP8266
|
||||
#include "sys/time.h"
|
||||
#endif
|
||||
@ -25,7 +29,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
.tv_sec = static_cast<time_t>(epoch), .tv_usec = 0,
|
||||
};
|
||||
ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch);
|
||||
timezone tz = {0, 0};
|
||||
struct timezone tz = {0, 0};
|
||||
int ret = settimeofday(&timev, &tz);
|
||||
if (ret == EINVAL) {
|
||||
// Some ESP8266 frameworks abort when timezone parameter is not NULL
|
||||
|
||||
@ -3,7 +3,14 @@ import esphome.codegen as cg
|
||||
|
||||
from esphome.components import display
|
||||
from esphome import automation
|
||||
from esphome.const import CONF_ON_TOUCH, CONF_ON_RELEASE
|
||||
from esphome.const import (
|
||||
CONF_ON_TOUCH,
|
||||
CONF_ON_RELEASE,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_SWAP_XY,
|
||||
CONF_TRANSFORM,
|
||||
)
|
||||
from esphome.core import coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@jesserockz", "@nielsnl68"]
|
||||
@ -24,28 +31,32 @@ CONF_DISPLAY = "display"
|
||||
CONF_TOUCHSCREEN_ID = "touchscreen_id"
|
||||
CONF_REPORT_INTERVAL = "report_interval" # not used yet:
|
||||
CONF_ON_UPDATE = "on_update"
|
||||
|
||||
CONF_MIRROR_X = "mirror_x"
|
||||
CONF_MIRROR_Y = "mirror_y"
|
||||
CONF_SWAP_XY = "swap_xy"
|
||||
CONF_TRANSFORM = "transform"
|
||||
CONF_TOUCH_TIMEOUT = "touch_timeout"
|
||||
|
||||
|
||||
TOUCHSCREEN_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_DISPLAY): cv.use_id(display.Display),
|
||||
cv.Optional(CONF_TRANSFORM): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SWAP_XY, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MIRROR_X, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True),
|
||||
}
|
||||
).extend(cv.polling_component_schema("50ms"))
|
||||
def touchscreen_schema(default_touch_timeout):
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_DISPLAY): cv.use_id(display.Display),
|
||||
cv.Optional(CONF_TRANSFORM): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SWAP_XY, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MIRROR_X, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_TOUCH_TIMEOUT, default=default_touch_timeout): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
||||
),
|
||||
cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True),
|
||||
}
|
||||
).extend(cv.polling_component_schema("50ms"))
|
||||
|
||||
|
||||
TOUCHSCREEN_SCHEMA = touchscreen_schema(cv.UNDEFINED)
|
||||
|
||||
|
||||
async def register_touchscreen(var, config):
|
||||
@ -54,6 +65,9 @@ async def register_touchscreen(var, config):
|
||||
disp = await cg.get_variable(config[CONF_DISPLAY])
|
||||
cg.add(var.set_display(disp))
|
||||
|
||||
if CONF_TOUCH_TIMEOUT in config:
|
||||
cg.add(var.set_touch_timeout(config[CONF_TOUCH_TIMEOUT]))
|
||||
|
||||
if CONF_TRANSFORM in config:
|
||||
transform = config[CONF_TRANSFORM]
|
||||
cg.add(var.set_swap_xy(transform[CONF_SWAP_XY]))
|
||||
|
||||
@ -47,6 +47,11 @@ void Touchscreen::loop() {
|
||||
} else {
|
||||
this->store_.touched = false;
|
||||
this->defer([this]() { this->send_touches_(); });
|
||||
if (this->touch_timeout_ > 0) {
|
||||
// Simulate a touch after <this->touch_timeout_> ms. This will reset any existing timeout operation.
|
||||
// This is to detect touch release.
|
||||
this->set_timeout(TAG, this->touch_timeout_, [this]() { this->store_.touched = true; });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -90,6 +95,9 @@ void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_r
|
||||
|
||||
void Touchscreen::send_touches_() {
|
||||
if (!this->is_touched_) {
|
||||
if (this->touch_timeout_ > 0) {
|
||||
this->cancel_timeout(TAG);
|
||||
}
|
||||
this->release_trigger_.trigger();
|
||||
for (auto *listener : this->touch_listeners_)
|
||||
listener->release();
|
||||
|
||||
@ -46,6 +46,7 @@ class Touchscreen : public PollingComponent {
|
||||
void set_display(display::Display *display) { this->display_ = display; }
|
||||
display::Display *get_display() const { return this->display_; }
|
||||
|
||||
void set_touch_timeout(uint16_t val) { this->touch_timeout_ = val; }
|
||||
void set_mirror_x(bool invert_x) { this->invert_x_ = invert_x; }
|
||||
void set_mirror_y(bool invert_y) { this->invert_y_ = invert_y; }
|
||||
void set_swap_xy(bool swap) { this->swap_x_y_ = swap; }
|
||||
@ -100,6 +101,7 @@ class Touchscreen : public PollingComponent {
|
||||
display::Display *display_{nullptr};
|
||||
|
||||
int16_t x_raw_min_{0}, x_raw_max_{0}, y_raw_min_{0}, y_raw_max_{0};
|
||||
uint16_t touch_timeout_{0};
|
||||
bool invert_x_{false}, invert_y_{false}, swap_x_y_{false};
|
||||
|
||||
Trigger<TouchPoint, const TouchPoints_t &> touch_trigger_;
|
||||
|
||||
@ -23,8 +23,8 @@ static const int MAX_RETRIES = 5;
|
||||
|
||||
void Tuya::setup() {
|
||||
this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
|
||||
if (this->status_pin_.has_value()) {
|
||||
this->status_pin_.value()->digital_write(false);
|
||||
if (this->status_pin_ != nullptr) {
|
||||
this->status_pin_->digital_write(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,9 +70,7 @@ void Tuya::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d", this->status_pin_reported_,
|
||||
this->reset_pin_reported_);
|
||||
}
|
||||
if (this->status_pin_.has_value()) {
|
||||
LOG_PIN(" Status Pin: ", this->status_pin_.value());
|
||||
}
|
||||
LOG_PIN(" Status Pin: ", this->status_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str());
|
||||
}
|
||||
|
||||
@ -194,7 +192,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
|
||||
this->init_state_ = TuyaInitState::INIT_DATAPOINT;
|
||||
this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY);
|
||||
bool is_pin_equals =
|
||||
this->status_pin_.has_value() && this->status_pin_.value()->get_pin() == this->status_pin_reported_;
|
||||
this->status_pin_ != nullptr && this->status_pin_->get_pin() == this->status_pin_reported_;
|
||||
// Configure status pin toggling (if reported and configured) or WIFI_STATE periodic send
|
||||
if (is_pin_equals) {
|
||||
ESP_LOGV(TAG, "Configured status pin %i", this->status_pin_reported_);
|
||||
@ -244,13 +242,12 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
|
||||
break;
|
||||
case TuyaCommandType::LOCAL_TIME_QUERY:
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_.has_value()) {
|
||||
if (this->time_id_ != nullptr) {
|
||||
this->send_local_time_();
|
||||
|
||||
if (!this->time_sync_callback_registered_) {
|
||||
// tuya mcu supports time, so we let them know when our time changed
|
||||
auto *time_id = *this->time_id_;
|
||||
time_id->add_on_time_sync_callback([this] { this->send_local_time_(); });
|
||||
this->time_id_->add_on_time_sync_callback([this] { this->send_local_time_(); });
|
||||
this->time_sync_callback_registered_ = true;
|
||||
}
|
||||
} else
|
||||
@ -463,7 +460,7 @@ void Tuya::send_empty_command_(TuyaCommandType command) {
|
||||
|
||||
void Tuya::set_status_pin_() {
|
||||
bool is_network_ready = network::is_connected() && remote_is_connected();
|
||||
this->status_pin_.value()->digital_write(is_network_ready);
|
||||
this->status_pin_->digital_write(is_network_ready);
|
||||
}
|
||||
|
||||
uint8_t Tuya::get_wifi_status_code_() {
|
||||
@ -511,8 +508,7 @@ void Tuya::send_wifi_status_() {
|
||||
#ifdef USE_TIME
|
||||
void Tuya::send_local_time_() {
|
||||
std::vector<uint8_t> payload;
|
||||
auto *time_id = *this->time_id_;
|
||||
ESPTime now = time_id->now();
|
||||
ESPTime now = this->time_id_->now();
|
||||
if (now.is_valid()) {
|
||||
uint8_t year = now.year - 2000;
|
||||
uint8_t month = now.month;
|
||||
|
||||
@ -130,14 +130,14 @@ class Tuya : public Component, public uart::UARTDevice {
|
||||
|
||||
#ifdef USE_TIME
|
||||
void send_local_time_();
|
||||
optional<time::RealTimeClock *> time_id_{};
|
||||
time::RealTimeClock *time_id_{nullptr};
|
||||
bool time_sync_callback_registered_{false};
|
||||
#endif
|
||||
TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT;
|
||||
bool init_failed_{false};
|
||||
int init_retries_{0};
|
||||
uint8_t protocol_version_ = -1;
|
||||
optional<InternalGPIOPin *> status_pin_{};
|
||||
InternalGPIOPin *status_pin_{nullptr};
|
||||
int status_pin_reported_ = -1;
|
||||
int reset_pin_reported_ = -1;
|
||||
uint32_t last_command_timestamp_ = 0;
|
||||
|
||||
@ -122,7 +122,7 @@ class UARTComponent {
|
||||
// @return Baud rate in bits per second.
|
||||
uint32_t get_baud_rate() const { return baud_rate_; }
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
/**
|
||||
* Load the UART settings.
|
||||
* @param dump_config If true (default), output the new settings to logs; otherwise, change settings quietly.
|
||||
@ -147,7 +147,7 @@ class UARTComponent {
|
||||
* This will load the current UART interface with the latest settings (baud_rate, parity, etc).
|
||||
*/
|
||||
virtual void load_settings(){};
|
||||
#endif // USE_ESP32
|
||||
#endif // USE_ESP8266 || USE_ESP32
|
||||
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
void add_debug_callback(std::function<void(UARTDirection, uint8_t)> &&callback) {
|
||||
|
||||
@ -98,10 +98,26 @@ void ESP8266UartComponent::setup() {
|
||||
}
|
||||
}
|
||||
|
||||
void ESP8266UartComponent::load_settings(bool dump_config) {
|
||||
ESP_LOGCONFIG(TAG, "Loading UART bus settings...");
|
||||
if (this->hw_serial_ != nullptr) {
|
||||
SerialConfig config = static_cast<SerialConfig>(get_config());
|
||||
this->hw_serial_->begin(this->baud_rate_, config);
|
||||
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
|
||||
} else {
|
||||
this->sw_serial_->setup(this->tx_pin_, this->rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_,
|
||||
this->parity_, this->rx_buffer_size_);
|
||||
}
|
||||
if (dump_config) {
|
||||
ESP_LOGCONFIG(TAG, "UART bus was reloaded.");
|
||||
this->dump_config();
|
||||
}
|
||||
}
|
||||
|
||||
void ESP8266UartComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "UART Bus:");
|
||||
LOG_PIN(" TX Pin: ", tx_pin_);
|
||||
LOG_PIN(" RX Pin: ", rx_pin_);
|
||||
LOG_PIN(" TX Pin: ", this->tx_pin_);
|
||||
LOG_PIN(" RX Pin: ", this->rx_pin_);
|
||||
if (this->rx_pin_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT
|
||||
}
|
||||
|
||||
@ -63,6 +63,21 @@ class ESP8266UartComponent : public UARTComponent, public Component {
|
||||
|
||||
uint32_t get_config();
|
||||
|
||||
/**
|
||||
* Load the UART with the current settings.
|
||||
* @param dump_config (Optional, default `true`): True for displaying new settings or
|
||||
* false to change it quitely
|
||||
*
|
||||
* Example:
|
||||
* ```cpp
|
||||
* id(uart1).load_settings();
|
||||
* ```
|
||||
*
|
||||
* This will load the current UART interface with the latest settings (baud_rate, parity, etc).
|
||||
*/
|
||||
void load_settings(bool dump_config) override;
|
||||
void load_settings() override { this->load_settings(true); }
|
||||
|
||||
protected:
|
||||
void check_logger_conflict() override;
|
||||
|
||||
|
||||
0
esphome/components/veml3235/__init__.py
Normal file
0
esphome/components/veml3235/__init__.py
Normal file
84
esphome/components/veml3235/sensor.py
Normal file
84
esphome/components/veml3235/sensor.py
Normal file
@ -0,0 +1,84 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import (
|
||||
CONF_GAIN,
|
||||
CONF_INTEGRATION_TIME,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_LUX,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
CONF_AUTO_GAIN = "auto_gain"
|
||||
CONF_AUTO_GAIN_THRESHOLD_HIGH = "auto_gain_threshold_high"
|
||||
CONF_AUTO_GAIN_THRESHOLD_LOW = "auto_gain_threshold_low"
|
||||
CONF_DIGITAL_GAIN = "digital_gain"
|
||||
|
||||
veml3235_ns = cg.esphome_ns.namespace("veml3235")
|
||||
|
||||
VEML3235Sensor = veml3235_ns.class_(
|
||||
"VEML3235Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
VEML3235IntegrationTime = veml3235_ns.enum("VEML3235IntegrationTime")
|
||||
VEML3235_INTEGRATION_TIMES = {
|
||||
"50ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_50MS,
|
||||
"100ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_100MS,
|
||||
"200ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_200MS,
|
||||
"400ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_400MS,
|
||||
"800ms": VEML3235IntegrationTime.VEML3235_INTEGRATION_TIME_800MS,
|
||||
}
|
||||
VEML3235ComponentDigitalGain = veml3235_ns.enum("VEML3235ComponentDigitalGain")
|
||||
DIGITAL_GAINS = {
|
||||
"1X": VEML3235ComponentDigitalGain.VEML3235_DIGITAL_GAIN_1X,
|
||||
"2X": VEML3235ComponentDigitalGain.VEML3235_DIGITAL_GAIN_2X,
|
||||
}
|
||||
VEML3235ComponentGain = veml3235_ns.enum("VEML3235ComponentGain")
|
||||
GAINS = {
|
||||
"1X": VEML3235ComponentGain.VEML3235_GAIN_1X,
|
||||
"2X": VEML3235ComponentGain.VEML3235_GAIN_2X,
|
||||
"4X": VEML3235ComponentGain.VEML3235_GAIN_4X,
|
||||
"AUTO": VEML3235ComponentGain.VEML3235_GAIN_AUTO,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
VEML3235Sensor,
|
||||
unit_of_measurement=UNIT_LUX,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_DIGITAL_GAIN, default="1X"): cv.enum(
|
||||
DIGITAL_GAINS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_AUTO_GAIN, default=True): cv.boolean,
|
||||
cv.Optional(CONF_AUTO_GAIN_THRESHOLD_HIGH, default="90%"): cv.percentage,
|
||||
cv.Optional(CONF_AUTO_GAIN_THRESHOLD_LOW, default="20%"): cv.percentage,
|
||||
cv.Optional(CONF_GAIN, default="1X"): cv.enum(GAINS, upper=True),
|
||||
cv.Optional(CONF_INTEGRATION_TIME, default="50ms"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.enum(VEML3235_INTEGRATION_TIMES, lower=True),
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x10))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_auto_gain(config[CONF_AUTO_GAIN]))
|
||||
cg.add(var.set_auto_gain_threshold_high(config[CONF_AUTO_GAIN_THRESHOLD_HIGH]))
|
||||
cg.add(var.set_auto_gain_threshold_low(config[CONF_AUTO_GAIN_THRESHOLD_LOW]))
|
||||
cg.add(var.set_digital_gain(DIGITAL_GAINS[config[CONF_DIGITAL_GAIN]]))
|
||||
cg.add(var.set_gain(GAINS[config[CONF_GAIN]]))
|
||||
cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME]))
|
||||
230
esphome/components/veml3235/veml3235.cpp
Normal file
230
esphome/components/veml3235/veml3235.cpp
Normal file
@ -0,0 +1,230 @@
|
||||
#include "veml3235.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace veml3235 {
|
||||
|
||||
static const char *const TAG = "veml3235.sensor";
|
||||
|
||||
void VEML3235Sensor::setup() {
|
||||
uint8_t device_id[] = {0, 0};
|
||||
|
||||
ESP_LOGCONFIG(TAG, "Setting up VEML3235 '%s'...", this->name_.c_str());
|
||||
|
||||
if (!this->refresh_config_reg()) {
|
||||
ESP_LOGE(TAG, "Unable to write configuration");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if ((this->write(&ID_REG, 1, false) != i2c::ERROR_OK) || !this->read_bytes_raw(device_id, 2)) {
|
||||
ESP_LOGE(TAG, "Unable to read ID");
|
||||
this->mark_failed();
|
||||
return;
|
||||
} else if (device_id[0] != DEVICE_ID) {
|
||||
ESP_LOGE(TAG, "Incorrect device ID - expected 0x%.2x, read 0x%.2x", DEVICE_ID, device_id[0]);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool VEML3235Sensor::refresh_config_reg(bool force_on) {
|
||||
uint16_t data = this->power_on_ || force_on ? 0 : SHUTDOWN_BITS;
|
||||
|
||||
data |= (uint16_t(this->integration_time_ << CONFIG_REG_IT_BIT));
|
||||
data |= (uint16_t(this->digital_gain_ << CONFIG_REG_DG_BIT));
|
||||
data |= (uint16_t(this->gain_ << CONFIG_REG_G_BIT));
|
||||
data |= 0x1; // mandatory 1 here per RM
|
||||
|
||||
ESP_LOGVV(TAG, "Writing 0x%.4x to register 0x%.2x", data, CONFIG_REG);
|
||||
return this->write_byte_16(CONFIG_REG, data);
|
||||
}
|
||||
|
||||
float VEML3235Sensor::read_lx_() {
|
||||
if (!this->power_on_) { // if off, turn on
|
||||
if (!this->refresh_config_reg(true)) {
|
||||
ESP_LOGW(TAG, "Turning on failed");
|
||||
this->status_set_warning();
|
||||
return NAN;
|
||||
}
|
||||
delay(4); // from RM: a wait time of 4 ms should be observed before the first measurement is picked up, to allow
|
||||
// for a correct start of the signal processor and oscillator
|
||||
}
|
||||
|
||||
uint8_t als_regs[] = {0, 0};
|
||||
if ((this->write(&ALS_REG, 1, false) != i2c::ERROR_OK) || !this->read_bytes_raw(als_regs, 2)) {
|
||||
this->status_set_warning();
|
||||
return NAN;
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
|
||||
float als_raw_value_multiplier = LUX_MULTIPLIER_BASE;
|
||||
uint16_t als_raw_value = encode_uint16(als_regs[1], als_regs[0]);
|
||||
// determine multiplier value based on gains and integration time
|
||||
if (this->digital_gain_ == VEML3235_DIGITAL_GAIN_1X) {
|
||||
als_raw_value_multiplier *= 2;
|
||||
}
|
||||
switch (this->gain_) {
|
||||
case VEML3235_GAIN_1X:
|
||||
als_raw_value_multiplier *= 4;
|
||||
break;
|
||||
case VEML3235_GAIN_2X:
|
||||
als_raw_value_multiplier *= 2;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (this->integration_time_) {
|
||||
case VEML3235_INTEGRATION_TIME_50MS:
|
||||
als_raw_value_multiplier *= 16;
|
||||
break;
|
||||
case VEML3235_INTEGRATION_TIME_100MS:
|
||||
als_raw_value_multiplier *= 8;
|
||||
break;
|
||||
case VEML3235_INTEGRATION_TIME_200MS:
|
||||
als_raw_value_multiplier *= 4;
|
||||
break;
|
||||
case VEML3235_INTEGRATION_TIME_400MS:
|
||||
als_raw_value_multiplier *= 2;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// finally, determine and return the actual lux value
|
||||
float lx = float(als_raw_value) * als_raw_value_multiplier;
|
||||
ESP_LOGVV(TAG, "'%s': ALS raw = %u, multiplier = %.5f", this->get_name().c_str(), als_raw_value,
|
||||
als_raw_value_multiplier);
|
||||
ESP_LOGD(TAG, "'%s': Illuminance = %.4flx", this->get_name().c_str(), lx);
|
||||
|
||||
if (!this->power_on_) { // turn off if required
|
||||
if (!this->refresh_config_reg()) {
|
||||
ESP_LOGW(TAG, "Turning off failed");
|
||||
this->status_set_warning();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->auto_gain_) {
|
||||
this->adjust_gain_(als_raw_value);
|
||||
}
|
||||
|
||||
return lx;
|
||||
}
|
||||
|
||||
void VEML3235Sensor::adjust_gain_(const uint16_t als_raw_value) {
|
||||
if ((als_raw_value > UINT16_MAX * this->auto_gain_threshold_low_) &&
|
||||
(als_raw_value < UINT16_MAX * this->auto_gain_threshold_high_)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (als_raw_value >= UINT16_MAX * 0.9) { // over-saturated, reset all gains and start over
|
||||
this->digital_gain_ = VEML3235_DIGITAL_GAIN_1X;
|
||||
this->gain_ = VEML3235_GAIN_1X;
|
||||
this->integration_time_ = VEML3235_INTEGRATION_TIME_50MS;
|
||||
this->refresh_config_reg();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->gain_ != VEML3235_GAIN_4X) { // increase gain if possible
|
||||
switch (this->gain_) {
|
||||
case VEML3235_GAIN_1X:
|
||||
this->gain_ = VEML3235_GAIN_2X;
|
||||
break;
|
||||
case VEML3235_GAIN_2X:
|
||||
this->gain_ = VEML3235_GAIN_4X;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this->refresh_config_reg();
|
||||
return;
|
||||
}
|
||||
// gain is maxed out; reset it and try to increase digital gain
|
||||
if (this->digital_gain_ != VEML3235_DIGITAL_GAIN_2X) { // increase digital gain if possible
|
||||
this->digital_gain_ = VEML3235_DIGITAL_GAIN_2X;
|
||||
this->gain_ = VEML3235_GAIN_1X;
|
||||
this->refresh_config_reg();
|
||||
return;
|
||||
}
|
||||
// digital gain is maxed out; reset it and try to increase integration time
|
||||
if (this->integration_time_ != VEML3235_INTEGRATION_TIME_800MS) { // increase integration time if possible
|
||||
switch (this->integration_time_) {
|
||||
case VEML3235_INTEGRATION_TIME_50MS:
|
||||
this->integration_time_ = VEML3235_INTEGRATION_TIME_100MS;
|
||||
break;
|
||||
case VEML3235_INTEGRATION_TIME_100MS:
|
||||
this->integration_time_ = VEML3235_INTEGRATION_TIME_200MS;
|
||||
break;
|
||||
case VEML3235_INTEGRATION_TIME_200MS:
|
||||
this->integration_time_ = VEML3235_INTEGRATION_TIME_400MS;
|
||||
break;
|
||||
case VEML3235_INTEGRATION_TIME_400MS:
|
||||
this->integration_time_ = VEML3235_INTEGRATION_TIME_800MS;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this->digital_gain_ = VEML3235_DIGITAL_GAIN_1X;
|
||||
this->gain_ = VEML3235_GAIN_1X;
|
||||
this->refresh_config_reg();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void VEML3235Sensor::dump_config() {
|
||||
uint8_t digital_gain = 1;
|
||||
uint8_t gain = 1;
|
||||
uint16_t integration_time = 0;
|
||||
|
||||
if (this->digital_gain_ == VEML3235_DIGITAL_GAIN_2X) {
|
||||
digital_gain = 2;
|
||||
}
|
||||
switch (this->gain_) {
|
||||
case VEML3235_GAIN_2X:
|
||||
gain = 2;
|
||||
break;
|
||||
case VEML3235_GAIN_4X:
|
||||
gain = 4;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (this->integration_time_) {
|
||||
case VEML3235_INTEGRATION_TIME_50MS:
|
||||
integration_time = 50;
|
||||
break;
|
||||
case VEML3235_INTEGRATION_TIME_100MS:
|
||||
integration_time = 100;
|
||||
break;
|
||||
case VEML3235_INTEGRATION_TIME_200MS:
|
||||
integration_time = 200;
|
||||
break;
|
||||
case VEML3235_INTEGRATION_TIME_400MS:
|
||||
integration_time = 400;
|
||||
break;
|
||||
case VEML3235_INTEGRATION_TIME_800MS:
|
||||
integration_time = 800;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_SENSOR("", "VEML3235", this);
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication failed");
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
ESP_LOGCONFIG(TAG, " Auto-gain enabled: %s", YESNO(this->auto_gain_));
|
||||
if (this->auto_gain_) {
|
||||
ESP_LOGCONFIG(TAG, " Auto-gain upper threshold: %f%%", this->auto_gain_threshold_high_ * 100.0);
|
||||
ESP_LOGCONFIG(TAG, " Auto-gain lower threshold: %f%%", this->auto_gain_threshold_low_ * 100.0);
|
||||
ESP_LOGCONFIG(TAG, " Values below will be used as initial values only");
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Digital gain: %uX", digital_gain);
|
||||
ESP_LOGCONFIG(TAG, " Gain: %uX", gain);
|
||||
ESP_LOGCONFIG(TAG, " Integration time: %ums", integration_time);
|
||||
}
|
||||
|
||||
} // namespace veml3235
|
||||
} // namespace esphome
|
||||
109
esphome/components/veml3235/veml3235.h
Normal file
109
esphome/components/veml3235/veml3235.h
Normal file
@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace veml3235 {
|
||||
|
||||
// Register IDs/locations
|
||||
//
|
||||
static const uint8_t CONFIG_REG = 0x00;
|
||||
static const uint8_t W_REG = 0x04;
|
||||
static const uint8_t ALS_REG = 0x05;
|
||||
static const uint8_t ID_REG = 0x09;
|
||||
|
||||
// Bit offsets within CONFIG_REG
|
||||
//
|
||||
static const uint8_t CONFIG_REG_IT_BIT = 12;
|
||||
static const uint8_t CONFIG_REG_DG_BIT = 5;
|
||||
static const uint8_t CONFIG_REG_G_BIT = 3;
|
||||
|
||||
// Other important constants
|
||||
//
|
||||
static const uint8_t DEVICE_ID = 0x35;
|
||||
static const uint16_t SHUTDOWN_BITS = 0x0018;
|
||||
|
||||
// Base multiplier value for lux computation
|
||||
//
|
||||
static const float LUX_MULTIPLIER_BASE = 0.00213;
|
||||
|
||||
// Enum for conversion/integration time settings for the VEML3235.
|
||||
//
|
||||
// Specific values of the enum constants are register values taken from the VEML3235 datasheet.
|
||||
// Longer times mean more accurate results, but will take more energy/more time.
|
||||
//
|
||||
enum VEML3235ComponentIntegrationTime {
|
||||
VEML3235_INTEGRATION_TIME_50MS = 0b000,
|
||||
VEML3235_INTEGRATION_TIME_100MS = 0b001,
|
||||
VEML3235_INTEGRATION_TIME_200MS = 0b010,
|
||||
VEML3235_INTEGRATION_TIME_400MS = 0b011,
|
||||
VEML3235_INTEGRATION_TIME_800MS = 0b100,
|
||||
};
|
||||
|
||||
// Enum for digital gain settings for the VEML3235.
|
||||
// Higher values are better for low light situations, but can increase noise.
|
||||
//
|
||||
enum VEML3235ComponentDigitalGain {
|
||||
VEML3235_DIGITAL_GAIN_1X = 0b0,
|
||||
VEML3235_DIGITAL_GAIN_2X = 0b1,
|
||||
};
|
||||
|
||||
// Enum for gain settings for the VEML3235.
|
||||
// Higher values are better for low light situations, but can increase noise.
|
||||
//
|
||||
enum VEML3235ComponentGain {
|
||||
VEML3235_GAIN_1X = 0b00,
|
||||
VEML3235_GAIN_2X = 0b01,
|
||||
VEML3235_GAIN_4X = 0b11,
|
||||
};
|
||||
|
||||
class VEML3235Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override { this->publish_state(this->read_lx_()); }
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
// Used by ESPHome framework. Does NOT actually set the value on the device.
|
||||
void set_auto_gain(bool auto_gain) { this->auto_gain_ = auto_gain; }
|
||||
void set_auto_gain_threshold_high(float auto_gain_threshold_high) {
|
||||
this->auto_gain_threshold_high_ = auto_gain_threshold_high;
|
||||
}
|
||||
void set_auto_gain_threshold_low(float auto_gain_threshold_low) {
|
||||
this->auto_gain_threshold_low_ = auto_gain_threshold_low;
|
||||
}
|
||||
void set_power_on(bool power_on) { this->power_on_ = power_on; }
|
||||
void set_digital_gain(VEML3235ComponentDigitalGain digital_gain) { this->digital_gain_ = digital_gain; }
|
||||
void set_gain(VEML3235ComponentGain gain) { this->gain_ = gain; }
|
||||
void set_integration_time(VEML3235ComponentIntegrationTime integration_time) {
|
||||
this->integration_time_ = integration_time;
|
||||
}
|
||||
|
||||
bool auto_gain() { return this->auto_gain_; }
|
||||
float auto_gain_threshold_high() { return this->auto_gain_threshold_high_; }
|
||||
float auto_gain_threshold_low() { return this->auto_gain_threshold_low_; }
|
||||
VEML3235ComponentDigitalGain digital_gain() { return this->digital_gain_; }
|
||||
VEML3235ComponentGain gain() { return this->gain_; }
|
||||
VEML3235ComponentIntegrationTime integration_time() { return this->integration_time_; }
|
||||
|
||||
// Updates the configuration register on the device
|
||||
bool refresh_config_reg(bool force_on = false);
|
||||
|
||||
protected:
|
||||
float read_lx_();
|
||||
void adjust_gain_(uint16_t als_raw_value);
|
||||
|
||||
bool auto_gain_{true};
|
||||
bool power_on_{true};
|
||||
float auto_gain_threshold_high_{0.9};
|
||||
float auto_gain_threshold_low_{0.2};
|
||||
VEML3235ComponentDigitalGain digital_gain_{VEML3235_DIGITAL_GAIN_1X};
|
||||
VEML3235ComponentGain gain_{VEML3235_GAIN_1X};
|
||||
VEML3235ComponentIntegrationTime integration_time_{VEML3235_INTEGRATION_TIME_50MS};
|
||||
};
|
||||
|
||||
} // namespace veml3235
|
||||
} // namespace esphome
|
||||
@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
@ -17,8 +17,12 @@ from esphome.const import (
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
waveshare_epaper_ns = cg.esphome_ns.namespace("waveshare_epaper")
|
||||
WaveshareEPaper = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
|
||||
WaveshareEPaperBase = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
|
||||
)
|
||||
WaveshareEPaper = waveshare_epaper_ns.class_("WaveshareEPaper", WaveshareEPaperBase)
|
||||
WaveshareEPaperBWR = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaperBWR", WaveshareEPaperBase
|
||||
)
|
||||
WaveshareEPaperTypeA = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaperTypeA", WaveshareEPaper
|
||||
@ -26,9 +30,21 @@ WaveshareEPaperTypeA = waveshare_epaper_ns.class_(
|
||||
WaveshareEPaper2P7In = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper2P7In", WaveshareEPaper
|
||||
)
|
||||
WaveshareEPaper2P7InB = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper2P7InB", WaveshareEPaperBWR
|
||||
)
|
||||
WaveshareEPaper2P7InBV2 = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper2P7InBV2", WaveshareEPaperBWR
|
||||
)
|
||||
WaveshareEPaper2P7InV2 = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper2P7InV2", WaveshareEPaper
|
||||
)
|
||||
WaveshareEPaper2P9InB = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper2P9InB", WaveshareEPaper
|
||||
)
|
||||
WaveshareEPaper2P9InBV3 = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper2P9InBV3", WaveshareEPaper
|
||||
)
|
||||
GDEY029T94 = waveshare_epaper_ns.class_("GDEY029T94", WaveshareEPaper)
|
||||
WaveshareEPaper4P2In = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper4P2In", WaveshareEPaper
|
||||
@ -66,6 +82,9 @@ WaveshareEPaper7P5InHDB = waveshare_epaper_ns.class_(
|
||||
WaveshareEPaper2P13InDKE = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper2P13InDKE", WaveshareEPaper
|
||||
)
|
||||
WaveshareEPaper2P13InV3 = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper2P13InV3", WaveshareEPaper
|
||||
)
|
||||
GDEW0154M09 = waveshare_epaper_ns.class_("GDEW0154M09", WaveshareEPaper)
|
||||
|
||||
WaveshareEPaperTypeAModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeAModel")
|
||||
@ -83,7 +102,11 @@ MODELS = {
|
||||
"2.90inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN_V2),
|
||||
"gdey029t94": ("c", GDEY029T94),
|
||||
"2.70in": ("b", WaveshareEPaper2P7In),
|
||||
"2.70in-b": ("b", WaveshareEPaper2P7InB),
|
||||
"2.70in-bv2": ("b", WaveshareEPaper2P7InBV2),
|
||||
"2.70inv2": ("b", WaveshareEPaper2P7InV2),
|
||||
"2.90in-b": ("b", WaveshareEPaper2P9InB),
|
||||
"2.90in-bv3": ("b", WaveshareEPaper2P9InBV3),
|
||||
"4.20in": ("b", WaveshareEPaper4P2In),
|
||||
"4.20in-bv2": ("b", WaveshareEPaper4P2InBV2),
|
||||
"5.83in": ("b", WaveshareEPaper5P8In),
|
||||
@ -96,6 +119,7 @@ MODELS = {
|
||||
"7.50inv2alt": ("b", WaveshareEPaper7P5InV2alt),
|
||||
"7.50in-hd-b": ("b", WaveshareEPaper7P5InHDB),
|
||||
"2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE),
|
||||
"2.13inv3": ("c", WaveshareEPaper2P13InV3),
|
||||
"1.54in-m5coreink-m09": ("c", GDEW0154M09),
|
||||
}
|
||||
|
||||
@ -118,12 +142,12 @@ def validate_full_update_every_only_types_ac(value):
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(WaveshareEPaper),
|
||||
cv.GenerateID(): cv.declare_id(WaveshareEPaperBase),
|
||||
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True),
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Optional(CONF_FULL_UPDATE_EVERY): cv.uint32_t,
|
||||
cv.Optional(CONF_FULL_UPDATE_EVERY): cv.int_range(min=1, max=4294967295),
|
||||
cv.Optional(CONF_RESET_DURATION): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=core.TimePeriod(milliseconds=500)),
|
||||
|
||||
186
esphome/components/waveshare_epaper/waveshare_213v3.cpp
Normal file
186
esphome/components/waveshare_epaper/waveshare_213v3.cpp
Normal file
@ -0,0 +1,186 @@
|
||||
#include "waveshare_epaper.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace waveshare_epaper {
|
||||
|
||||
static const char *const TAG = "waveshare_2.13v3";
|
||||
|
||||
static const uint8_t PARTIAL_LUT[] = {
|
||||
0x32, // cmd
|
||||
0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0,
|
||||
};
|
||||
|
||||
static const uint8_t FULL_LUT[] = {
|
||||
0x32, // CMD
|
||||
0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x4A, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x4A, 0x80, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF, 0x0, 0x0, 0xF, 0x0, 0x0, 0x2, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0,
|
||||
};
|
||||
|
||||
static const uint8_t SW_RESET = 0x12;
|
||||
static const uint8_t ACTIVATE = 0x20;
|
||||
static const uint8_t WRITE_BUFFER = 0x24;
|
||||
static const uint8_t WRITE_BASE = 0x26;
|
||||
|
||||
static const uint8_t DRV_OUT_CTL[] = {0x01, 0x27, 0x01, 0x00}; // driver output control
|
||||
static const uint8_t GATEV[] = {0x03, 0x17};
|
||||
static const uint8_t SRCV[] = {0x04, 0x41, 0x0C, 0x32};
|
||||
static const uint8_t SLEEP[] = {0x10, 0x01};
|
||||
static const uint8_t DATA_ENTRY[] = {0x11, 0x03}; // data entry mode
|
||||
static const uint8_t TEMP_SENS[] = {0x18, 0x80}; // Temp sensor
|
||||
static const uint8_t DISPLAY_UPDATE[] = {0x21, 0x00, 0x80}; // Display update control
|
||||
static const uint8_t UPSEQ[] = {0x22, 0xC0};
|
||||
static const uint8_t ON_FULL[] = {0x22, 0xC7};
|
||||
static const uint8_t ON_PARTIAL[] = {0x22, 0x0F};
|
||||
static const uint8_t VCOM[] = {0x2C, 0x36};
|
||||
static const uint8_t CMD5[] = {0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00};
|
||||
static const uint8_t BORDER_PART[] = {0x3C, 0x80}; // border waveform
|
||||
static const uint8_t BORDER_FULL[] = {0x3C, 0x05}; // border waveform
|
||||
static const uint8_t CMD1[] = {0x3F, 0x22};
|
||||
static const uint8_t RAM_X_START[] = {0x44, 0x00, 121 / 8}; // set ram_x_address_start_end
|
||||
static const uint8_t RAM_Y_START[] = {0x45, 0x00, 0x00, 250 - 1, 0}; // set ram_y_address_start_end
|
||||
static const uint8_t RAM_X_POS[] = {0x4E, 0x00}; // set ram_x_address_counter
|
||||
// static const uint8_t RAM_Y_POS[] = {0x4F, 0x00, 0x00}; // set ram_y_address_counter
|
||||
#define SEND(x) this->cmd_data(x, sizeof(x))
|
||||
|
||||
void WaveshareEPaper2P13InV3::write_lut_(const uint8_t *lut) {
|
||||
this->wait_until_idle_();
|
||||
this->cmd_data(lut, sizeof(PARTIAL_LUT));
|
||||
SEND(CMD1);
|
||||
SEND(GATEV);
|
||||
SEND(SRCV);
|
||||
SEND(VCOM);
|
||||
}
|
||||
|
||||
// write the buffer starting on line top, up to line bottom.
|
||||
void WaveshareEPaper2P13InV3::write_buffer_(uint8_t cmd, int top, int bottom) {
|
||||
this->wait_until_idle_();
|
||||
this->set_window_(top, bottom);
|
||||
this->command(cmd);
|
||||
this->start_data_();
|
||||
auto width_bytes = this->get_width_internal() / 8;
|
||||
this->write_array(this->buffer_ + top * width_bytes, (bottom - top) * width_bytes);
|
||||
this->end_data_();
|
||||
}
|
||||
|
||||
void WaveshareEPaper2P13InV3::send_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(2);
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
|
||||
void WaveshareEPaper2P13InV3::setup() {
|
||||
setup_pins_();
|
||||
delay(20);
|
||||
this->send_reset_();
|
||||
// as a one-off delay this is not worth working around.
|
||||
delay(100); // NOLINT
|
||||
this->wait_until_idle_();
|
||||
this->command(SW_RESET);
|
||||
this->wait_until_idle_();
|
||||
|
||||
SEND(DRV_OUT_CTL);
|
||||
SEND(DATA_ENTRY);
|
||||
SEND(CMD5);
|
||||
this->set_window_(0, this->get_height_internal());
|
||||
SEND(BORDER_FULL);
|
||||
SEND(DISPLAY_UPDATE);
|
||||
SEND(TEMP_SENS);
|
||||
this->wait_until_idle_();
|
||||
this->write_lut_(FULL_LUT);
|
||||
}
|
||||
|
||||
// t and b are y positions, i.e. line numbers.
|
||||
void WaveshareEPaper2P13InV3::set_window_(int t, int b) {
|
||||
uint8_t buffer[3];
|
||||
|
||||
SEND(RAM_X_START);
|
||||
SEND(RAM_Y_START);
|
||||
SEND(RAM_X_POS);
|
||||
buffer[0] = 0x4F;
|
||||
buffer[1] = (uint8_t) t;
|
||||
buffer[2] = (uint8_t) (t >> 8);
|
||||
SEND(buffer);
|
||||
}
|
||||
|
||||
// must implement, but we override setup to have more control
|
||||
void WaveshareEPaper2P13InV3::initialize() {}
|
||||
|
||||
void WaveshareEPaper2P13InV3::partial_update_() {
|
||||
this->send_reset_();
|
||||
this->set_timeout(100, [this] {
|
||||
this->write_lut_(PARTIAL_LUT);
|
||||
SEND(BORDER_PART);
|
||||
SEND(UPSEQ);
|
||||
this->command(ACTIVATE);
|
||||
this->set_timeout(100, [this] {
|
||||
this->wait_until_idle_();
|
||||
this->write_buffer_(WRITE_BUFFER, 0, this->get_height_internal());
|
||||
SEND(ON_PARTIAL);
|
||||
this->command(ACTIVATE); // Activate Display Update Sequence
|
||||
this->is_busy_ = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void WaveshareEPaper2P13InV3::full_update_() {
|
||||
ESP_LOGI(TAG, "Performing full e-paper update.");
|
||||
this->write_lut_(FULL_LUT);
|
||||
this->write_buffer_(WRITE_BUFFER, 0, this->get_height_internal());
|
||||
this->write_buffer_(WRITE_BASE, 0, this->get_height_internal());
|
||||
SEND(ON_FULL);
|
||||
this->command(ACTIVATE); // don't wait here
|
||||
this->is_busy_ = false;
|
||||
}
|
||||
|
||||
void WaveshareEPaper2P13InV3::display() {
|
||||
if (this->is_busy_ || (this->busy_pin_ != nullptr && this->busy_pin_->digital_read()))
|
||||
return;
|
||||
this->is_busy_ = true;
|
||||
const bool partial = this->at_update_ != 0;
|
||||
this->at_update_ = (this->at_update_ + 1) % this->full_update_every_;
|
||||
if (partial) {
|
||||
this->partial_update_();
|
||||
} else {
|
||||
this->full_update_();
|
||||
}
|
||||
}
|
||||
|
||||
int WaveshareEPaper2P13InV3::get_width_internal() { return 128; }
|
||||
|
||||
int WaveshareEPaper2P13InV3::get_height_internal() { return 250; }
|
||||
|
||||
uint32_t WaveshareEPaper2P13InV3::idle_timeout_() { return 5000; }
|
||||
|
||||
void WaveshareEPaper2P13InV3::dump_config() {
|
||||
LOG_DISPLAY("", "Waveshare E-Paper", this)
|
||||
ESP_LOGCONFIG(TAG, " Model: 2.13inV3");
|
||||
LOG_PIN(" CS Pin: ", this->cs_)
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_)
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_)
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_)
|
||||
LOG_UPDATE_INTERVAL(this)
|
||||
}
|
||||
|
||||
void WaveshareEPaper2P13InV3::set_full_update_every(uint32_t full_update_every) {
|
||||
this->full_update_every_ = full_update_every;
|
||||
}
|
||||
|
||||
} // namespace waveshare_epaper
|
||||
} // namespace esphome
|
||||
@ -83,7 +83,7 @@ static const uint8_t PARTIAL_UPDATE_LUT_TTGO_B1[LUT_SIZE_TTGO_B1] = {
|
||||
0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x0F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
void WaveshareEPaper::setup_pins_() {
|
||||
void WaveshareEPaperBase::setup_pins_() {
|
||||
this->init_internal_(this->get_buffer_length_());
|
||||
this->dc_pin_->setup(); // OUTPUT
|
||||
this->dc_pin_->digital_write(false);
|
||||
@ -98,19 +98,31 @@ void WaveshareEPaper::setup_pins_() {
|
||||
|
||||
this->reset_();
|
||||
}
|
||||
float WaveshareEPaper::get_setup_priority() const { return setup_priority::PROCESSOR; }
|
||||
void WaveshareEPaper::command(uint8_t value) {
|
||||
float WaveshareEPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; }
|
||||
void WaveshareEPaperBase::command(uint8_t value) {
|
||||
this->start_command_();
|
||||
this->write_byte(value);
|
||||
this->end_command_();
|
||||
}
|
||||
void WaveshareEPaper::data(uint8_t value) {
|
||||
void WaveshareEPaperBase::data(uint8_t value) {
|
||||
this->start_data_();
|
||||
this->write_byte(value);
|
||||
this->end_data_();
|
||||
}
|
||||
bool WaveshareEPaper::wait_until_idle_() {
|
||||
if (this->busy_pin_ == nullptr) {
|
||||
|
||||
// write a command followed by one or more bytes of data.
|
||||
// The command is the first byte, length is the total including cmd.
|
||||
void WaveshareEPaperBase::cmd_data(const uint8_t *c_data, size_t length) {
|
||||
this->dc_pin_->digital_write(false);
|
||||
this->enable();
|
||||
this->write_byte(c_data[0]);
|
||||
this->dc_pin_->digital_write(true);
|
||||
this->write_array(c_data + 1, length - 1);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
bool WaveshareEPaperBase::wait_until_idle_() {
|
||||
if (this->busy_pin_ == nullptr || !this->busy_pin_->digital_read()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -120,11 +132,11 @@ bool WaveshareEPaper::wait_until_idle_() {
|
||||
ESP_LOGE(TAG, "Timeout while displaying image!");
|
||||
return false;
|
||||
}
|
||||
delay(10);
|
||||
delay(1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void WaveshareEPaper::update() {
|
||||
void WaveshareEPaperBase::update() {
|
||||
this->do_update_();
|
||||
this->display();
|
||||
}
|
||||
@ -147,20 +159,51 @@ void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color
|
||||
this->buffer_[pos] &= ~(0x80 >> subpos);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t WaveshareEPaper::get_buffer_length_() {
|
||||
return this->get_width_controller() * this->get_height_internal() / 8u;
|
||||
} // just a black buffer
|
||||
uint32_t WaveshareEPaperBWR::get_buffer_length_() {
|
||||
return this->get_width_controller() * this->get_height_internal() / 4u;
|
||||
} // black and red buffer
|
||||
|
||||
void WaveshareEPaperBWR::fill(Color color) {
|
||||
this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color);
|
||||
}
|
||||
void WaveshareEPaper::start_command_() {
|
||||
void HOT WaveshareEPaperBWR::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0)
|
||||
return;
|
||||
|
||||
const uint32_t buf_half_len = this->get_buffer_length_() / 2u;
|
||||
|
||||
const uint32_t pos = (x + y * this->get_width_internal()) / 8u;
|
||||
const uint8_t subpos = x & 0x07;
|
||||
// flip logic
|
||||
if (color.is_on()) {
|
||||
this->buffer_[pos] |= 0x80 >> subpos;
|
||||
} else {
|
||||
this->buffer_[pos] &= ~(0x80 >> subpos);
|
||||
}
|
||||
|
||||
// draw red pixels only, if the color contains red only
|
||||
if (((color.red > 0) && (color.green == 0) && (color.blue == 0))) {
|
||||
this->buffer_[pos + buf_half_len] |= 0x80 >> subpos;
|
||||
} else {
|
||||
this->buffer_[pos + buf_half_len] &= ~(0x80 >> subpos);
|
||||
}
|
||||
}
|
||||
|
||||
void WaveshareEPaperBase::start_command_() {
|
||||
this->dc_pin_->digital_write(false);
|
||||
this->enable();
|
||||
}
|
||||
void WaveshareEPaper::end_command_() { this->disable(); }
|
||||
void WaveshareEPaper::start_data_() {
|
||||
void WaveshareEPaperBase::end_command_() { this->disable(); }
|
||||
void WaveshareEPaperBase::start_data_() {
|
||||
this->dc_pin_->digital_write(true);
|
||||
this->enable();
|
||||
}
|
||||
void WaveshareEPaper::end_data_() { this->disable(); }
|
||||
void WaveshareEPaper::on_safe_shutdown() { this->deep_sleep(); }
|
||||
void WaveshareEPaperBase::end_data_() { this->disable(); }
|
||||
void WaveshareEPaperBase::on_safe_shutdown() { this->deep_sleep(); }
|
||||
|
||||
// ========================================================
|
||||
// Type A
|
||||
@ -481,7 +524,7 @@ uint32_t WaveshareEPaperTypeA::idle_timeout_() {
|
||||
case TTGO_EPAPER_2_13_IN_B1:
|
||||
return 2500;
|
||||
default:
|
||||
return WaveshareEPaper::idle_timeout_();
|
||||
return WaveshareEPaperBase::idle_timeout_();
|
||||
}
|
||||
}
|
||||
|
||||
@ -634,6 +677,299 @@ void WaveshareEPaper2P7In::dump_config() {
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
void WaveshareEPaper2P7InV2::initialize() {
|
||||
this->reset_();
|
||||
this->wait_until_idle_();
|
||||
|
||||
this->command(0x12); // SWRESET
|
||||
this->wait_until_idle_();
|
||||
|
||||
// SET WINDOWS
|
||||
// XRAM_START_AND_END_POSITION
|
||||
this->command(0x44);
|
||||
this->data(0x00);
|
||||
this->data(((get_width_internal() - 1) >> 3) & 0xFF);
|
||||
// YRAM_START_AND_END_POSITION
|
||||
this->command(0x45);
|
||||
this->data(0x00);
|
||||
this->data(0x00);
|
||||
this->data((get_height_internal() - 1) & 0xFF);
|
||||
this->data(((get_height_internal() - 1) >> 8) & 0xFF);
|
||||
|
||||
// SET CURSOR
|
||||
// XRAM_ADDRESS
|
||||
this->command(0x4E);
|
||||
this->data(0x00);
|
||||
// YRAM_ADDRESS
|
||||
this->command(0x4F);
|
||||
this->data(0x00);
|
||||
this->data(0x00);
|
||||
|
||||
this->command(0x11); // data entry mode
|
||||
this->data(0x03);
|
||||
}
|
||||
void HOT WaveshareEPaper2P7InV2::display() {
|
||||
this->command(0x24);
|
||||
this->start_data_();
|
||||
this->write_array(this->buffer_, this->get_buffer_length_());
|
||||
this->end_data_();
|
||||
|
||||
// COMMAND DISPLAY REFRESH
|
||||
this->command(0x22);
|
||||
this->data(0xF7);
|
||||
this->command(0x20);
|
||||
}
|
||||
int WaveshareEPaper2P7InV2::get_width_internal() { return 176; }
|
||||
int WaveshareEPaper2P7InV2::get_height_internal() { return 264; }
|
||||
void WaveshareEPaper2P7InV2::dump_config() {
|
||||
LOG_DISPLAY("", "Waveshare E-Paper", this);
|
||||
ESP_LOGCONFIG(TAG, " Model: 2.7in V2");
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// 2.7inch_e-paper_b
|
||||
// ========================================================
|
||||
// Datasheet:
|
||||
// - https://www.waveshare.com/w/upload/d/d8/2.7inch-e-paper-b-specification.pdf
|
||||
// - https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_2in7b.c
|
||||
|
||||
static const uint8_t LUT_VCOM_DC_2_7B[44] = {0x00, 0x00, 0x00, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x00, 0x0A, 0x0A,
|
||||
0x00, 0x00, 0x08, 0x00, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x00, 0x0A,
|
||||
0x0A, 0x00, 0x00, 0x08, 0x00, 0x04, 0x10, 0x00, 0x00, 0x05, 0x00,
|
||||
0x03, 0x0E, 0x00, 0x00, 0x0A, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01};
|
||||
|
||||
static const uint8_t LUT_WHITE_TO_WHITE_2_7B[42] = {0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x40, 0x0A, 0x0A, 0x00, 0x00,
|
||||
0x08, 0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x80, 0x0A, 0x0A, 0x00,
|
||||
0x00, 0x08, 0x00, 0x04, 0x10, 0x00, 0x00, 0x05, 0x00, 0x03, 0x0E,
|
||||
0x00, 0x00, 0x0A, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01};
|
||||
|
||||
static const uint8_t LUT_BLACK_TO_WHITE_2_7B[42] = {0xA0, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x00, 0x0A, 0x0A, 0x00, 0x00,
|
||||
0x08, 0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x90, 0x0A, 0x0A, 0x00,
|
||||
0x00, 0x08, 0xB0, 0x04, 0x10, 0x00, 0x00, 0x05, 0xB0, 0x03, 0x0E,
|
||||
0x00, 0x00, 0x0A, 0xC0, 0x23, 0x00, 0x00, 0x00, 0x01};
|
||||
|
||||
static const uint8_t LUT_WHITE_TO_BLACK_2_7B[] = {0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x20, 0x0A, 0x0A, 0x00, 0x00,
|
||||
0x08, 0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x10, 0x0A, 0x0A, 0x00,
|
||||
0x00, 0x08, 0x00, 0x04, 0x10, 0x00, 0x00, 0x05, 0x00, 0x03, 0x0E,
|
||||
0x00, 0x00, 0x0A, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01};
|
||||
|
||||
static const uint8_t LUT_BLACK_TO_BLACK_2_7B[42] = {0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, 0x40, 0x0A, 0x0A, 0x00, 0x00,
|
||||
0x08, 0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, 0x80, 0x0A, 0x0A, 0x00,
|
||||
0x00, 0x08, 0x00, 0x04, 0x10, 0x00, 0x00, 0x05, 0x00, 0x03, 0x0E,
|
||||
0x00, 0x00, 0x0A, 0x00, 0x23, 0x00, 0x00, 0x00, 0x01};
|
||||
|
||||
void WaveshareEPaper2P7InB::initialize() {
|
||||
this->reset_();
|
||||
|
||||
// command power on
|
||||
this->command(0x04);
|
||||
this->wait_until_idle_();
|
||||
delay(10);
|
||||
|
||||
// Command panel setting
|
||||
this->command(0x00);
|
||||
this->data(0xAF); // KW-BF KWR-AF BWROTP 0f
|
||||
// command pll control
|
||||
this->command(0x30);
|
||||
this->data(0x3A); // 3A 100HZ 29 150Hz 39 200HZ 31 171HZ
|
||||
|
||||
// command power setting
|
||||
this->command(0x01);
|
||||
this->data(0x03); // VDS_EN, VDG_EN
|
||||
this->data(0x00); // VCOM_HV, VGHL_LV[1], VGHL_LV[0]
|
||||
this->data(0x2B); // VDH
|
||||
this->data(0x2B); // VDL
|
||||
this->data(0x09); // VDHR
|
||||
|
||||
// command booster soft start
|
||||
this->command(0x06);
|
||||
this->data(0x07);
|
||||
this->data(0x07);
|
||||
this->data(0x17);
|
||||
|
||||
// Power optimization - ???
|
||||
this->command(0xF8);
|
||||
this->data(0x60);
|
||||
this->data(0xA5);
|
||||
this->command(0xF8);
|
||||
this->data(0x89);
|
||||
this->data(0xA5);
|
||||
this->command(0xF8);
|
||||
this->data(0x90);
|
||||
this->data(0x00);
|
||||
this->command(0xF8);
|
||||
this->data(0x93);
|
||||
this->data(0x2A);
|
||||
this->command(0xF8);
|
||||
this->data(0x73);
|
||||
this->data(0x41);
|
||||
|
||||
// COMMAND VCM DC SETTING
|
||||
this->command(0x82);
|
||||
this->data(0x12);
|
||||
|
||||
// VCOM_AND_DATA_INTERVAL_SETTING
|
||||
this->command(0x50);
|
||||
this->data(0x87); // define by OTP
|
||||
|
||||
delay(2);
|
||||
// COMMAND LUT FOR VCOM
|
||||
this->command(0x20);
|
||||
for (uint8_t i : LUT_VCOM_DC_2_7B)
|
||||
this->data(i);
|
||||
// COMMAND LUT WHITE TO WHITE
|
||||
this->command(0x21);
|
||||
for (uint8_t i : LUT_WHITE_TO_WHITE_2_7B)
|
||||
this->data(i);
|
||||
// COMMAND LUT BLACK TO WHITE
|
||||
this->command(0x22);
|
||||
for (uint8_t i : LUT_BLACK_TO_WHITE_2_7B)
|
||||
this->data(i);
|
||||
// COMMAND LUT WHITE TO BLACK
|
||||
this->command(0x23);
|
||||
for (uint8_t i : LUT_WHITE_TO_BLACK_2_7B) {
|
||||
this->data(i);
|
||||
}
|
||||
// COMMAND LUT BLACK TO BLACK
|
||||
this->command(0x24);
|
||||
|
||||
for (uint8_t i : LUT_BLACK_TO_BLACK_2_7B) {
|
||||
this->data(i);
|
||||
}
|
||||
|
||||
delay(2);
|
||||
}
|
||||
|
||||
void HOT WaveshareEPaper2P7InB::display() {
|
||||
uint32_t buf_len_half = this->get_buffer_length_() >> 1;
|
||||
this->initialize();
|
||||
|
||||
// TCON_RESOLUTION
|
||||
this->command(0x61);
|
||||
this->data(this->get_width_internal() >> 8);
|
||||
this->data(this->get_width_internal() & 0xff); // 176
|
||||
this->data(this->get_height_internal() >> 8);
|
||||
this->data(this->get_height_internal() & 0xff); // 264
|
||||
|
||||
// COMMAND DATA START TRANSMISSION 1 (BLACK)
|
||||
this->command(0x10);
|
||||
delay(2);
|
||||
for (uint32_t i = 0; i < buf_len_half; i++) {
|
||||
this->data(this->buffer_[i]);
|
||||
}
|
||||
this->command(0x11);
|
||||
delay(2);
|
||||
|
||||
// COMMAND DATA START TRANSMISSION 2 (RED)
|
||||
this->command(0x13);
|
||||
delay(2);
|
||||
for (uint32_t i = buf_len_half; i < buf_len_half * 2u; i++) {
|
||||
this->data(this->buffer_[i]);
|
||||
}
|
||||
this->command(0x11);
|
||||
|
||||
delay(2);
|
||||
|
||||
// COMMAND DISPLAY REFRESH
|
||||
this->command(0x12);
|
||||
this->wait_until_idle_();
|
||||
|
||||
this->deep_sleep();
|
||||
}
|
||||
int WaveshareEPaper2P7InB::get_width_internal() { return 176; }
|
||||
int WaveshareEPaper2P7InB::get_height_internal() { return 264; }
|
||||
void WaveshareEPaper2P7InB::dump_config() {
|
||||
LOG_DISPLAY("", "Waveshare E-Paper", this);
|
||||
ESP_LOGCONFIG(TAG, " Model: 2.7in B");
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// 2.7inch_e-paper_b_v2
|
||||
// ========================================================
|
||||
// Datasheet:
|
||||
// - https://www.waveshare.com/w/upload/7/7b/2.7inch-e-paper-b-v2-specification.pdf
|
||||
// - https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_2in7b_V2.c
|
||||
|
||||
void WaveshareEPaper2P7InBV2::initialize() {
|
||||
this->reset_();
|
||||
|
||||
this->wait_until_idle_();
|
||||
this->command(0x12);
|
||||
this->wait_until_idle_();
|
||||
|
||||
this->command(0x00);
|
||||
this->data(0x27);
|
||||
this->data(0x01);
|
||||
this->data(0x00);
|
||||
|
||||
this->command(0x11);
|
||||
this->data(0x03);
|
||||
|
||||
// self.SetWindows(0, 0, self.width-1, self.height-1)
|
||||
// SetWindows(self, Xstart, Ystart, Xend, Yend):
|
||||
|
||||
uint32_t xend = this->get_width_internal() - 1;
|
||||
uint32_t yend = this->get_height_internal() - 1;
|
||||
this->command(0x44);
|
||||
this->data(0x00);
|
||||
this->data((xend >> 3) & 0xff);
|
||||
|
||||
this->command(0x45);
|
||||
this->data(0x00);
|
||||
this->data(0x00);
|
||||
this->data(yend & 0xff);
|
||||
this->data((yend >> 8) & 0xff);
|
||||
|
||||
// SetCursor(self, Xstart, Ystart):
|
||||
this->command(0x4E);
|
||||
this->data(0x00);
|
||||
this->command(0x4F);
|
||||
this->data(0x00);
|
||||
this->data(0x00);
|
||||
}
|
||||
|
||||
void HOT WaveshareEPaper2P7InBV2::display() {
|
||||
uint32_t buf_len = this->get_buffer_length_();
|
||||
// COMMAND DATA START TRANSMISSION 1 (BLACK)
|
||||
this->command(0x24);
|
||||
delay(2);
|
||||
for (uint32_t i = 0; i < buf_len; i++) {
|
||||
this->data(this->buffer_[i]);
|
||||
}
|
||||
delay(2);
|
||||
|
||||
// COMMAND DATA START TRANSMISSION 2 (RED)
|
||||
this->command(0x26);
|
||||
delay(2);
|
||||
for (uint32_t i = 0; i < buf_len; i++) {
|
||||
this->data(this->buffer_[i]);
|
||||
}
|
||||
|
||||
delay(2);
|
||||
|
||||
this->command(0x20);
|
||||
|
||||
this->wait_until_idle_();
|
||||
}
|
||||
int WaveshareEPaper2P7InBV2::get_width_internal() { return 176; }
|
||||
int WaveshareEPaper2P7InBV2::get_height_internal() { return 264; }
|
||||
void WaveshareEPaper2P7InBV2::dump_config() {
|
||||
LOG_DISPLAY("", "Waveshare E-Paper", this);
|
||||
ESP_LOGCONFIG(TAG, " Model: 2.7in B V2");
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// 2.90in Type B (LUT from OTP)
|
||||
// Datasheet:
|
||||
@ -713,6 +1049,75 @@ void WaveshareEPaper2P9InB::dump_config() {
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// 2.90in Type B (LUT from OTP)
|
||||
// Datasheet:
|
||||
// - https://files.waveshare.com/upload/a/af/2.9inch-e-paper-b-v3-specification.pdf
|
||||
// ========================================================
|
||||
|
||||
void WaveshareEPaper2P9InBV3::initialize() {
|
||||
// from https://github.com/waveshareteam/e-Paper/blob/master/Arduino/epd2in9b_V3/epd2in9b_V3.cpp
|
||||
this->reset_();
|
||||
|
||||
// COMMAND POWER ON
|
||||
this->command(0x04);
|
||||
this->wait_until_idle_();
|
||||
|
||||
// COMMAND PANEL SETTING
|
||||
this->command(0x00);
|
||||
this->data(0x0F);
|
||||
this->data(0x89);
|
||||
|
||||
// COMMAND RESOLUTION SETTING
|
||||
this->command(0x61);
|
||||
this->data(0x80);
|
||||
this->data(0x01);
|
||||
this->data(0x28);
|
||||
|
||||
// COMMAND VCOM AND DATA INTERVAL SETTING
|
||||
this->command(0x50);
|
||||
this->data(0x77);
|
||||
}
|
||||
void HOT WaveshareEPaper2P9InBV3::display() {
|
||||
// COMMAND DATA START TRANSMISSION 1 (B/W data)
|
||||
this->command(0x10);
|
||||
delay(2);
|
||||
this->start_data_();
|
||||
this->write_array(this->buffer_, this->get_buffer_length_());
|
||||
this->end_data_();
|
||||
this->command(0x92);
|
||||
delay(2);
|
||||
|
||||
// COMMAND DATA START TRANSMISSION 2 (RED data)
|
||||
this->command(0x13);
|
||||
delay(2);
|
||||
this->start_data_();
|
||||
for (size_t i = 0; i < this->get_buffer_length_(); i++)
|
||||
this->write_byte(0xFF);
|
||||
this->end_data_();
|
||||
this->command(0x92);
|
||||
delay(2);
|
||||
|
||||
// COMMAND DISPLAY REFRESH
|
||||
this->command(0x12);
|
||||
delay(2);
|
||||
this->wait_until_idle_();
|
||||
|
||||
// COMMAND POWER OFF
|
||||
// NOTE: power off < deep sleep
|
||||
this->command(0x02);
|
||||
}
|
||||
int WaveshareEPaper2P9InBV3::get_width_internal() { return 128; }
|
||||
int WaveshareEPaper2P9InBV3::get_height_internal() { return 296; }
|
||||
void WaveshareEPaper2P9InBV3::dump_config() {
|
||||
LOG_DISPLAY("", "Waveshare E-Paper", this);
|
||||
ESP_LOGCONFIG(TAG, " Model: 2.9in (B) V3");
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// Good Display 2.9in black/white/grey
|
||||
// Datasheet:
|
||||
@ -1321,6 +1726,12 @@ void WaveshareEPaper7P5InBV2::initialize() {
|
||||
// COMMAND TCON SETTING
|
||||
this->command(0x60);
|
||||
this->data(0x22);
|
||||
|
||||
this->command(0x82);
|
||||
this->data(0x08);
|
||||
this->command(0x30);
|
||||
this->data(0x06);
|
||||
|
||||
// COMMAND RESOLUTION SETTING
|
||||
this->command(0x65);
|
||||
this->data(0x00);
|
||||
@ -1350,6 +1761,7 @@ void HOT WaveshareEPaper7P5InBV2::display() {
|
||||
this->command(0x12);
|
||||
delay(100); // NOLINT
|
||||
this->wait_until_idle_();
|
||||
this->deep_sleep();
|
||||
}
|
||||
int WaveshareEPaper7P5InBV2::get_width_internal() { return 800; }
|
||||
int WaveshareEPaper7P5InBV2::get_height_internal() { return 480; }
|
||||
@ -1495,7 +1907,7 @@ void HOT WaveshareEPaper7P5InBV3::display() {
|
||||
this->command(0x13); // Start Transmission
|
||||
delay(2);
|
||||
for (uint32_t i = 0; i < buf_len; i++) {
|
||||
this->data(this->buffer_[i]);
|
||||
this->data(~this->buffer_[i]);
|
||||
}
|
||||
|
||||
this->command(0x12); // Display Refresh
|
||||
@ -2089,8 +2501,9 @@ void HOT WaveshareEPaper2P13InDKE::display() {
|
||||
} else {
|
||||
// set up partial update
|
||||
this->command(0x32);
|
||||
for (uint8_t v : PART_UPDATE_LUT_TTGO_DKE)
|
||||
this->data(v);
|
||||
this->start_data_();
|
||||
this->write_array(PART_UPDATE_LUT_TTGO_DKE, sizeof(PART_UPDATE_LUT_TTGO_DKE));
|
||||
this->end_data_();
|
||||
this->command(0x3F);
|
||||
this->data(0x22);
|
||||
|
||||
@ -2135,12 +2548,10 @@ void HOT WaveshareEPaper2P13InDKE::display() {
|
||||
this->wait_until_idle_();
|
||||
|
||||
// data must be sent again on partial update
|
||||
delay(300); // NOLINT
|
||||
this->command(0x24);
|
||||
this->start_data_();
|
||||
this->write_array(this->buffer_, this->get_buffer_length_());
|
||||
this->end_data_();
|
||||
delay(300); // NOLINT
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Completed e-paper update.");
|
||||
@ -2152,6 +2563,7 @@ uint32_t WaveshareEPaper2P13InDKE::idle_timeout_() { return 5000; }
|
||||
void WaveshareEPaper2P13InDKE::dump_config() {
|
||||
LOG_DISPLAY("", "Waveshare E-Paper", this);
|
||||
ESP_LOGCONFIG(TAG, " Model: 2.13inDKE");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
namespace esphome {
|
||||
namespace waveshare_epaper {
|
||||
|
||||
class WaveshareEPaper : public display::DisplayBuffer,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> {
|
||||
class WaveshareEPaperBase : public display::DisplayBuffer,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> {
|
||||
public:
|
||||
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
|
||||
float get_setup_priority() const override;
|
||||
@ -19,6 +19,7 @@ class WaveshareEPaper : public display::DisplayBuffer,
|
||||
|
||||
void command(uint8_t value);
|
||||
void data(uint8_t value);
|
||||
void cmd_data(const uint8_t *data, size_t length);
|
||||
|
||||
virtual void display() = 0;
|
||||
virtual void initialize() = 0;
|
||||
@ -26,8 +27,6 @@ class WaveshareEPaper : public display::DisplayBuffer,
|
||||
|
||||
void update() override;
|
||||
|
||||
void fill(Color color) override;
|
||||
|
||||
void setup() override {
|
||||
this->setup_pins_();
|
||||
this->initialize();
|
||||
@ -35,11 +34,7 @@ class WaveshareEPaper : public display::DisplayBuffer,
|
||||
|
||||
void on_safe_shutdown() override;
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
|
||||
bool wait_until_idle_();
|
||||
|
||||
void setup_pins_();
|
||||
@ -49,13 +44,13 @@ class WaveshareEPaper : public display::DisplayBuffer,
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(reset_duration_); // NOLINT
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(200); // NOLINT
|
||||
delay(20);
|
||||
}
|
||||
}
|
||||
|
||||
virtual int get_width_controller() { return this->get_width_internal(); };
|
||||
|
||||
uint32_t get_buffer_length_();
|
||||
virtual uint32_t get_buffer_length_() = 0; // NOLINT(readability-identifier-naming)
|
||||
uint32_t reset_duration_{200};
|
||||
|
||||
void start_command_();
|
||||
@ -69,6 +64,28 @@ class WaveshareEPaper : public display::DisplayBuffer,
|
||||
virtual uint32_t idle_timeout_() { return 1000u; } // NOLINT(readability-identifier-naming)
|
||||
};
|
||||
|
||||
class WaveshareEPaper : public WaveshareEPaperBase {
|
||||
public:
|
||||
void fill(Color color) override;
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
uint32_t get_buffer_length_() override;
|
||||
};
|
||||
|
||||
class WaveshareEPaperBWR : public WaveshareEPaperBase {
|
||||
public:
|
||||
void fill(Color color) override;
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
uint32_t get_buffer_length_() override;
|
||||
};
|
||||
|
||||
enum WaveshareEPaperTypeAModel {
|
||||
WAVESHARE_EPAPER_1_54_IN = 0,
|
||||
WAVESHARE_EPAPER_1_54_IN_V2,
|
||||
@ -133,6 +150,8 @@ class WaveshareEPaperTypeA : public WaveshareEPaper {
|
||||
|
||||
enum WaveshareEPaperTypeBModel {
|
||||
WAVESHARE_EPAPER_2_7_IN = 0,
|
||||
WAVESHARE_EPAPER_2_7_IN_B,
|
||||
WAVESHARE_EPAPER_2_7_IN_B_V2,
|
||||
WAVESHARE_EPAPER_4_2_IN,
|
||||
WAVESHARE_EPAPER_4_2_IN_B_V2,
|
||||
WAVESHARE_EPAPER_7_5_IN,
|
||||
@ -156,7 +175,49 @@ class WaveshareEPaper2P7In : public WaveshareEPaper {
|
||||
|
||||
protected:
|
||||
int get_width_internal() override;
|
||||
int get_height_internal() override;
|
||||
};
|
||||
|
||||
class WaveshareEPaper2P7InB : public WaveshareEPaperBWR {
|
||||
public:
|
||||
void initialize() override;
|
||||
|
||||
void display() override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
void deep_sleep() override {
|
||||
// COMMAND VCOM_AND_DATA_INTERVAL_SETTING
|
||||
this->command(0x50);
|
||||
// COMMAND POWER OFF
|
||||
this->command(0x02);
|
||||
this->wait_until_idle_();
|
||||
// COMMAND DEEP SLEEP
|
||||
this->command(0x07); // deep sleep
|
||||
this->data(0xA5); // check byte
|
||||
}
|
||||
|
||||
protected:
|
||||
int get_width_internal() override;
|
||||
int get_height_internal() override;
|
||||
};
|
||||
|
||||
class WaveshareEPaper2P7InBV2 : public WaveshareEPaperBWR {
|
||||
public:
|
||||
void initialize() override;
|
||||
|
||||
void display() override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
void deep_sleep() override {
|
||||
// COMMAND DEEP SLEEP
|
||||
this->command(0x10);
|
||||
this->data(0x01);
|
||||
}
|
||||
|
||||
protected:
|
||||
int get_width_internal() override;
|
||||
int get_height_internal() override;
|
||||
};
|
||||
|
||||
@ -180,6 +241,22 @@ class GDEY029T94 : public WaveshareEPaper {
|
||||
int get_height_internal() override;
|
||||
};
|
||||
|
||||
class WaveshareEPaper2P7InV2 : public WaveshareEPaper {
|
||||
public:
|
||||
void initialize() override;
|
||||
|
||||
void display() override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
void deep_sleep() override { ; }
|
||||
|
||||
protected:
|
||||
int get_width_internal() override;
|
||||
|
||||
int get_height_internal() override;
|
||||
};
|
||||
|
||||
class GDEW0154M09 : public WaveshareEPaper {
|
||||
public:
|
||||
void initialize() override;
|
||||
@ -240,6 +317,26 @@ class WaveshareEPaper2P9InB : public WaveshareEPaper {
|
||||
int get_height_internal() override;
|
||||
};
|
||||
|
||||
class WaveshareEPaper2P9InBV3 : public WaveshareEPaper {
|
||||
public:
|
||||
void initialize() override;
|
||||
|
||||
void display() override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
void deep_sleep() override {
|
||||
// COMMAND DEEP SLEEP
|
||||
this->command(0x07);
|
||||
this->data(0xA5); // check byte
|
||||
}
|
||||
|
||||
protected:
|
||||
int get_width_internal() override;
|
||||
|
||||
int get_height_internal() override;
|
||||
};
|
||||
|
||||
class WaveshareEPaper4P2In : public WaveshareEPaper {
|
||||
public:
|
||||
void initialize() override;
|
||||
@ -578,5 +675,39 @@ class WaveshareEPaper2P13InDKE : public WaveshareEPaper {
|
||||
uint32_t at_update_{0};
|
||||
};
|
||||
|
||||
class WaveshareEPaper2P13InV3 : public WaveshareEPaper {
|
||||
public:
|
||||
void display() override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
void deep_sleep() override {
|
||||
// COMMAND POWER DOWN
|
||||
this->command(0x10);
|
||||
this->data(0x01);
|
||||
// cannot wait until idle here, the device no longer responds
|
||||
}
|
||||
|
||||
void set_full_update_every(uint32_t full_update_every);
|
||||
|
||||
void setup() override;
|
||||
void initialize() override;
|
||||
|
||||
protected:
|
||||
int get_width_internal() override;
|
||||
int get_height_internal() override;
|
||||
uint32_t idle_timeout_() override;
|
||||
|
||||
void write_buffer_(uint8_t cmd, int top, int bottom);
|
||||
void set_window_(int t, int b);
|
||||
void send_reset_();
|
||||
void partial_update_();
|
||||
void full_update_();
|
||||
|
||||
uint32_t full_update_every_{30};
|
||||
uint32_t at_update_{0};
|
||||
bool is_busy_{false};
|
||||
void write_lut_(const uint8_t *lut);
|
||||
};
|
||||
} // namespace waveshare_epaper
|
||||
} // namespace esphome
|
||||
|
||||
@ -397,19 +397,21 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
|
||||
|
||||
#define set_json_id(root, obj, sensor, start_config) \
|
||||
(root)["id"] = sensor; \
|
||||
if (((start_config) == DETAIL_ALL)) \
|
||||
(root)["name"] = (obj)->get_name();
|
||||
if (((start_config) == DETAIL_ALL)) { \
|
||||
(root)["name"] = (obj)->get_name(); \
|
||||
(root)["icon"] = (obj)->get_icon(); \
|
||||
(root)["entity_category"] = (obj)->get_entity_category(); \
|
||||
if ((obj)->is_disabled_by_default()) \
|
||||
(root)["is_disabled_by_default"] = (obj)->is_disabled_by_default(); \
|
||||
}
|
||||
|
||||
#define set_json_value(root, obj, sensor, value, start_config) \
|
||||
set_json_id((root), (obj), sensor, start_config)(root)["value"] = value;
|
||||
|
||||
#define set_json_state_value(root, obj, sensor, state, value, start_config) \
|
||||
set_json_value(root, obj, sensor, value, start_config)(root)["state"] = state;
|
||||
set_json_id((root), (obj), sensor, start_config); \
|
||||
(root)["value"] = value;
|
||||
|
||||
#define set_json_icon_state_value(root, obj, sensor, state, value, start_config) \
|
||||
set_json_value(root, obj, sensor, value, start_config)(root)["state"] = state; \
|
||||
if (((start_config) == DETAIL_ALL)) \
|
||||
(root)["icon"] = (obj)->get_icon();
|
||||
set_json_value(root, obj, sensor, value, start_config); \
|
||||
(root)["state"] = state;
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void WebServer::on_sensor_update(sensor::Sensor *obj, float state) {
|
||||
@ -436,6 +438,10 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail
|
||||
state += " " + obj->get_unit_of_measurement();
|
||||
}
|
||||
set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
if (!obj->get_unit_of_measurement().empty())
|
||||
root["uom"] = obj->get_unit_of_measurement();
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
@ -529,7 +535,8 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool s
|
||||
}
|
||||
std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) {
|
||||
return json::build_json([obj, value, start_config](JsonObject root) {
|
||||
set_json_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config);
|
||||
set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value,
|
||||
start_config);
|
||||
});
|
||||
}
|
||||
void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@ -548,7 +555,8 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con
|
||||
void WebServer::on_fan_update(fan::Fan *obj) { this->events_.send(this->fan_json(obj, DETAIL_STATE).c_str(), "state"); }
|
||||
std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
|
||||
return json::build_json([obj, start_config](JsonObject root) {
|
||||
set_json_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state, start_config);
|
||||
set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state,
|
||||
start_config);
|
||||
const auto traits = obj->get_traits();
|
||||
if (traits.supports_speed()) {
|
||||
root["speed_level"] = obj->speed;
|
||||
@ -773,8 +781,8 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
|
||||
}
|
||||
std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
|
||||
return json::build_json([obj, start_config](JsonObject root) {
|
||||
set_json_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN",
|
||||
obj->position, start_config);
|
||||
set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN",
|
||||
obj->position, start_config);
|
||||
root["current_operation"] = cover::cover_operation_to_str(obj->current_operation);
|
||||
|
||||
if (obj->get_traits().get_supports_tilt())
|
||||
@ -824,6 +832,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
|
||||
root["max_value"] = obj->traits.get_max_value();
|
||||
root["step"] = obj->traits.get_step();
|
||||
root["mode"] = (int) obj->traits.get_mode();
|
||||
if (!obj->traits.get_unit_of_measurement().empty())
|
||||
root["uom"] = obj->traits.get_unit_of_measurement();
|
||||
}
|
||||
if (std::isnan(value)) {
|
||||
root["value"] = "\"NaN\"";
|
||||
@ -930,7 +940,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
std::string WebServer::select_json(select::Select *obj, const std::string &value, JsonDetail start_config) {
|
||||
return json::build_json([obj, value, start_config](JsonObject root) {
|
||||
set_json_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
|
||||
set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
JsonArray opt = root.createNestedArray("option");
|
||||
for (auto &option : obj->traits.get_options()) {
|
||||
|
||||
@ -55,6 +55,9 @@ void WiFiComponent::start() {
|
||||
uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL;
|
||||
|
||||
this->pref_ = global_preferences->make_preference<wifi::SavedWifiSettings>(hash, true);
|
||||
if (this->fast_connect_) {
|
||||
this->fast_connect_pref_ = global_preferences->make_preference<wifi::SavedWifiFastConnectSettings>(hash, false);
|
||||
}
|
||||
|
||||
SavedWifiSettings save{};
|
||||
if (this->pref_.load(&save)) {
|
||||
@ -78,6 +81,7 @@ void WiFiComponent::start() {
|
||||
|
||||
if (this->fast_connect_) {
|
||||
this->selected_ap_ = this->sta_[0];
|
||||
this->load_fast_connect_settings_();
|
||||
this->start_connecting(this->selected_ap_, false);
|
||||
} else {
|
||||
this->start_scanning();
|
||||
@ -604,6 +608,11 @@ void WiFiComponent::check_connecting_finished() {
|
||||
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
|
||||
this->num_retried_ = 0;
|
||||
|
||||
if (this->fast_connect_) {
|
||||
this->save_fast_connect_settings_();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -705,6 +714,35 @@ bool WiFiComponent::is_esp32_improv_active_() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void WiFiComponent::load_fast_connect_settings_() {
|
||||
SavedWifiFastConnectSettings fast_connect_save{};
|
||||
|
||||
if (this->fast_connect_pref_.load(&fast_connect_save)) {
|
||||
bssid_t bssid{};
|
||||
std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin());
|
||||
this->selected_ap_.set_bssid(bssid);
|
||||
this->selected_ap_.set_channel(fast_connect_save.channel);
|
||||
|
||||
ESP_LOGD(TAG, "Loaded saved fast_connect wifi settings");
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiComponent::save_fast_connect_settings_() {
|
||||
bssid_t bssid = wifi_bssid();
|
||||
uint8_t channel = wifi_channel_();
|
||||
|
||||
if (bssid != this->selected_ap_.get_bssid() || channel != this->selected_ap_.get_channel()) {
|
||||
SavedWifiFastConnectSettings fast_connect_save{};
|
||||
|
||||
memcpy(fast_connect_save.bssid, bssid.data(), 6);
|
||||
fast_connect_save.channel = channel;
|
||||
|
||||
this->fast_connect_pref_.save(&fast_connect_save);
|
||||
|
||||
ESP_LOGD(TAG, "Saved fast_connect wifi settings");
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
|
||||
void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; }
|
||||
void WiFiAP::set_bssid(optional<bssid_t> bssid) { this->bssid_ = bssid; }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user