Compare commits
433 Commits
jesserockz
...
dev
Author | SHA1 | Date | |
---|---|---|---|
|
e0555e140f | ||
|
093989406f | ||
|
cdb16f08f6 | ||
|
53139c293b | ||
|
6a8bdcc315 | ||
|
fe535939a3 | ||
|
09e6c11d73 | ||
|
8112bdfaa8 | ||
|
f7db9aaa9f | ||
|
435f972357 | ||
|
f82b46c16b | ||
|
bca96f91b2 | ||
|
6f83a49c63 | ||
|
72cce391ab | ||
|
28d2949ebe | ||
|
c4a0015997 | ||
|
f564be6aea | ||
|
988f15e6af | ||
|
37b6d442bd | ||
|
fb2467f6f0 | ||
|
29045b0435 | ||
|
311a48c64e | ||
|
01b3815f27 | ||
|
b0d1c801bd | ||
|
5aaac06f5b | ||
|
34adbf0588 | ||
|
0a4213182e | ||
|
b0c0258e70 | ||
|
8110e591d0 | ||
|
fe05d7aec1 | ||
|
57f5884070 | ||
|
f329c74a15 | ||
|
7c86f3fa9e | ||
|
203b8b01bf | ||
|
8a1034a92f | ||
|
aa0c2dedd9 | ||
|
d045908e05 | ||
|
f002a23d2d | ||
|
29d6d0a906 | ||
|
c8b58b5c23 | ||
|
01bfafc5f1 | ||
|
8c9948bb56 | ||
|
2d1abaa68e | ||
|
664a3df0b4 | ||
|
9ff893881c | ||
|
94f6c6861a | ||
|
b1d614e6c4 | ||
|
7fceb070e5 | ||
|
06440d0202 | ||
|
0ecf9f4f2f | ||
|
5c7c0834c0 | ||
|
f3a25de11d | ||
|
041bef8bcd | ||
|
8998c5f6dd | ||
|
6e83790308 | ||
|
d2d4eb4eae | ||
|
5942a3898c | ||
|
93421f0fa7 | ||
|
3a9ab50dd2 | ||
|
5abd91d6d5 | ||
|
c3da42516b | ||
|
6cb5cd48c2 | ||
|
ec1fae6883 | ||
|
746fd1122f | ||
|
9663760ec5 | ||
|
a3d73d1e23 | ||
|
d63e14a4b6 | ||
|
03944e6cd8 | ||
|
0d1028be2e | ||
|
6a85259e4d | ||
|
ebca936b7e | ||
|
31c4551890 | ||
|
dd470d4197 | ||
|
612822490b | ||
|
f8969605e8 | ||
|
dd24ffa24e | ||
|
d0dda48932 | ||
|
6349b5f654 | ||
|
a6ff02a3cf | ||
|
4f57bf786b | ||
|
6221f6d47d | ||
|
a922efeafa | ||
|
5aa42e5e66 | ||
|
708672ec7e | ||
|
d2cefbf224 | ||
|
adb7aa6950 | ||
|
77f322166e | ||
|
f3f6e54818 | ||
|
fb0fec1f25 | ||
|
b66af9fb4d | ||
|
6617d576a7 | ||
|
420dacb22d | ||
|
ae2f6ad4d1 | ||
|
2c28d79bf8 | ||
|
c5069edc78 | ||
|
282d9e138c | ||
|
72fcf2cbe1 | ||
|
6f49f5465b | ||
|
17b8bd8316 | ||
|
7e88938932 | ||
|
c707e64685 | ||
|
a639690716 | ||
|
01222dbab7 | ||
|
ff72d6a146 | ||
|
603d0d0c7c | ||
|
28883f711b | ||
|
e914828add | ||
|
c1480029fb | ||
|
40f622949e | ||
|
63096ac2bc | ||
|
c2a59cb476 | ||
|
d6e039a1d1 | ||
|
0f1a7c2b69 | ||
|
993044c870 | ||
|
a8c1b63edb | ||
|
db7d946e1b | ||
|
41d9059a2f | ||
|
e26e0d7c01 | ||
|
ad41c07a1f | ||
|
5732f3b044 | ||
|
712115b6ce | ||
|
9283559c6b | ||
|
6b393438e9 | ||
|
343b9ab455 | ||
|
dcb226b202 | ||
|
2243021b58 | ||
|
d5134e88b1 | ||
|
c59adf612f | ||
|
a82d8ea0c3 | ||
|
ad57faa9a9 | ||
|
a9b5e8d036 | ||
|
fc7348d46d | ||
|
8be2456c7e | ||
|
bb5f7249a6 | ||
|
fc94a5d0ee | ||
|
24029cc918 | ||
|
9a9d5964ee | ||
|
4e4a512107 | ||
|
0729ed538e | ||
|
24b75b7ed6 | ||
|
ec3618ecb8 | ||
|
792a24f38d | ||
|
652e8a015b | ||
|
59e6e798dd | ||
|
e5c2dbc7ec | ||
|
756f71c382 | ||
|
b7535693fa | ||
|
06a3505698 | ||
|
efa8f0730d | ||
|
023d26f521 | ||
|
5068619f1b | ||
|
1ef6fd8fb0 | ||
|
942b0de7fd | ||
|
859cca49d1 | ||
|
8f7ff25624 | ||
|
97aca8e54c | ||
|
95acf19067 | ||
|
3d0899aa58 | ||
|
138d6e505b | ||
|
2748e6ba29 | ||
|
dbd4e927d8 | ||
|
e73d47918f | ||
|
b881bc071e | ||
|
1d0395d1c7 | ||
|
616c787e37 | ||
|
0c4de2bc97 | ||
|
c2f5ac9eba | ||
|
5764c988af | ||
|
ccc2fbfd67 | ||
|
10b4adb8e6 | ||
|
83b7181bcb | ||
|
8886b7e141 | ||
|
7dcc4d030b | ||
|
b9398897c1 | ||
|
657b1c60ae | ||
|
dc54b17778 | ||
|
1fb214165b | ||
|
81b2fd78f5 | ||
|
69002fb1e6 | ||
|
75332a752d | ||
|
b528f48417 | ||
|
ec7a79049a | ||
|
6ddad6b299 | ||
|
16dc7762f9 | ||
|
dc0ed8857f | ||
|
bb6b77bd98 | ||
|
dcc80f9032 | ||
|
dd554bcdf4 | ||
|
f376a39e55 | ||
|
8dcc9d6b66 | ||
|
a576c9f21f | ||
|
71a438e2cb | ||
|
272d6f2a8b | ||
|
09ed1aed93 | ||
|
53d3718028 | ||
|
2b5dce5232 | ||
|
9ad84150aa | ||
|
c0523590b4 | ||
|
c7f091ab10 | ||
|
7479e0aada | ||
|
5bbee1a1fe | ||
|
bdb9546ca3 | ||
|
46af4cad6e | ||
|
76a238912b | ||
|
909a526967 | ||
|
cd6f4fb93f | ||
|
c19458696e | ||
|
318b930e9f | ||
|
9296a078a7 | ||
|
5dc776e55f | ||
|
72d60f30f7 | ||
|
869743a742 | ||
|
7b03e07908 | ||
|
348f880e15 | ||
|
ead597d0fb | ||
|
afbf989715 | ||
|
01b62a16c3 | ||
|
c5eba04517 | ||
|
282313ab52 | ||
|
d274545e77 | ||
|
d3fda37615 | ||
|
cbe3092404 | ||
|
6dfe3039d0 | ||
|
d6009453df | ||
|
c81323ef91 | ||
|
961c27f1c2 | ||
|
fe4a14e6cc | ||
|
50848c2f4d | ||
|
d32633b3c7 | ||
|
b37739eec2 | ||
|
28f87dc804 | ||
|
41879e41e6 | ||
|
fc0a6546a2 | ||
|
ffd4280d6c | ||
|
db3b955b0f | ||
|
5516f65971 | ||
|
9471df0a1b | ||
|
6d39f64be7 | ||
|
b89d0a9a73 | ||
|
4bb779d9a5 | ||
|
386a5b6362 | ||
|
e32a999cd0 | ||
|
bfbc6a4bad | ||
|
8c9e0e552d | ||
|
8aaf9fd83f | ||
|
08057720b8 | ||
|
bfaa648837 | ||
|
d504daef91 | ||
|
b8d3ef2f49 | ||
|
3bf6320030 | ||
|
708b928c73 | ||
|
649366ff44 | ||
|
e5c9e87fad | ||
|
f3d9d707b6 | ||
|
090e10730c | ||
|
fbc84861c7 | ||
|
e763469af8 | ||
|
3c0c514e44 | ||
|
ed5e2dd332 | ||
|
09b7c6f550 | ||
|
df315a1f51 | ||
|
7ee4bb621c | ||
|
24874f4c3c | ||
|
c128880033 | ||
|
a66e94a0b0 | ||
|
56870ed4a8 | ||
|
3ac720df47 | ||
|
1bc757ad06 | ||
|
f72abc6f3d | ||
|
5ac88de985 | ||
|
0826b367d6 | ||
|
329bf861d6 | ||
|
9dcd3d18a0 | ||
|
db66cd88b6 | ||
|
86c205fe43 | ||
|
c6414138c7 | ||
|
36b355eb82 | ||
|
7be9291b13 | ||
|
ea9e75039b | ||
|
a5fb036011 | ||
|
e55506f9db | ||
|
50ec1d0445 | ||
|
3d5e1d8d91 | ||
|
db2128a344 | ||
|
21db43db06 | ||
|
5009b3029f | ||
|
57a029189c | ||
|
0cb715bb76 | ||
|
7d03823afd | ||
|
8e1c9f5042 | ||
|
980b7cda8f | ||
|
3a72dd5cb6 | ||
|
3178243811 | ||
|
d30e2f2a4f | ||
|
6226dae05c | ||
|
9c6a475a6e | ||
|
8294d10d5b | ||
|
67558bec47 | ||
|
84873d4074 | ||
|
58a0b28a39 | ||
|
b37d3a66cc | ||
|
7e495a5e27 | ||
|
c41547fd4a | ||
|
0d47d41c85 | ||
|
41a3a17456 | ||
|
cbbafbcca2 | ||
|
c75566b374 | ||
|
7279f1fcc1 | ||
|
d7432f7c10 | ||
|
b0a0a153f3 | ||
|
024632dbd0 | ||
|
0a545a28b9 | ||
|
0f2df59998 | ||
|
29a7d32f77 | ||
|
687a7e9b2f | ||
|
09e8782318 | ||
|
f2aea02210 | ||
|
194f922312 | ||
|
fea3c48098 | ||
|
c2f57baec2 | ||
|
f4a140e126 | ||
|
ab506b09fe | ||
|
87e1cdeedb | ||
|
81a36146ef | ||
|
7fa4a68a27 | ||
|
f1c5e2ef81 | ||
|
b526155cce | ||
|
62c3f301e7 | ||
|
38cb988809 | ||
|
b976ac54c8 | ||
|
78026e766f | ||
|
b4cd8d21a5 | ||
|
7552893311 | ||
|
21c896d8f8 | ||
|
4b7fe202ec | ||
|
9f4519210f | ||
|
b0506afa5b | ||
|
8cbb379898 | ||
|
b226215593 | ||
|
19970729a9 | ||
|
d2ebfd2833 | ||
|
bd782fc828 | ||
|
23560e608c | ||
|
f1377b560e | ||
|
72108684ea | ||
|
c6adaaea97 | ||
|
91999a38ca | ||
|
b34eed125d | ||
|
2abe09529a | ||
|
9aaaf4dd4b | ||
|
cbfbcf7f1b | ||
|
c7651dc40d | ||
|
eda1c471ad | ||
|
c7ef18fbc4 | ||
|
901ec918b1 | ||
|
6bdae55ee1 | ||
|
dfb96e4b7f | ||
|
ff2c316b18 | ||
|
5be52f71f9 | ||
|
42873dd37c | ||
|
f93e7d4e3a | ||
|
bbcd523967 | ||
|
68cbe58d00 | ||
|
115bca98f1 | ||
|
ed0b34b2fe | ||
|
ab34401421 | ||
|
eed0c18d65 | ||
|
e5a38ce748 | ||
|
7d9d9fcf36 | ||
|
f0aba6ceb2 | ||
|
ab07ee57c6 | ||
|
eae3d72a4d | ||
|
7b8d826704 | ||
|
e7baa42e63 | ||
|
2f32833a22 | ||
|
f6935a4b4b | ||
|
332c9e891b | ||
|
b91ee4847f | ||
|
625463d871 | ||
|
6433a01e07 | ||
|
56cc31e8e7 | ||
|
3af297aa76 | ||
|
996ec59d28 | ||
|
95593eeeab | ||
|
dad244fb7a | ||
|
adb5d27d95 | ||
|
8456a8cecb | ||
|
b9f66373c1 | ||
|
9ac365feef | ||
|
43bbd58a44 | ||
|
7feffa64f3 | ||
|
ea0977abb4 | ||
|
4c83dc7c28 | ||
|
e10ab1da78 | ||
|
1b0e60374b | ||
|
3a760fbb44 | ||
|
6ef57a2973 | ||
|
3e9c7f2e9f | ||
|
430598b7a1 | ||
|
91611b09b4 | ||
|
ecd115851f | ||
|
4a1e50fed1 | ||
|
d6d037047b | ||
|
b5734c2b20 | ||
|
723fb7eaac | ||
|
63a9acaa19 | ||
|
0524f8c677 | ||
|
70b62f272e | ||
|
f0089b7940 | ||
|
4b44280d53 | ||
|
f045382d20 | ||
|
db3fa1ade7 | ||
|
f83950fd75 | ||
|
4dd1bf920d | ||
|
98755f3621 | ||
|
c3a8a044b9 | ||
|
15b5ea43a7 | ||
|
ec683fc227 | ||
|
d4e65eb82a | ||
|
10c6601b0a | ||
|
73940bc1bd | ||
|
9b7fb829f9 | ||
|
c51d8c9021 | ||
|
d8a6dfe5ce | ||
|
5f7cef0b06 | ||
|
48ff2ffc68 | ||
|
b3b9ccd314 | ||
|
e63c7b483b | ||
|
f57980b069 | ||
|
7006aa0d2a | ||
|
8051c1ca99 | ||
|
a779592414 | ||
|
112215848d |
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
@ -7,3 +7,8 @@ updates:
|
||||
ignore:
|
||||
# Hypotehsis is only used for testing and is updated quite often
|
||||
- dependency-name: hypothesis
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
|
8
.github/workflows/ci-docker.yml
vendored
8
.github/workflows/ci-docker.yml
vendored
@ -30,15 +30,15 @@ jobs:
|
||||
arch: [amd64, armv7, aarch64]
|
||||
build_type: ["ha-addon", "docker", "lint"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set TAG
|
||||
run: |
|
||||
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -75,15 +75,15 @@ jobs:
|
||||
pio_cache_key: tidyesp32-idf
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
id: python
|
||||
with:
|
||||
python-version: '3.8'
|
||||
|
||||
- name: Cache virtualenv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }}
|
||||
@ -102,7 +102,7 @@ jobs:
|
||||
|
||||
# Use per check platformio cache because checks use different parts
|
||||
- name: Cache platformio
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
outputs:
|
||||
tag: ${{ steps.tag.outputs.tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Get tag
|
||||
id: tag
|
||||
run: |
|
||||
@ -35,9 +35,9 @@ jobs:
|
||||
if: github.repository == 'esphome/esphome' && github.event_name == 'release'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Set up python environment
|
||||
@ -65,24 +65,24 @@ jobs:
|
||||
arch: [amd64, armv7, aarch64]
|
||||
build_type: ["ha-addon", "docker", "lint"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@ -108,9 +108,9 @@ jobs:
|
||||
matrix:
|
||||
build_type: ["ha-addon", "docker", "lint"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Enable experimental manifest support
|
||||
@ -119,12 +119,12 @@ jobs:
|
||||
echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
days-before-pr-stale: 90
|
||||
days-before-pr-close: 7
|
||||
@ -35,7 +35,7 @@ jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
|
@ -2,7 +2,7 @@
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/ambv/black
|
||||
rev: 22.3.0
|
||||
rev: 22.6.0
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
@ -26,7 +26,7 @@ repos:
|
||||
- --branch=release
|
||||
- --branch=beta
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.31.1
|
||||
rev: v2.34.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
|
@ -52,6 +52,7 @@ esphome/components/cs5460a/* @balrog-kun
|
||||
esphome/components/cse7761/* @berfenger
|
||||
esphome/components/ct_clamp/* @jesserockz
|
||||
esphome/components/current_based/* @djwmarcx
|
||||
esphome/components/dac7678/* @NickB1
|
||||
esphome/components/daly_bms/* @s1lvi0
|
||||
esphome/components/dashboard_import/* @esphome/core
|
||||
esphome/components/debug/* @OttoWinter
|
||||
@ -88,6 +89,7 @@ esphome/components/honeywellabp/* @RubyBailey
|
||||
esphome/components/hrxl_maxsonar_wr/* @netmikey
|
||||
esphome/components/hydreon_rgxx/* @functionpointer
|
||||
esphome/components/i2c/* @esphome/core
|
||||
esphome/components/i2s_audio/* @jesserockz
|
||||
esphome/components/improv_serial/* @esphome/core
|
||||
esphome/components/ina260/* @MrEditor97
|
||||
esphome/components/inkbird_ibsth1_mini/* @fkirill
|
||||
@ -102,6 +104,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
|
||||
esphome/components/lock/* @esphome/core
|
||||
esphome/components/logger/* @esphome/core
|
||||
esphome/components/ltr390/* @sjtrny
|
||||
esphome/components/max31865/* @DAVe3283
|
||||
esphome/components/max44009/* @berfenger
|
||||
esphome/components/max7219digit/* @rspaargaren
|
||||
esphome/components/max9611/* @mckaymatthew
|
||||
@ -119,6 +122,7 @@ esphome/components/mcp47a1/* @jesserockz
|
||||
esphome/components/mcp9808/* @k7hpn
|
||||
esphome/components/md5/* @esphome/core
|
||||
esphome/components/mdns/* @esphome/core
|
||||
esphome/components/media_player/* @jesserockz
|
||||
esphome/components/midea/* @dudanov
|
||||
esphome/components/midea_ir/* @dudanov
|
||||
esphome/components/mitsubishi/* @RubyBailey
|
||||
|
@ -46,12 +46,10 @@ RUN \
|
||||
# Ubuntu python3-pip is missing wheel
|
||||
pip3 install --no-cache-dir \
|
||||
wheel==0.37.1 \
|
||||
platformio==5.2.5 \
|
||||
platformio==6.0.2 \
|
||||
# Change some platformio settings
|
||||
&& platformio settings set enable_telemetry No \
|
||||
&& platformio settings set check_libraries_interval 1000000 \
|
||||
&& platformio settings set check_platformio_interval 1000000 \
|
||||
&& platformio settings set check_platforms_interval 1000000 \
|
||||
&& mkdir -p /piolibs
|
||||
|
||||
|
||||
|
@ -12,7 +12,7 @@ from esphome.const import (
|
||||
CONF_TYPE_ID,
|
||||
CONF_TIME,
|
||||
)
|
||||
from esphome.jsonschema import jschema_extractor
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.util import Registry
|
||||
|
||||
|
||||
@ -23,11 +23,10 @@ def maybe_simple_id(*validators):
|
||||
def maybe_conf(conf, *validators):
|
||||
validator = cv.All(*validators)
|
||||
|
||||
@jschema_extractor("maybe")
|
||||
@schema_extractor("maybe")
|
||||
def validate(value):
|
||||
# pylint: disable=comparison-with-callable
|
||||
if value == jschema_extractor:
|
||||
return validator
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return (validator, conf)
|
||||
|
||||
if isinstance(value, dict):
|
||||
return validator(value)
|
||||
@ -111,11 +110,9 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
|
||||
# This should only happen with invalid configs, but let's have a nice error message.
|
||||
return [schema(value)]
|
||||
|
||||
@jschema_extractor("automation")
|
||||
@schema_extractor("automation")
|
||||
def validator(value):
|
||||
# hack to get the schema
|
||||
# pylint: disable=comparison-with-callable
|
||||
if value == jschema_extractor:
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return schema
|
||||
|
||||
value = validator_(value)
|
||||
|
@ -15,10 +15,21 @@ namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
static const char *const TAG = "adc";
|
||||
// 13 bits for S3 / 12 bit for all other esp32 variants
|
||||
// create a const to avoid the repated cast to enum
|
||||
|
||||
// 13bit for S2, and 12bit for all other esp32 variants
|
||||
#ifdef USE_ESP32
|
||||
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
|
||||
|
||||
#ifndef SOC_ADC_RTC_MAX_BITWIDTH
|
||||
#if USE_ESP32_VARIANT_ESP32S2
|
||||
static const int SOC_ADC_RTC_MAX_BITWIDTH = 13;
|
||||
#else
|
||||
static const int SOC_ADC_RTC_MAX_BITWIDTH = 12;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; // 4095 (12 bit) or 8191 (13 bit)
|
||||
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; // 2048 (12 bit) or 4096 (13 bit)
|
||||
#endif
|
||||
|
||||
void ADCSensor::setup() {
|
||||
@ -75,16 +86,16 @@ void ADCSensor::dump_config() {
|
||||
} else {
|
||||
switch (this->attenuation_) {
|
||||
case ADC_ATTEN_DB_0:
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)");
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: 0db");
|
||||
break;
|
||||
case ADC_ATTEN_DB_2_5:
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)");
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: 2.5db");
|
||||
break;
|
||||
case ADC_ATTEN_DB_6:
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)");
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: 6db");
|
||||
break;
|
||||
case ADC_ATTEN_DB_11:
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)");
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: 11db");
|
||||
break;
|
||||
default: // This is to satisfy the unused ADC_ATTEN_MAX
|
||||
break;
|
||||
@ -129,16 +140,16 @@ float ADCSensor::sample() {
|
||||
return mv / 1000.0f;
|
||||
}
|
||||
|
||||
int raw11, raw6 = 4095, raw2 = 4095, raw0 = 4095;
|
||||
int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
|
||||
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11);
|
||||
raw11 = adc1_get_raw(channel_);
|
||||
if (raw11 < 4095) {
|
||||
if (raw11 < ADC_MAX) {
|
||||
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6);
|
||||
raw6 = adc1_get_raw(channel_);
|
||||
if (raw6 < 4095) {
|
||||
if (raw6 < ADC_MAX) {
|
||||
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5);
|
||||
raw2 = adc1_get_raw(channel_);
|
||||
if (raw2 < 4095) {
|
||||
if (raw2 < ADC_MAX) {
|
||||
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0);
|
||||
raw0 = adc1_get_raw(channel_);
|
||||
}
|
||||
@ -154,15 +165,15 @@ float ADCSensor::sample() {
|
||||
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]);
|
||||
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]);
|
||||
|
||||
// Contribution of each value, in range 0-2048
|
||||
uint32_t c11 = std::min(raw11, 2048);
|
||||
uint32_t c6 = 2048 - std::abs(raw6 - 2048);
|
||||
uint32_t c2 = 2048 - std::abs(raw2 - 2048);
|
||||
uint32_t c0 = std::min(4095 - raw0, 2048);
|
||||
// max theoretical csum value is 2048*4 = 8192
|
||||
// Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC)
|
||||
uint32_t c11 = std::min(raw11, ADC_HALF);
|
||||
uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF);
|
||||
uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF);
|
||||
uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF);
|
||||
// max theoretical csum value is 4096*4 = 16384
|
||||
uint32_t csum = c11 + c6 + c2 + c0;
|
||||
|
||||
// each mv is max 3900; so max value is 3900*2048*4, fits in unsigned
|
||||
// each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32
|
||||
uint32_t mv_scaled = (mv11 * c11) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0);
|
||||
return mv_scaled / (float) (csum * 1000U);
|
||||
}
|
||||
|
@ -40,6 +40,8 @@ class AddressableLightDisplay : public display::DisplayBuffer, public PollingCom
|
||||
void setup() override;
|
||||
void display();
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
protected:
|
||||
int get_width_internal() override;
|
||||
int get_height_internal() override;
|
||||
|
@ -76,6 +76,8 @@ async def to_code(config):
|
||||
pos = 0
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
if CONF_RESIZE in config:
|
||||
image.thumbnail(config[CONF_RESIZE])
|
||||
frame = image.convert("RGB")
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
|
@ -92,7 +92,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
|
||||
}
|
||||
if (this->codec_->has_unit()) {
|
||||
this->fahrenheit_ = (this->codec_->unit_ == 'f');
|
||||
ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celcius");
|
||||
ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celsius");
|
||||
this->current_request_++;
|
||||
}
|
||||
this->publish_state();
|
||||
|
@ -42,6 +42,7 @@ service APIConnection {
|
||||
rpc select_command (SelectCommandRequest) returns (void) {}
|
||||
rpc button_command (ButtonCommandRequest) returns (void) {}
|
||||
rpc lock_command (LockCommandRequest) returns (void) {}
|
||||
rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
|
||||
}
|
||||
|
||||
|
||||
@ -991,7 +992,7 @@ message ListEntitiesLockResponse {
|
||||
bool supports_open = 9;
|
||||
bool requires_code = 10;
|
||||
|
||||
# Not yet implemented:
|
||||
// Not yet implemented:
|
||||
string code_format = 11;
|
||||
}
|
||||
message LockStateResponse {
|
||||
@ -1010,7 +1011,7 @@ message LockCommandRequest {
|
||||
fixed32 key = 1;
|
||||
LockCommand command = 2;
|
||||
|
||||
# Not yet implemented:
|
||||
// Not yet implemented:
|
||||
bool has_code = 3;
|
||||
string code = 4;
|
||||
}
|
||||
@ -1040,3 +1041,60 @@ message ButtonCommandRequest {
|
||||
fixed32 key = 1;
|
||||
}
|
||||
|
||||
// ==================== MEDIA PLAYER ====================
|
||||
enum MediaPlayerState {
|
||||
MEDIA_PLAYER_STATE_NONE = 0;
|
||||
MEDIA_PLAYER_STATE_IDLE = 1;
|
||||
MEDIA_PLAYER_STATE_PLAYING = 2;
|
||||
MEDIA_PLAYER_STATE_PAUSED = 3;
|
||||
}
|
||||
enum MediaPlayerCommand {
|
||||
MEDIA_PLAYER_COMMAND_PLAY = 0;
|
||||
MEDIA_PLAYER_COMMAND_PAUSE = 1;
|
||||
MEDIA_PLAYER_COMMAND_STOP = 2;
|
||||
MEDIA_PLAYER_COMMAND_MUTE = 3;
|
||||
MEDIA_PLAYER_COMMAND_UNMUTE = 4;
|
||||
}
|
||||
message ListEntitiesMediaPlayerResponse {
|
||||
option (id) = 63;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_MEDIA_PLAYER";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string unique_id = 4;
|
||||
|
||||
string icon = 5;
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
|
||||
bool supports_pause = 8;
|
||||
}
|
||||
message MediaPlayerStateResponse {
|
||||
option (id) = 64;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_MEDIA_PLAYER";
|
||||
option (no_delay) = true;
|
||||
fixed32 key = 1;
|
||||
MediaPlayerState state = 2;
|
||||
float volume = 3;
|
||||
bool muted = 4;
|
||||
}
|
||||
message MediaPlayerCommandRequest {
|
||||
option (id) = 65;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_MEDIA_PLAYER";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
|
||||
bool has_command = 2;
|
||||
MediaPlayerCommand command = 3;
|
||||
|
||||
bool has_volume = 4;
|
||||
float volume = 5;
|
||||
|
||||
bool has_media_url = 6;
|
||||
string media_url = 7;
|
||||
}
|
||||
|
@ -733,6 +733,52 @@ void APIConnection::lock_command(const LockCommandRequest &msg) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) {
|
||||
if (!this->state_subscription_)
|
||||
return false;
|
||||
|
||||
MediaPlayerStateResponse resp{};
|
||||
resp.key = media_player->get_object_id_hash();
|
||||
resp.state = static_cast<enums::MediaPlayerState>(media_player->state);
|
||||
resp.volume = media_player->volume;
|
||||
resp.muted = media_player->is_muted();
|
||||
return this->send_media_player_state_response(resp);
|
||||
}
|
||||
bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) {
|
||||
ListEntitiesMediaPlayerResponse msg;
|
||||
msg.key = media_player->get_object_id_hash();
|
||||
msg.object_id = media_player->get_object_id();
|
||||
msg.name = media_player->get_name();
|
||||
msg.unique_id = get_default_unique_id("media_player", media_player);
|
||||
msg.icon = media_player->get_icon();
|
||||
msg.disabled_by_default = media_player->is_disabled_by_default();
|
||||
msg.entity_category = static_cast<enums::EntityCategory>(media_player->get_entity_category());
|
||||
|
||||
auto traits = media_player->get_traits();
|
||||
msg.supports_pause = traits.get_supports_pause();
|
||||
|
||||
return this->send_list_entities_media_player_response(msg);
|
||||
}
|
||||
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
|
||||
media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key);
|
||||
if (media_player == nullptr)
|
||||
return;
|
||||
|
||||
auto call = media_player->make_call();
|
||||
if (msg.has_command) {
|
||||
call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command));
|
||||
}
|
||||
if (msg.has_volume) {
|
||||
call.set_volume(msg.volume);
|
||||
}
|
||||
if (msg.has_media_url) {
|
||||
call.set_media_url(msg.media_url);
|
||||
}
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
|
||||
if (!this->state_subscription_)
|
||||
|
@ -82,6 +82,11 @@ class APIConnection : public APIServerConnection {
|
||||
bool send_lock_state(lock::Lock *a_lock, lock::LockState state);
|
||||
bool send_lock_info(lock::Lock *a_lock);
|
||||
void lock_command(const LockCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool send_media_player_state(media_player::MediaPlayer *media_player);
|
||||
bool send_media_player_info(media_player::MediaPlayer *media_player);
|
||||
void media_player_command(const MediaPlayerCommandRequest &msg) override;
|
||||
#endif
|
||||
bool send_log_message(int level, const char *tag, const char *line);
|
||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
||||
|
@ -270,7 +270,7 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
|
||||
*
|
||||
* If the handshake is still active when this method returns and a read/write can't take place at
|
||||
* the moment, returns WOULD_BLOCK.
|
||||
* If an error occured, returns that error. Only returns OK if the transport is ready for data
|
||||
* If an error occurred, returns that error. Only returns OK if the transport is ready for data
|
||||
* traffic.
|
||||
*/
|
||||
APIError APINoiseFrameHelper::state_action_() {
|
||||
@ -586,7 +586,7 @@ APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
|
||||
}
|
||||
return APIError::OK;
|
||||
} else if (sent == -1) {
|
||||
// an error occured
|
||||
// an error occurred
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Socket write failed with errno %d", errno);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
@ -980,7 +980,7 @@ APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt
|
||||
}
|
||||
return APIError::OK;
|
||||
} else if (sent == -1) {
|
||||
// an error occured
|
||||
// an error occurred
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Socket write failed with errno %d", errno);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
|
@ -308,6 +308,36 @@ template<> const char *proto_enum_to_string<enums::LockCommand>(enums::LockComma
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::MediaPlayerState>(enums::MediaPlayerState value) {
|
||||
switch (value) {
|
||||
case enums::MEDIA_PLAYER_STATE_NONE:
|
||||
return "MEDIA_PLAYER_STATE_NONE";
|
||||
case enums::MEDIA_PLAYER_STATE_IDLE:
|
||||
return "MEDIA_PLAYER_STATE_IDLE";
|
||||
case enums::MEDIA_PLAYER_STATE_PLAYING:
|
||||
return "MEDIA_PLAYER_STATE_PLAYING";
|
||||
case enums::MEDIA_PLAYER_STATE_PAUSED:
|
||||
return "MEDIA_PLAYER_STATE_PAUSED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::MediaPlayerCommand>(enums::MediaPlayerCommand value) {
|
||||
switch (value) {
|
||||
case enums::MEDIA_PLAYER_COMMAND_PLAY:
|
||||
return "MEDIA_PLAYER_COMMAND_PLAY";
|
||||
case enums::MEDIA_PLAYER_COMMAND_PAUSE:
|
||||
return "MEDIA_PLAYER_COMMAND_PAUSE";
|
||||
case enums::MEDIA_PLAYER_COMMAND_STOP:
|
||||
return "MEDIA_PLAYER_COMMAND_STOP";
|
||||
case enums::MEDIA_PLAYER_COMMAND_MUTE:
|
||||
return "MEDIA_PLAYER_COMMAND_MUTE";
|
||||
case enums::MEDIA_PLAYER_COMMAND_UNMUTE:
|
||||
return "MEDIA_PLAYER_COMMAND_UNMUTE";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
@ -4574,6 +4604,254 @@ void ButtonCommandRequest::dump_to(std::string &out) const {
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 6: {
|
||||
this->disabled_by_default = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 7: {
|
||||
this->entity_category = value.as_enum<enums::EntityCategory>();
|
||||
return true;
|
||||
}
|
||||
case 8: {
|
||||
this->supports_pause = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->object_id = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 3: {
|
||||
this->name = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 4: {
|
||||
this->unique_id = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 5: {
|
||||
this->icon = value.as_string();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool ListEntitiesMediaPlayerResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->key = value.as_fixed32();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->object_id);
|
||||
buffer.encode_fixed32(2, this->key);
|
||||
buffer.encode_string(3, this->name);
|
||||
buffer.encode_string(4, this->unique_id);
|
||||
buffer.encode_string(5, this->icon);
|
||||
buffer.encode_bool(6, this->disabled_by_default);
|
||||
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
|
||||
buffer.encode_bool(8, this->supports_pause);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("ListEntitiesMediaPlayerResponse {\n");
|
||||
out.append(" object_id: ");
|
||||
out.append("'").append(this->object_id).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" key: ");
|
||||
sprintf(buffer, "%u", this->key);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" name: ");
|
||||
out.append("'").append(this->name).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" unique_id: ");
|
||||
out.append("'").append(this->unique_id).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" icon: ");
|
||||
out.append("'").append(this->icon).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" disabled_by_default: ");
|
||||
out.append(YESNO(this->disabled_by_default));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" entity_category: ");
|
||||
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" supports_pause: ");
|
||||
out.append(YESNO(this->supports_pause));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool MediaPlayerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->state = value.as_enum<enums::MediaPlayerState>();
|
||||
return true;
|
||||
}
|
||||
case 4: {
|
||||
this->muted = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool MediaPlayerStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->key = value.as_fixed32();
|
||||
return true;
|
||||
}
|
||||
case 3: {
|
||||
this->volume = value.as_float();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_fixed32(1, this->key);
|
||||
buffer.encode_enum<enums::MediaPlayerState>(2, this->state);
|
||||
buffer.encode_float(3, this->volume);
|
||||
buffer.encode_bool(4, this->muted);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void MediaPlayerStateResponse::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("MediaPlayerStateResponse {\n");
|
||||
out.append(" key: ");
|
||||
sprintf(buffer, "%u", this->key);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" state: ");
|
||||
out.append(proto_enum_to_string<enums::MediaPlayerState>(this->state));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" volume: ");
|
||||
sprintf(buffer, "%g", this->volume);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" muted: ");
|
||||
out.append(YESNO(this->muted));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->has_command = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 3: {
|
||||
this->command = value.as_enum<enums::MediaPlayerCommand>();
|
||||
return true;
|
||||
}
|
||||
case 4: {
|
||||
this->has_volume = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 6: {
|
||||
this->has_media_url = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 7: {
|
||||
this->media_url = value.as_string();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->key = value.as_fixed32();
|
||||
return true;
|
||||
}
|
||||
case 5: {
|
||||
this->volume = value.as_float();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void MediaPlayerCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_fixed32(1, this->key);
|
||||
buffer.encode_bool(2, this->has_command);
|
||||
buffer.encode_enum<enums::MediaPlayerCommand>(3, this->command);
|
||||
buffer.encode_bool(4, this->has_volume);
|
||||
buffer.encode_float(5, this->volume);
|
||||
buffer.encode_bool(6, this->has_media_url);
|
||||
buffer.encode_string(7, this->media_url);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void MediaPlayerCommandRequest::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("MediaPlayerCommandRequest {\n");
|
||||
out.append(" key: ");
|
||||
sprintf(buffer, "%u", this->key);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" has_command: ");
|
||||
out.append(YESNO(this->has_command));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" command: ");
|
||||
out.append(proto_enum_to_string<enums::MediaPlayerCommand>(this->command));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" has_volume: ");
|
||||
out.append(YESNO(this->has_volume));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" volume: ");
|
||||
sprintf(buffer, "%g", this->volume);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" has_media_url: ");
|
||||
out.append(YESNO(this->has_media_url));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" media_url: ");
|
||||
out.append("'").append(this->media_url).append("'");
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
|
@ -141,6 +141,19 @@ enum LockCommand : uint32_t {
|
||||
LOCK_LOCK = 1,
|
||||
LOCK_OPEN = 2,
|
||||
};
|
||||
enum MediaPlayerState : uint32_t {
|
||||
MEDIA_PLAYER_STATE_NONE = 0,
|
||||
MEDIA_PLAYER_STATE_IDLE = 1,
|
||||
MEDIA_PLAYER_STATE_PLAYING = 2,
|
||||
MEDIA_PLAYER_STATE_PAUSED = 3,
|
||||
};
|
||||
enum MediaPlayerCommand : uint32_t {
|
||||
MEDIA_PLAYER_COMMAND_PLAY = 0,
|
||||
MEDIA_PLAYER_COMMAND_PAUSE = 1,
|
||||
MEDIA_PLAYER_COMMAND_STOP = 2,
|
||||
MEDIA_PLAYER_COMMAND_MUTE = 3,
|
||||
MEDIA_PLAYER_COMMAND_UNMUTE = 4,
|
||||
};
|
||||
|
||||
} // namespace enums
|
||||
|
||||
@ -1146,6 +1159,60 @@ class ButtonCommandRequest : public ProtoMessage {
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
};
|
||||
class ListEntitiesMediaPlayerResponse : public ProtoMessage {
|
||||
public:
|
||||
std::string object_id{};
|
||||
uint32_t key{0};
|
||||
std::string name{};
|
||||
std::string unique_id{};
|
||||
std::string icon{};
|
||||
bool disabled_by_default{false};
|
||||
enums::EntityCategory entity_category{};
|
||||
bool supports_pause{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class MediaPlayerStateResponse : public ProtoMessage {
|
||||
public:
|
||||
uint32_t key{0};
|
||||
enums::MediaPlayerState state{};
|
||||
float volume{0.0f};
|
||||
bool muted{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class MediaPlayerCommandRequest : public ProtoMessage {
|
||||
public:
|
||||
uint32_t key{0};
|
||||
bool has_command{false};
|
||||
enums::MediaPlayerCommand command{};
|
||||
bool has_volume{false};
|
||||
float volume{0.0f};
|
||||
bool has_media_url{false};
|
||||
std::string media_url{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
|
@ -310,6 +310,24 @@ bool APIServerConnectionBase::send_list_entities_button_response(const ListEntit
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool APIServerConnectionBase::send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "send_list_entities_media_player_response: %s", msg.dump().c_str());
|
||||
#endif
|
||||
return this->send_message_<ListEntitiesMediaPlayerResponse>(msg, 63);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool APIServerConnectionBase::send_media_player_state_response(const MediaPlayerStateResponse &msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "send_media_player_state_response: %s", msg.dump().c_str());
|
||||
#endif
|
||||
return this->send_message_<MediaPlayerStateResponse>(msg, 64);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
#endif
|
||||
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
|
||||
switch (msg_type) {
|
||||
case 1: {
|
||||
@ -563,6 +581,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
this->on_button_command_request(msg);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
case 65: {
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
MediaPlayerCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
this->on_media_player_command_request(msg);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
@ -813,6 +842,19 @@ void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg)
|
||||
this->lock_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->media_player_command(msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
|
@ -144,6 +144,15 @@ class APIServerConnectionBase : public ProtoService {
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
virtual void on_button_command_request(const ButtonCommandRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg);
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool send_media_player_state_response(const MediaPlayerStateResponse &msg);
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){};
|
||||
#endif
|
||||
protected:
|
||||
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
|
||||
@ -192,6 +201,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
virtual void lock_command(const LockCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
|
||||
#endif
|
||||
protected:
|
||||
void on_hello_request(const HelloRequest &msg) override;
|
||||
@ -236,6 +248,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_LOCK
|
||||
void on_lock_command_request(const LockCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
|
@ -272,6 +272,15 @@ void APIServer::on_lock_update(lock::Lock *obj) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void APIServer::on_media_player_update(media_player::MediaPlayer *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_media_player_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
|
||||
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
@ -68,6 +68,9 @@ class APIServer : public Component, public Controller {
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void on_lock_update(lock::Lock *obj) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
||||
#endif
|
||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
|
||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||
|
@ -64,5 +64,11 @@ bool ListEntitiesIterator::on_number(number::Number *number) { return this->clie
|
||||
bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) {
|
||||
return this->client_->send_media_player_info(media_player);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
|
@ -51,6 +51,9 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool on_lock(lock::Lock *a_lock) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool on_media_player(media_player::MediaPlayer *media_player) override;
|
||||
#endif
|
||||
bool on_end() override;
|
||||
|
||||
|
@ -50,6 +50,11 @@ bool InitialStateIterator::on_select(select::Select *select) {
|
||||
#ifdef USE_LOCK
|
||||
bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); }
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) {
|
||||
return this->client_->send_media_player_state(media_player);
|
||||
}
|
||||
#endif
|
||||
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
|
||||
|
||||
} // namespace api
|
||||
|
@ -48,6 +48,9 @@ class InitialStateIterator : public ComponentIterator {
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool on_lock(lock::Lock *a_lock) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool on_media_player(media_player::MediaPlayer *media_player) override;
|
||||
#endif
|
||||
protected:
|
||||
APIConnection *client_;
|
||||
|
@ -36,6 +36,14 @@ static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
static BedjetButton heat_button(BedjetHeatMode mode) {
|
||||
BedjetButton btn = BTN_HEAT;
|
||||
if (mode == HEAT_MODE_EXTENDED) {
|
||||
btn = BTN_EXTHT;
|
||||
}
|
||||
return btn;
|
||||
}
|
||||
|
||||
void Bedjet::upgrade_firmware() {
|
||||
auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE);
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
@ -117,7 +125,7 @@ void Bedjet::control(const ClimateCall &call) {
|
||||
pkt = this->codec_->get_button_request(BTN_OFF);
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
pkt = this->codec_->get_button_request(BTN_HEAT);
|
||||
pkt = this->codec_->get_button_request(heat_button(this->heating_mode_));
|
||||
break;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
pkt = this->codec_->get_button_request(BTN_COOL);
|
||||
@ -186,6 +194,8 @@ void Bedjet::control(const ClimateCall &call) {
|
||||
pkt = this->codec_->get_button_request(BTN_M2);
|
||||
} else if (preset == "M3") {
|
||||
pkt = this->codec_->get_button_request(BTN_M3);
|
||||
} else if (preset == "LTD HT") {
|
||||
pkt = this->codec_->get_button_request(BTN_HEAT);
|
||||
} else if (preset == "EXT HT") {
|
||||
pkt = this->codec_->get_button_request(BTN_EXTHT);
|
||||
} else {
|
||||
@ -298,7 +308,7 @@ void Bedjet::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
|
||||
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_.has_value()) {
|
||||
this->send_local_time_();
|
||||
this->send_local_time();
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
@ -453,40 +463,47 @@ uint8_t Bedjet::write_notify_config_descriptor_(bool enable) {
|
||||
|
||||
#ifdef USE_TIME
|
||||
/** Attempts to sync the local time (via `time_id`) to the BedJet device. */
|
||||
void Bedjet::send_local_time_() {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
auto *time_id = *this->time_id_;
|
||||
time::ESPTime now = time_id->now();
|
||||
if (now.is_valid()) {
|
||||
uint8_t hour = now.hour;
|
||||
uint8_t minute = now.minute;
|
||||
BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute);
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute);
|
||||
void Bedjet::send_local_time() {
|
||||
if (this->time_id_.has_value()) {
|
||||
auto *time_id = *this->time_id_;
|
||||
time::ESPTime now = 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);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Initializes time sync callbacks to support syncing current time to the BedJet. */
|
||||
void Bedjet::setup_time_() {
|
||||
if (this->time_id_.has_value()) {
|
||||
this->send_local_time_();
|
||||
this->send_local_time();
|
||||
auto *time_id = *this->time_id_;
|
||||
time_id->add_on_time_sync_callback([this] { this->send_local_time_(); });
|
||||
time::ESPTime now = time_id->now();
|
||||
ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute);
|
||||
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.");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/** Attempt to set the BedJet device's clock to the specified time. */
|
||||
void Bedjet::set_clock(uint8_t hour, uint8_t minute) {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute);
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute);
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes one BedjetPacket to the BLE client on the BEDJET_COMMAND_UUID. */
|
||||
uint8_t Bedjet::write_bedjet_packet_(BedjetPacket *pkt) {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
@ -557,11 +574,25 @@ bool Bedjet::update_status_() {
|
||||
break;
|
||||
|
||||
case MODE_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
this->action = climate::CLIMATE_ACTION_HEATING;
|
||||
this->preset.reset();
|
||||
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
||||
this->set_custom_preset_("LTD HT");
|
||||
} else {
|
||||
this->custom_preset.reset();
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_EXTHT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
this->action = climate::CLIMATE_ACTION_HEATING;
|
||||
this->custom_preset.reset();
|
||||
this->preset.reset();
|
||||
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
||||
this->custom_preset.reset();
|
||||
} else {
|
||||
this->set_custom_preset_("EXT HT");
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_COOL:
|
||||
|
@ -38,8 +38,12 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
|
||||
|
||||
#ifdef USE_TIME
|
||||
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
|
||||
void send_local_time();
|
||||
#endif
|
||||
void set_clock(uint8_t hour, uint8_t minute);
|
||||
void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; }
|
||||
/** Sets the default strategy to use for climate::CLIMATE_MODE_HEAT. */
|
||||
void set_heating_mode(BedjetHeatMode mode) { this->heating_mode_ = mode; }
|
||||
|
||||
/** Attempts to check for and apply firmware updates. */
|
||||
void upgrade_firmware();
|
||||
@ -74,6 +78,11 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
|
||||
"M2",
|
||||
"M3",
|
||||
});
|
||||
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
||||
traits.add_supported_custom_preset("LTD HT");
|
||||
} else {
|
||||
traits.add_supported_custom_preset("EXT HT");
|
||||
}
|
||||
traits.set_visual_min_temperature(19.0);
|
||||
traits.set_visual_max_temperature(43.0);
|
||||
traits.set_visual_temperature_step(1.0);
|
||||
@ -85,11 +94,11 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
|
||||
|
||||
#ifdef USE_TIME
|
||||
void setup_time_();
|
||||
void send_local_time_();
|
||||
optional<time::RealTimeClock *> time_id_{};
|
||||
#endif
|
||||
|
||||
uint32_t timeout_{DEFAULT_STATUS_TIMEOUT};
|
||||
BedjetHeatMode heating_mode_ = HEAT_MODE_HEAT;
|
||||
|
||||
static const uint32_t MIN_NOTIFY_THROTTLE = 5000;
|
||||
static const uint32_t NOTIFY_WARN_THRESHOLD = 300000;
|
||||
|
@ -24,6 +24,14 @@ enum BedjetMode : uint8_t {
|
||||
MODE_WAIT = 6,
|
||||
};
|
||||
|
||||
/** Optional heating strategies to use for climate::CLIMATE_MODE_HEAT. */
|
||||
enum BedjetHeatMode {
|
||||
/// HVACMode.HEAT is handled using BTN_HEAT (default)
|
||||
HEAT_MODE_HEAT,
|
||||
/// HVACMode.HEAT is handled using BTN_EXTHT
|
||||
HEAT_MODE_EXTENDED,
|
||||
};
|
||||
|
||||
enum BedjetButton : uint8_t {
|
||||
/// Turn BedJet off
|
||||
BTN_OFF = 0x1,
|
||||
|
@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import climate, ble_client, time
|
||||
from esphome.const import (
|
||||
CONF_HEAT_MODE,
|
||||
CONF_ID,
|
||||
CONF_RECEIVE_TIMEOUT,
|
||||
CONF_TIME_ID,
|
||||
@ -14,11 +15,19 @@ bedjet_ns = cg.esphome_ns.namespace("bedjet")
|
||||
Bedjet = bedjet_ns.class_(
|
||||
"Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent
|
||||
)
|
||||
BedjetHeatMode = bedjet_ns.enum("BedjetHeatMode")
|
||||
BEDJET_HEAT_MODES = {
|
||||
"heat": BedjetHeatMode.HEAT_MODE_HEAT,
|
||||
"extended": BedjetHeatMode.HEAT_MODE_EXTENDED,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
climate.CLIMATE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Bedjet),
|
||||
cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum(
|
||||
BEDJET_HEAT_MODES, lower=True
|
||||
),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
cv.Optional(
|
||||
CONF_RECEIVE_TIMEOUT, default="0s"
|
||||
@ -35,6 +44,7 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
await climate.register_climate(var, config)
|
||||
await ble_client.register_ble_node(var, config)
|
||||
cg.add(var.set_heating_mode(config[CONF_HEAT_MODE]))
|
||||
if CONF_TIME_ID in config:
|
||||
time_ = await cg.get_variable(config[CONF_TIME_ID])
|
||||
cg.add(var.set_time_id(time_))
|
||||
|
@ -69,7 +69,6 @@ void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
|
||||
}
|
||||
}
|
||||
bool BinarySensor::has_state() const { return this->has_state_; }
|
||||
uint32_t BinarySensor::hash_base() { return 1210250844UL; }
|
||||
bool BinarySensor::is_status_binary_sensor() const { return false; }
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
@ -76,8 +76,6 @@ class BinarySensor : public EntityBase {
|
||||
virtual std::string device_class();
|
||||
|
||||
protected:
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void(bool)> state_callback_{};
|
||||
optional<std::string> device_class_{}; ///< Stores the override of the device class
|
||||
Filter *filter_list_{nullptr};
|
||||
|
@ -7,7 +7,7 @@ namespace bl0939 {
|
||||
static const char *const TAG = "bl0939";
|
||||
|
||||
// https://www.belling.com.cn/media/file_object/bel_product/BL0939/datasheet/BL0939_V1.2_cn.pdf
|
||||
// (unfortunatelly chinese, but the protocol can be understood with some translation tool)
|
||||
// (unfortunately chinese, but the protocol can be understood with some translation tool)
|
||||
static const uint8_t BL0939_READ_COMMAND = 0x55; // 0x5{A4,A3,A2,A1}
|
||||
static const uint8_t BL0939_FULL_PACKET = 0xAA;
|
||||
static const uint8_t BL0939_PACKET_HEADER = 0x55;
|
||||
|
@ -8,7 +8,7 @@ namespace esphome {
|
||||
namespace bl0939 {
|
||||
|
||||
// https://datasheet.lcsc.com/lcsc/2108071830_BL-Shanghai-Belling-BL0939_C2841044.pdf
|
||||
// (unfortunatelly chinese, but the formulas can be easily understood)
|
||||
// (unfortunately chinese, but the formulas can be easily understood)
|
||||
// Sonoff Dual R3 V2 has the exact same resistor values for the current shunts (RL=1miliOhm)
|
||||
// and for the voltage divider (R1=0.51kOhm, R2=5*390kOhm)
|
||||
// as in the manufacturer's reference circuit, so the same formulas were used here (Vref=1.218V)
|
||||
|
@ -113,6 +113,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
|
||||
}
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str());
|
||||
this->conn_id = param->open.conn_id;
|
||||
if (param->open.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "connect to %s failed, status=%d", this->address_str().c_str(), param->open.status);
|
||||
this->set_states_(espbt::ClientState::IDLE);
|
||||
@ -122,7 +123,10 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
|
||||
}
|
||||
case ESP_GATTC_CONNECT_EVT: {
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_CONNECT_EVT", this->address_str().c_str());
|
||||
this->conn_id = param->connect.conn_id;
|
||||
if (this->conn_id != param->connect.conn_id) {
|
||||
ESP_LOGD(TAG, "[%s] Unexpected conn_id in CONNECT_EVT: param conn=%d, open conn=%d",
|
||||
this->address_str().c_str(), param->connect.conn_id, this->conn_id);
|
||||
}
|
||||
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->connect.conn_id);
|
||||
if (ret) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%x", ret);
|
||||
@ -183,9 +187,10 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
|
||||
descr->uuid.to_string().c_str());
|
||||
break;
|
||||
}
|
||||
uint8_t notify_en = 1;
|
||||
auto status = esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en),
|
||||
¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
uint16_t notify_en = 1;
|
||||
auto status =
|
||||
esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en),
|
||||
(uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status);
|
||||
}
|
||||
|
@ -11,8 +11,6 @@ namespace ble_client {
|
||||
|
||||
static const char *const TAG = "ble_sensor";
|
||||
|
||||
uint32_t BLESensor::hash_base() { return 343459825UL; }
|
||||
|
||||
void BLESensor::loop() {}
|
||||
|
||||
void BLESensor::dump_config() {
|
||||
|
@ -37,7 +37,6 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
|
||||
uint16_t handle;
|
||||
|
||||
protected:
|
||||
uint32_t hash_base() override;
|
||||
float parse_data_(uint8_t *value, uint16_t value_len);
|
||||
optional<data_to_value_t> data_to_value_func_{};
|
||||
bool notify_;
|
||||
|
@ -14,8 +14,6 @@ static const char *const TAG = "ble_text_sensor";
|
||||
|
||||
static const std::string EMPTY = "";
|
||||
|
||||
uint32_t BLETextSensor::hash_base() { return 193967603UL; }
|
||||
|
||||
void BLETextSensor::loop() {}
|
||||
|
||||
void BLETextSensor::dump_config() {
|
||||
|
@ -35,7 +35,6 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p
|
||||
uint16_t handle;
|
||||
|
||||
protected:
|
||||
uint32_t hash_base() override;
|
||||
bool notify_;
|
||||
espbt::ESPBTUUID service_uuid_;
|
||||
espbt::ESPBTUUID char_uuid_;
|
||||
|
@ -25,7 +25,7 @@ OVERSAMPLING_OPTIONS = {
|
||||
"4X": Oversampling.OVERSAMPLING_X4,
|
||||
"8X": Oversampling.OVERSAMPLING_X8,
|
||||
"16X": Oversampling.OVERSAMPLING_X16,
|
||||
"32x": Oversampling.OVERSAMPLING_X32,
|
||||
"32X": Oversampling.OVERSAMPLING_X32,
|
||||
}
|
||||
|
||||
IIRFilter = bmp3xx_ns.enum("IIRFilter")
|
||||
|
@ -15,7 +15,6 @@ void Button::press() {
|
||||
this->press_callback_.call();
|
||||
}
|
||||
void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); }
|
||||
uint32_t Button::hash_base() { return 1495763804UL; }
|
||||
|
||||
void Button::set_device_class(const std::string &device_class) { this->device_class_ = device_class; }
|
||||
std::string Button::get_device_class() { return this->device_class_; }
|
||||
|
@ -47,8 +47,6 @@ class Button : public EntityBase {
|
||||
*/
|
||||
virtual void press_action() = 0;
|
||||
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void()> press_callback_{};
|
||||
std::string device_class_{};
|
||||
};
|
||||
|
@ -419,7 +419,6 @@ void Climate::publish_state() {
|
||||
// Save state
|
||||
this->save_state_();
|
||||
}
|
||||
uint32_t Climate::hash_base() { return 3104134496UL; }
|
||||
|
||||
ClimateTraits Climate::get_traits() {
|
||||
auto traits = this->traits();
|
||||
|
@ -282,7 +282,6 @@ class Climate : public EntityBase {
|
||||
*/
|
||||
void save_state_();
|
||||
|
||||
uint32_t hash_base() override;
|
||||
void dump_traits_(const char *tag);
|
||||
|
||||
CallbackManager<void()> state_callback_{};
|
||||
|
@ -33,8 +33,6 @@ const char *cover_operation_to_str(CoverOperation op) {
|
||||
|
||||
Cover::Cover(const std::string &name) : EntityBase(name), position{COVER_OPEN} {}
|
||||
|
||||
uint32_t Cover::hash_base() { return 1727367479UL; }
|
||||
|
||||
CoverCall::CoverCall(Cover *parent) : parent_(parent) {}
|
||||
CoverCall &CoverCall::set_command(const char *command) {
|
||||
if (strcasecmp(command, "OPEN") == 0) {
|
||||
|
@ -177,7 +177,6 @@ class Cover : public EntityBase {
|
||||
virtual std::string device_class();
|
||||
|
||||
optional<CoverRestoreState> restore_state_();
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void()> state_callback_{};
|
||||
optional<std::string> device_class_override_{};
|
||||
|
@ -131,7 +131,7 @@ void CurrentBasedCover::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f);
|
||||
ESP_LOGCONFIG(TAG, "Obstacle Rollback: %.1f%%", this->obstacle_rollback_ * 100);
|
||||
if (this->max_duration_ != UINT32_MAX) {
|
||||
ESP_LOGCONFIG(TAG, "Maximun duration: %.1fs", this->max_duration_ / 1e3f);
|
||||
ESP_LOGCONFIG(TAG, "Maximum duration: %.1fs", this->max_duration_ / 1e3f);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "Start sensing delay: %.1fs", this->start_sensing_delay_ / 1e3f);
|
||||
ESP_LOGCONFIG(TAG, "Malfunction detection: %s", YESNO(this->malfunction_detection_));
|
||||
|
32
esphome/components/dac7678/__init__.py
Normal file
32
esphome/components/dac7678/__init__.py
Normal file
@ -0,0 +1,32 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
AUTO_LOAD = ["output"]
|
||||
CODEOWNERS = ["@NickB1"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
MULTI_CONF = True
|
||||
|
||||
dac7678_ns = cg.esphome_ns.namespace("dac7678")
|
||||
DAC7678Output = dac7678_ns.class_("DAC7678Output", cg.Component, i2c.I2CDevice)
|
||||
CONF_INTERNAL_REFERENCE = "internal_reference"
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DAC7678Output),
|
||||
cv.Optional(CONF_INTERNAL_REFERENCE, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x48))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_internal_reference(config[CONF_INTERNAL_REFERENCE]))
|
||||
await i2c.register_i2c_device(var, config)
|
||||
return var
|
83
esphome/components/dac7678/dac7678_output.cpp
Normal file
83
esphome/components/dac7678/dac7678_output.cpp
Normal file
@ -0,0 +1,83 @@
|
||||
#include "dac7678_output.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace dac7678 {
|
||||
|
||||
static const char *const TAG = "dac7678";
|
||||
|
||||
static const uint8_t DAC7678_REG_INPUT_N = 0x00;
|
||||
static const uint8_t DAC7678_REG_SELECT_UPDATE_N = 0x10;
|
||||
static const uint8_t DAC7678_REG_WRITE_N_UPDATE_ALL = 0x20;
|
||||
static const uint8_t DAC7678_REG_WRITE_N_UPDATE_N = 0x30;
|
||||
static const uint8_t DAC7678_REG_POWER = 0x40;
|
||||
static const uint8_t DAC7678_REG_CLEAR_CODE = 0x50;
|
||||
static const uint8_t DAC7678_REG_LDAC = 0x60;
|
||||
static const uint8_t DAC7678_REG_SOFTWARE_RESET = 0x70;
|
||||
static const uint8_t DAC7678_REG_INTERNAL_REF_0 = 0x80;
|
||||
static const uint8_t DAC7678_REG_INTERNAL_REF_1 = 0x90;
|
||||
|
||||
void DAC7678Output::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up DAC7678OutputComponent...");
|
||||
|
||||
ESP_LOGV(TAG, "Resetting device...");
|
||||
|
||||
// Reset device
|
||||
if (!this->write_byte_16(DAC7678_REG_SOFTWARE_RESET, 0x0000)) {
|
||||
ESP_LOGE(TAG, "Reset failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
} else
|
||||
ESP_LOGV(TAG, "Reset succeeded");
|
||||
|
||||
delayMicroseconds(1000);
|
||||
|
||||
// Set internal reference
|
||||
if (this->internal_reference_) {
|
||||
if (!this->write_byte_16(DAC7678_REG_INTERNAL_REF_0, 1 << 4)) {
|
||||
ESP_LOGE(TAG, "Set internal reference failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
} else
|
||||
ESP_LOGV(TAG, "Internal reference enabled");
|
||||
}
|
||||
}
|
||||
|
||||
void DAC7678Output::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Setting up DAC7678 failed!");
|
||||
} else
|
||||
ESP_LOGCONFIG(TAG, "DAC7678 initialised");
|
||||
}
|
||||
|
||||
void DAC7678Output::register_channel(DAC7678Channel *channel) {
|
||||
auto c = channel->channel_;
|
||||
this->min_channel_ = std::min(this->min_channel_, c);
|
||||
this->max_channel_ = std::max(this->max_channel_, c);
|
||||
channel->set_parent(this);
|
||||
ESP_LOGV(TAG, "Registered channel: %01u", channel->channel_);
|
||||
}
|
||||
|
||||
void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) {
|
||||
if (this->dac_input_reg_[channel] != value) {
|
||||
ESP_LOGV(TAG, "Channel %01u: input_reg=%04u ", channel, value);
|
||||
|
||||
if (!this->write_byte_16(DAC7678_REG_WRITE_N_UPDATE_N | channel, value << 4)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->dac_input_reg_[channel] = value;
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
void DAC7678Channel::write_state(float state) {
|
||||
const float input_rounded = roundf(state * this->full_scale_);
|
||||
auto input = static_cast<uint16_t>(input_rounded);
|
||||
this->parent_->set_channel_value_(this->channel_, input);
|
||||
}
|
||||
|
||||
} // namespace dac7678
|
||||
} // namespace esphome
|
55
esphome/components/dac7678/dac7678_output.h
Normal file
55
esphome/components/dac7678/dac7678_output.h
Normal file
@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace dac7678 {
|
||||
|
||||
class DAC7678Output;
|
||||
|
||||
class DAC7678Channel : public output::FloatOutput, public Parented<DAC7678Output> {
|
||||
public:
|
||||
void set_channel(uint8_t channel) { channel_ = channel; }
|
||||
|
||||
protected:
|
||||
friend class DAC7678Output;
|
||||
|
||||
const uint16_t full_scale_ = 0xFFF;
|
||||
|
||||
void write_state(float state) override;
|
||||
|
||||
uint8_t channel_;
|
||||
};
|
||||
|
||||
/// DAC7678 float output component.
|
||||
class DAC7678Output : public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
DAC7678Output() {}
|
||||
|
||||
void register_channel(DAC7678Channel *channel);
|
||||
|
||||
void set_internal_reference(const bool value) { this->internal_reference_ = value; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
protected:
|
||||
friend DAC7678Channel;
|
||||
|
||||
bool internal_reference_;
|
||||
|
||||
void set_channel_value_(uint8_t channel, uint16_t value);
|
||||
|
||||
uint8_t min_channel_{0xFF};
|
||||
uint8_t max_channel_{0x00};
|
||||
uint16_t dac_input_reg_[8] = {
|
||||
0,
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace dac7678
|
||||
} // namespace esphome
|
27
esphome/components/dac7678/output.py
Normal file
27
esphome/components/dac7678/output.py
Normal file
@ -0,0 +1,27 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import output
|
||||
from esphome.const import CONF_CHANNEL, CONF_ID
|
||||
from . import DAC7678Output, dac7678_ns
|
||||
|
||||
DEPENDENCIES = ["dac7678"]
|
||||
|
||||
DAC7678Channel = dac7678_ns.class_("DAC7678Channel", output.FloatOutput)
|
||||
CONF_DAC7678_ID = "dac7678_id"
|
||||
|
||||
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(DAC7678Channel),
|
||||
cv.GenerateID(CONF_DAC7678_ID): cv.use_id(DAC7678Output),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
paren = await cg.get_variable(config[CONF_DAC7678_ID])
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_channel(config[CONF_CHANNEL]))
|
||||
cg.add(paren.register_channel(var))
|
||||
await output.register_output(var, config)
|
||||
return var
|
@ -134,7 +134,6 @@ void DallasComponent::update() {
|
||||
return;
|
||||
}
|
||||
if (!sensor->check_scratch_pad()) {
|
||||
ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", sensor->get_name().c_str());
|
||||
sensor->publish_state(NAN);
|
||||
this->status_set_warning();
|
||||
return;
|
||||
@ -241,13 +240,29 @@ bool DallasTemperatureSensor::setup_sensor() {
|
||||
return true;
|
||||
}
|
||||
bool DallasTemperatureSensor::check_scratch_pad() {
|
||||
bool chksum_validity = (crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]);
|
||||
bool config_validity = false;
|
||||
|
||||
switch (this->get_address8()[0]) {
|
||||
case DALLAS_MODEL_DS18B20:
|
||||
config_validity = ((this->scratch_pad_[4] & 0x9F) == 0x1F);
|
||||
break;
|
||||
default:
|
||||
config_validity = ((this->scratch_pad_[4] & 0x10) == 0x10);
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
ESP_LOGVV(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0],
|
||||
this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4],
|
||||
this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8],
|
||||
crc8(this->scratch_pad_, 8));
|
||||
#endif
|
||||
return crc8(this->scratch_pad_, 8) == this->scratch_pad_[8];
|
||||
if (!chksum_validity) {
|
||||
ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str());
|
||||
} else if (!config_validity) {
|
||||
ESP_LOGW(TAG, "'%s' - Scratch pad config register invalid!", this->get_name().c_str());
|
||||
}
|
||||
return chksum_validity && config_validity;
|
||||
}
|
||||
float DallasTemperatureSensor::get_temp_c() {
|
||||
int16_t temp = (int16_t(this->scratch_pad_[1]) << 11) | (int16_t(this->scratch_pad_[0]) << 3);
|
||||
|
@ -348,7 +348,7 @@ async def dfplayer_random_to_code(config, action_id, template_arg, args):
|
||||
}
|
||||
),
|
||||
)
|
||||
async def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args):
|
||||
async def dfplayer_is_playing_to_code(config, condition_id, template_arg, args):
|
||||
var = cg.new_Pvariable(condition_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
@ -85,6 +85,12 @@ enum ImageType {
|
||||
IMAGE_TYPE_RGB565 = 4,
|
||||
};
|
||||
|
||||
enum DisplayType {
|
||||
DISPLAY_TYPE_BINARY = 1,
|
||||
DISPLAY_TYPE_GRAYSCALE = 2,
|
||||
DISPLAY_TYPE_COLOR = 3,
|
||||
};
|
||||
|
||||
enum DisplayRotation {
|
||||
DISPLAY_ROTATION_0_DEGREES = 0,
|
||||
DISPLAY_ROTATION_90_DEGREES = 90,
|
||||
@ -361,6 +367,11 @@ class DisplayBuffer {
|
||||
virtual int get_width_internal() = 0;
|
||||
DisplayRotation get_rotation() const { return this->rotation_; }
|
||||
|
||||
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
|
||||
* returns the type the display is currently configured to.
|
||||
*/
|
||||
virtual DisplayType get_display_type() = 0;
|
||||
|
||||
protected:
|
||||
void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg);
|
||||
|
||||
|
@ -132,7 +132,7 @@ class ColorUtil {
|
||||
int16_t plt_r = (int16_t) palette[i * 3 + 0];
|
||||
int16_t plt_g = (int16_t) palette[i * 3 + 1];
|
||||
int16_t plt_b = (int16_t) palette[i * 3 + 2];
|
||||
// Calculate euclidian distance (linear distance in rgb cube).
|
||||
// Calculate euclidean distance (linear distance in rgb cube).
|
||||
x = (uint32_t) std::abs(tgt_r - plt_r);
|
||||
y = (uint32_t) std::abs(tgt_g - plt_g);
|
||||
z = (uint32_t) std::abs(tgt_b - plt_b);
|
||||
|
@ -79,7 +79,7 @@ async def to_code(config):
|
||||
cg.add(var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds))
|
||||
cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds))
|
||||
|
||||
cg.add_define("DSMR_GAS_MBUS_ID", config[CONF_GAS_MBUS_ID])
|
||||
cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID]))
|
||||
|
||||
# DSMR Parser
|
||||
cg.add_library("glmnet/Dsmr", "0.5")
|
||||
|
@ -171,7 +171,7 @@ void Dsmr::receive_telegram_() {
|
||||
this->telegram_[this->bytes_read_] = c;
|
||||
this->bytes_read_++;
|
||||
|
||||
// Check for a footer, i.e. exlamation mark, followed by a hex checksum.
|
||||
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
|
||||
if (c == '!') {
|
||||
ESP_LOGV(TAG, "Footer of telegram found");
|
||||
this->footer_found_ = true;
|
||||
|
@ -199,7 +199,7 @@ void ENS210Component::update() {
|
||||
});
|
||||
}
|
||||
|
||||
// Extracts measurement 'data' and 'status' from a 'val' obtained from measurment.
|
||||
// Extracts measurement 'data' and 'status' from a 'val' obtained from measurement.
|
||||
void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status) {
|
||||
*data = (val >> 0) & 0xffff;
|
||||
int valid = (val >> 16) & 0x1;
|
||||
|
@ -521,6 +521,33 @@ ESP32_BOARD_PINS = {
|
||||
},
|
||||
"lolin32": {"LED": 5},
|
||||
"lolin32_lite": {"LED": 22},
|
||||
"lolin_c3_mini": {
|
||||
"TX": 21,
|
||||
"RX": 20,
|
||||
"SDA": 8,
|
||||
"SCL": 10,
|
||||
"SS": 5,
|
||||
"MOSI": 4,
|
||||
"MISO": 3,
|
||||
"SCK": 2,
|
||||
"A0": 0,
|
||||
"A1": 1,
|
||||
"A2": 2,
|
||||
"A3": 3,
|
||||
"A4": 4,
|
||||
"A5": 5,
|
||||
"D0": 1,
|
||||
"D1": 10,
|
||||
"D2": 8,
|
||||
"D3": 7,
|
||||
"D4": 6,
|
||||
"D5": 2,
|
||||
"D6": 3,
|
||||
"D7": 4,
|
||||
"D8": 5,
|
||||
"LED": 7,
|
||||
"BUTTON": 9,
|
||||
},
|
||||
"lolin_d32": {"LED": 5, "_VBAT": 35},
|
||||
"lolin_d32_pro": {"LED": 5, "_VBAT": 35},
|
||||
"lopy": {
|
||||
@ -1026,6 +1053,7 @@ BOARD_TO_VARIANT = {
|
||||
"labplus_mpython": VARIANT_ESP32,
|
||||
"lolin32_lite": VARIANT_ESP32,
|
||||
"lolin32": VARIANT_ESP32,
|
||||
"lolin_c3_mini": VARIANT_ESP32C3,
|
||||
"lolin_d32_pro": VARIANT_ESP32,
|
||||
"lolin_d32": VARIANT_ESP32,
|
||||
"lopy4": VARIANT_ESP32,
|
||||
|
@ -36,6 +36,7 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
save.key = key;
|
||||
save.data.assign(data, data + len);
|
||||
s_pending_save.emplace_back(save);
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %d", key.c_str(), len);
|
||||
return true;
|
||||
}
|
||||
bool load(uint8_t *data, size_t len) override {
|
||||
@ -65,6 +66,8 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key.c_str(), esp_err_to_name(err));
|
||||
return false;
|
||||
} else {
|
||||
ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %d", key.c_str(), len);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -73,7 +76,6 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
class ESP32Preferences : public ESPPreferences {
|
||||
public:
|
||||
uint32_t nvs_handle;
|
||||
uint32_t current_offset = 0;
|
||||
|
||||
void open() {
|
||||
nvs_flash_init();
|
||||
@ -97,12 +99,9 @@ class ESP32Preferences : public ESPPreferences {
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
|
||||
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
pref->nvs_handle = nvs_handle;
|
||||
current_offset += length;
|
||||
|
||||
uint32_t keyval = current_offset ^ type;
|
||||
char keybuf[16];
|
||||
snprintf(keybuf, sizeof(keybuf), "%d", keyval);
|
||||
pref->key = keybuf; // copied to std::string
|
||||
uint32_t keyval = type;
|
||||
pref->key = str_sprintf("%u", keyval);
|
||||
|
||||
return ESPPreferenceObject(pref);
|
||||
}
|
||||
@ -121,6 +120,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str());
|
||||
if (is_changed(nvs_handle, save)) {
|
||||
esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %d", save.key.c_str(), save.data.size());
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(),
|
||||
esp_err_to_name(err));
|
||||
|
@ -317,12 +317,11 @@ void ESP32Camera::update_camera_parameters() {
|
||||
s->set_gainceiling(s, (gainceiling_t) this->agc_gain_ceiling_);
|
||||
/* update white balance mode */
|
||||
s->set_wb_mode(s, (int) this->wb_mode_); // 0 to 4
|
||||
/* update test patern */
|
||||
/* update test pattern */
|
||||
s->set_colorbar(s, this->test_pattern_);
|
||||
}
|
||||
|
||||
/* ---------------- Internal methods ---------------- */
|
||||
uint32_t ESP32Camera::hash_base() { return 3010542557UL; }
|
||||
bool ESP32Camera::has_requested_image_() const { return this->single_requesters_ || this->stream_requesters_; }
|
||||
bool ESP32Camera::can_return_image_() const { return this->current_image_.use_count() == 1; }
|
||||
void ESP32Camera::framebuffer_task(void *pv) {
|
||||
|
@ -151,7 +151,6 @@ class ESP32Camera : public Component, public EntityBase {
|
||||
|
||||
protected:
|
||||
/* internal methods */
|
||||
uint32_t hash_base() override;
|
||||
bool has_requested_image_() const;
|
||||
bool can_return_image_() const;
|
||||
|
||||
|
@ -152,7 +152,6 @@ void Fan::dump_traits_(const char *tag, const char *prefix) {
|
||||
if (this->get_traits().supports_direction())
|
||||
ESP_LOGCONFIG(tag, "%s Direction: YES", prefix);
|
||||
}
|
||||
uint32_t Fan::hash_base() { return 418001110UL; }
|
||||
|
||||
} // namespace fan
|
||||
} // namespace esphome
|
||||
|
@ -136,7 +136,6 @@ class Fan : public EntityBase {
|
||||
void save_state_();
|
||||
|
||||
void dump_traits_(const char *tag, const char *prefix);
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void()> state_callback_{};
|
||||
ESPPreferenceObject rtc_;
|
||||
|
@ -44,7 +44,14 @@ template<typename T> class RestoringGlobalsComponent : public Component {
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
void loop() override {
|
||||
void loop() override { store_value_(); }
|
||||
|
||||
void on_shutdown() override { store_value_(); }
|
||||
|
||||
void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; }
|
||||
|
||||
protected:
|
||||
void store_value_() {
|
||||
int diff = memcmp(&this->value_, &this->prev_value_, sizeof(T));
|
||||
if (diff != 0) {
|
||||
this->rtc_.save(&this->value_);
|
||||
@ -52,9 +59,6 @@ template<typename T> class RestoringGlobalsComponent : public Component {
|
||||
}
|
||||
}
|
||||
|
||||
void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; }
|
||||
|
||||
protected:
|
||||
T value_{};
|
||||
T prev_value_{};
|
||||
uint32_t name_hash_{};
|
||||
|
@ -118,7 +118,7 @@ def _relocate_fields_to_subfolder(config, subfolder, subschema):
|
||||
fields = [k.schema for k in subschema.schema.keys()]
|
||||
fields.remove(CONF_ID)
|
||||
if subfolder in config:
|
||||
# Ensure no ambigious fields in base of config
|
||||
# Ensure no ambiguous fields in base of config
|
||||
for f in fields:
|
||||
if f in config:
|
||||
raise cv.Invalid(
|
||||
|
@ -195,7 +195,7 @@ void HydreonRGxxComponent::process_line_() {
|
||||
if (n == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
int data = strtol(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr, 10);
|
||||
float data = strtof(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr);
|
||||
this->sensors_[i]->publish_state(data);
|
||||
ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state());
|
||||
this->sensors_received_ |= (1 << i);
|
||||
|
@ -37,7 +37,7 @@ SUPPORTED_SENSORS = {
|
||||
PROTOCOL_NAMES = {
|
||||
CONF_MOISTURE: "R",
|
||||
CONF_ACC: "Acc",
|
||||
CONF_R_INT: "Rint",
|
||||
CONF_R_INT: "RInt",
|
||||
CONF_EVENT_ACC: "EventAcc",
|
||||
CONF_TOTAL_ACC: "TotalAcc",
|
||||
}
|
||||
|
@ -224,7 +224,7 @@ void ArduinoI2CBus::recover_() {
|
||||
digitalWrite(sda_pin_, LOW); // NOLINT
|
||||
|
||||
// By now, any stuck device ought to have sent all remaining bits of its
|
||||
// transation, meaning that it should have freed up the SDA line, resulting
|
||||
// transaction, meaning that it should have freed up the SDA line, resulting
|
||||
// in SDA being pulled up.
|
||||
if (digitalRead(sda_pin_) == LOW) { // NOLINT
|
||||
ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle");
|
||||
|
@ -285,7 +285,7 @@ void IDFI2CBus::recover_() {
|
||||
}
|
||||
|
||||
// By now, any stuck device ought to have sent all remaining bits of its
|
||||
// transation, meaning that it should have freed up the SDA line, resulting
|
||||
// transaction, meaning that it should have freed up the SDA line, resulting
|
||||
// in SDA being pulled up.
|
||||
if (gpio_get_level(sda_pin) == 0) {
|
||||
ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle");
|
||||
|
0
esphome/components/i2s_audio/__init__.py
Normal file
0
esphome/components/i2s_audio/__init__.py
Normal file
160
esphome/components/i2s_audio/i2s_audio_media_player.cpp
Normal file
160
esphome/components/i2s_audio/i2s_audio_media_player.cpp
Normal file
@ -0,0 +1,160 @@
|
||||
#include "i2s_audio_media_player.h"
|
||||
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace i2s_audio {
|
||||
|
||||
static const char *const TAG = "audio";
|
||||
|
||||
void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) {
|
||||
if (call.get_media_url().has_value()) {
|
||||
if (this->audio_->isRunning())
|
||||
this->audio_->stopSong();
|
||||
this->high_freq_.start();
|
||||
this->audio_->connecttohost(call.get_media_url().value().c_str());
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
|
||||
}
|
||||
if (call.get_volume().has_value()) {
|
||||
this->volume = call.get_volume().value();
|
||||
this->set_volume_(volume);
|
||||
this->unmute_();
|
||||
}
|
||||
if (call.get_command().has_value()) {
|
||||
switch (call.get_command().value()) {
|
||||
case media_player::MEDIA_PLAYER_COMMAND_PLAY:
|
||||
if (!this->audio_->isRunning())
|
||||
this->audio_->pauseResume();
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_PAUSE:
|
||||
if (this->audio_->isRunning())
|
||||
this->audio_->pauseResume();
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_STOP:
|
||||
this->stop_();
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_MUTE:
|
||||
this->mute_();
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_UNMUTE:
|
||||
this->unmute_();
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_TOGGLE:
|
||||
this->audio_->pauseResume();
|
||||
if (this->audio_->isRunning()) {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
|
||||
} else {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
|
||||
}
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: {
|
||||
float new_volume = this->volume + 0.1f;
|
||||
if (new_volume > 1.0f)
|
||||
new_volume = 1.0f;
|
||||
this->set_volume_(new_volume);
|
||||
this->unmute_();
|
||||
break;
|
||||
}
|
||||
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: {
|
||||
float new_volume = this->volume - 0.1f;
|
||||
if (new_volume < 0.0f)
|
||||
new_volume = 0.0f;
|
||||
this->set_volume_(new_volume);
|
||||
this->unmute_();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
void I2SAudioMediaPlayer::mute_() {
|
||||
if (this->mute_pin_ != nullptr) {
|
||||
this->mute_pin_->digital_write(true);
|
||||
} else {
|
||||
this->set_volume_(0.0f, false);
|
||||
}
|
||||
this->muted_ = true;
|
||||
}
|
||||
void I2SAudioMediaPlayer::unmute_() {
|
||||
if (this->mute_pin_ != nullptr) {
|
||||
this->mute_pin_->digital_write(false);
|
||||
} else {
|
||||
this->set_volume_(this->volume, false);
|
||||
}
|
||||
this->muted_ = false;
|
||||
}
|
||||
void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) {
|
||||
this->audio_->setVolume(remap<uint8_t, float>(volume, 0.0f, 1.0f, 0, 21));
|
||||
if (publish)
|
||||
this->volume = volume;
|
||||
}
|
||||
|
||||
void I2SAudioMediaPlayer::stop_() {
|
||||
if (this->audio_->isRunning())
|
||||
this->audio_->stopSong();
|
||||
this->high_freq_.stop();
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
|
||||
}
|
||||
|
||||
void I2SAudioMediaPlayer::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up Audio...");
|
||||
if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
|
||||
this->audio_ = make_unique<Audio>(true, this->internal_dac_mode_);
|
||||
} else {
|
||||
this->audio_ = make_unique<Audio>(false);
|
||||
this->audio_->setPinout(this->bclk_pin_, this->lrclk_pin_, this->dout_pin_);
|
||||
this->audio_->forceMono(this->external_dac_channels_ == 1);
|
||||
if (this->mute_pin_ != nullptr) {
|
||||
this->mute_pin_->setup();
|
||||
this->mute_pin_->digital_write(false);
|
||||
}
|
||||
}
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
|
||||
}
|
||||
|
||||
void I2SAudioMediaPlayer::loop() {
|
||||
this->audio_->loop();
|
||||
if (this->state == media_player::MEDIA_PLAYER_STATE_PLAYING && !this->audio_->isRunning()) {
|
||||
this->stop_();
|
||||
this->publish_state();
|
||||
}
|
||||
}
|
||||
|
||||
media_player::MediaPlayerTraits I2SAudioMediaPlayer::get_traits() {
|
||||
auto traits = media_player::MediaPlayerTraits();
|
||||
traits.set_supports_pause(true);
|
||||
return traits;
|
||||
};
|
||||
|
||||
void I2SAudioMediaPlayer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Audio:");
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGCONFIG(TAG, "Audio failed to initialize!");
|
||||
return;
|
||||
}
|
||||
if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
|
||||
switch (this->internal_dac_mode_) {
|
||||
case I2S_DAC_CHANNEL_LEFT_EN:
|
||||
ESP_LOGCONFIG(TAG, " Internal DAC mode: Left");
|
||||
break;
|
||||
case I2S_DAC_CHANNEL_RIGHT_EN:
|
||||
ESP_LOGCONFIG(TAG, " Internal DAC mode: Right");
|
||||
break;
|
||||
case I2S_DAC_CHANNEL_BOTH_EN:
|
||||
ESP_LOGCONFIG(TAG, " Internal DAC mode: Left & Right");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace i2s_audio
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
63
esphome/components/i2s_audio/i2s_audio_media_player.h
Normal file
63
esphome/components/i2s_audio/i2s_audio_media_player.h
Normal file
@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
|
||||
#include "esphome/components/media_player/media_player.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <Audio.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace i2s_audio {
|
||||
|
||||
class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer {
|
||||
public:
|
||||
void setup() override;
|
||||
float get_setup_priority() const override { return esphome::setup_priority::LATE; }
|
||||
|
||||
void loop() override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
|
||||
void set_bclk_pin(uint8_t pin) { this->bclk_pin_ = pin; }
|
||||
void set_lrclk_pin(uint8_t pin) { this->lrclk_pin_ = pin; }
|
||||
void set_mute_pin(GPIOPin *mute_pin) { this->mute_pin_ = mute_pin; }
|
||||
void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
|
||||
void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; }
|
||||
|
||||
media_player::MediaPlayerTraits get_traits() override;
|
||||
|
||||
bool is_muted() const override { return this->muted_; }
|
||||
|
||||
protected:
|
||||
void control(const media_player::MediaPlayerCall &call) override;
|
||||
|
||||
void mute_();
|
||||
void unmute_();
|
||||
void set_volume_(float volume, bool publish = true);
|
||||
void stop_();
|
||||
|
||||
std::unique_ptr<Audio> audio_;
|
||||
|
||||
uint8_t dout_pin_{0};
|
||||
uint8_t din_pin_{0};
|
||||
uint8_t bclk_pin_;
|
||||
uint8_t lrclk_pin_;
|
||||
|
||||
GPIOPin *mute_pin_{nullptr};
|
||||
bool muted_{false};
|
||||
float unmuted_volume_{0};
|
||||
|
||||
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
|
||||
uint8_t external_dac_channels_;
|
||||
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
};
|
||||
|
||||
} // namespace i2s_audio
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
94
esphome/components/i2s_audio/media_player.py
Normal file
94
esphome/components/i2s_audio/media_player.py
Normal file
@ -0,0 +1,94 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import media_player
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from esphome import pins
|
||||
|
||||
from esphome.const import CONF_ID, CONF_MODE
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
|
||||
i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio")
|
||||
|
||||
I2SAudioMediaPlayer = i2s_audio_ns.class_(
|
||||
"I2SAudioMediaPlayer", cg.Component, media_player.MediaPlayer
|
||||
)
|
||||
|
||||
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
|
||||
|
||||
CONF_I2S_DOUT_PIN = "i2s_dout_pin"
|
||||
CONF_I2S_BCLK_PIN = "i2s_bclk_pin"
|
||||
CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin"
|
||||
CONF_MUTE_PIN = "mute_pin"
|
||||
CONF_AUDIO_ID = "audio_id"
|
||||
CONF_DAC_TYPE = "dac_type"
|
||||
|
||||
INTERNAL_DAC_OPTIONS = {
|
||||
"left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
|
||||
"right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
|
||||
"stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
|
||||
}
|
||||
|
||||
EXTERNAL_DAC_OPTIONS = ["mono", "stereo"]
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.typed_schema(
|
||||
{
|
||||
"internal": cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer),
|
||||
cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True),
|
||||
}
|
||||
)
|
||||
.extend(media_player.MEDIA_PLAYER_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
"external": cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer),
|
||||
cv.Required(
|
||||
CONF_I2S_DOUT_PIN
|
||||
): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(
|
||||
CONF_I2S_BCLK_PIN
|
||||
): pins.internal_gpio_output_pin_number,
|
||||
cv.Required(
|
||||
CONF_I2S_LRCLK_PIN
|
||||
): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_MUTE_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_MODE, default="mono"): cv.one_of(
|
||||
*EXTERNAL_DAC_OPTIONS, lower=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(media_player.MEDIA_PLAYER_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
},
|
||||
key=CONF_DAC_TYPE,
|
||||
),
|
||||
cv.only_with_arduino,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await media_player.register_media_player(var, config)
|
||||
|
||||
if config[CONF_DAC_TYPE] == "internal":
|
||||
cg.add(var.set_internal_dac_mode(config[CONF_MODE]))
|
||||
else:
|
||||
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
|
||||
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))
|
||||
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
|
||||
if CONF_MUTE_PIN in config:
|
||||
pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN])
|
||||
cg.add(var.set_mute_pin(pin))
|
||||
cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))
|
||||
|
||||
if CORE.is_esp32:
|
||||
cg.add_library("WiFiClientSecure", None)
|
||||
cg.add_library("HTTPClient", None)
|
||||
cg.add_library("esphome/ESP32-audioI2S", "2.1.0")
|
||||
cg.add_build_flag("-DAUDIO_NO_SD_FS")
|
@ -24,6 +24,7 @@ ili9341 = ili9341_ns.class_(
|
||||
)
|
||||
ILI9341M5Stack = ili9341_ns.class_("ILI9341M5Stack", ili9341)
|
||||
ILI9341TFT24 = ili9341_ns.class_("ILI9341TFT24", ili9341)
|
||||
ILI9341TFT24R = ili9341_ns.class_("ILI9341TFT24R", ili9341)
|
||||
|
||||
ILI9341Model = ili9341_ns.enum("ILI9341Model")
|
||||
ILI9341ColorMode = ili9341_ns.enum("ILI9341ColorMode")
|
||||
@ -31,6 +32,7 @@ ILI9341ColorMode = ili9341_ns.enum("ILI9341ColorMode")
|
||||
MODELS = {
|
||||
"M5STACK": ILI9341Model.M5STACK,
|
||||
"TFT_2.4": ILI9341Model.TFT_24,
|
||||
"TFT_2.4R": ILI9341Model.TFT_24R,
|
||||
}
|
||||
|
||||
ILI9341_MODEL = cv.enum(MODELS, upper=True, space="_")
|
||||
@ -60,6 +62,8 @@ async def to_code(config):
|
||||
lcd_type = ILI9341M5Stack
|
||||
if config[CONF_MODEL] == "TFT_2.4":
|
||||
lcd_type = ILI9341TFT24
|
||||
if config[CONF_MODEL] == "TFT_2.4R":
|
||||
lcd_type = ILI9341TFT24R
|
||||
rhs = lcd_type.new()
|
||||
var = cg.Pvariable(config[CONF_ID], rhs)
|
||||
|
||||
|
@ -263,5 +263,13 @@ void ILI9341TFT24::initialize() {
|
||||
this->fill_internal_(Color::BLACK);
|
||||
}
|
||||
|
||||
// 24_TFT rotated display
|
||||
void ILI9341TFT24R::initialize() {
|
||||
this->init_lcd_(INITCMD_TFT);
|
||||
this->width_ = 320;
|
||||
this->height_ = 240;
|
||||
this->fill_internal_(Color::BLACK);
|
||||
}
|
||||
|
||||
} // namespace ili9341
|
||||
} // namespace esphome
|
||||
|
@ -12,6 +12,7 @@ namespace ili9341 {
|
||||
enum ILI9341Model {
|
||||
M5STACK = 0,
|
||||
TFT_24,
|
||||
TFT_24R,
|
||||
};
|
||||
|
||||
enum ILI9341ColorMode {
|
||||
@ -48,6 +49,8 @@ class ILI9341Display : public PollingComponent,
|
||||
this->initialize();
|
||||
}
|
||||
|
||||
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;
|
||||
void setup_pins_();
|
||||
@ -100,5 +103,12 @@ class ILI9341TFT24 : public ILI9341Display {
|
||||
public:
|
||||
void initialize() override;
|
||||
};
|
||||
|
||||
//----------- ILI9341_24_TFT rotated display --------------
|
||||
class ILI9341TFT24R : public ILI9341Display {
|
||||
public:
|
||||
void initialize() override;
|
||||
};
|
||||
|
||||
} // namespace ili9341
|
||||
} // namespace esphome
|
||||
|
@ -86,6 +86,10 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public
|
||||
|
||||
void block_partial() { this->block_partial_ = true; }
|
||||
|
||||
display::DisplayType get_display_type() override {
|
||||
return get_greyscale() ? display::DisplayType::DISPLAY_TYPE_GRAYSCALE : display::DisplayType::DISPLAY_TYPE_BINARY;
|
||||
}
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
void display1b_();
|
||||
|
@ -10,14 +10,13 @@ static const char *const TAG = "integration";
|
||||
|
||||
void IntegrationSensor::setup() {
|
||||
if (this->restore_) {
|
||||
this->rtc_ = global_preferences->make_preference<float>(this->get_object_id_hash());
|
||||
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
|
||||
float preference_value = 0;
|
||||
this->rtc_.load(&preference_value);
|
||||
this->pref_.load(&preference_value);
|
||||
this->result_ = preference_value;
|
||||
}
|
||||
|
||||
this->last_update_ = millis();
|
||||
this->last_save_ = this->last_update_;
|
||||
|
||||
this->publish_and_save_(this->result_);
|
||||
this->sensor_->add_on_state_callback([this](float state) { this->process_sensor_value_(state); });
|
||||
|
@ -28,7 +28,6 @@ class IntegrationSensor : public sensor::Sensor, public Component {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; }
|
||||
void set_sensor(Sensor *sensor) { sensor_ = sensor; }
|
||||
void set_time(IntegrationSensorTime time) { time_ = time; }
|
||||
void set_method(IntegrationMethod method) { method_ = method; }
|
||||
@ -56,22 +55,18 @@ class IntegrationSensor : public sensor::Sensor, public Component {
|
||||
void publish_and_save_(double result) {
|
||||
this->result_ = result;
|
||||
this->publish_state(result);
|
||||
float result_f = result;
|
||||
const uint32_t now = millis();
|
||||
if (now - this->last_save_ < this->min_save_interval_)
|
||||
return;
|
||||
this->last_save_ = now;
|
||||
this->rtc_.save(&result_f);
|
||||
if (this->restore_) {
|
||||
float result_f = result;
|
||||
this->pref_.save(&result_f);
|
||||
}
|
||||
}
|
||||
|
||||
sensor::Sensor *sensor_;
|
||||
IntegrationSensorTime time_;
|
||||
IntegrationMethod method_;
|
||||
bool restore_;
|
||||
ESPPreferenceObject rtc_;
|
||||
ESPPreferenceObject pref_;
|
||||
|
||||
uint32_t last_save_{0};
|
||||
uint32_t min_save_interval_{0};
|
||||
uint32_t last_update_;
|
||||
double result_{0.0f};
|
||||
float last_value_{0.0f};
|
||||
|
@ -35,7 +35,6 @@ INTEGRATION_METHODS = {
|
||||
|
||||
CONF_TIME_UNIT = "time_unit"
|
||||
CONF_INTEGRATION_METHOD = "integration_method"
|
||||
CONF_MIN_SAVE_INTERVAL = "min_save_interval"
|
||||
|
||||
|
||||
def inherit_unit_of_measurement(uom, config):
|
||||
@ -58,9 +57,9 @@ CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend(
|
||||
INTEGRATION_METHODS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_RESTORE, default=False): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_MIN_SAVE_INTERVAL, default="0s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional("min_save_interval"): cv.invalid(
|
||||
"min_save_interval was removed in 2022.8.0. Please use the `preferences` -> `flash_write_interval` to adjust."
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
@ -97,7 +96,6 @@ async def to_code(config):
|
||||
cg.add(var.set_time(config[CONF_TIME_UNIT]))
|
||||
cg.add(var.set_method(config[CONF_INTEGRATION_METHOD]))
|
||||
cg.add(var.set_restore(config[CONF_RESTORE]))
|
||||
cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL]))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
|
@ -1,4 +1,4 @@
|
||||
from esphome.jsonschema import jschema_extractor
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
@ -479,11 +479,11 @@ async def addressable_flicker_effect_to_code(config, effect_id):
|
||||
|
||||
|
||||
def validate_effects(allowed_effects):
|
||||
@jschema_extractor("effects")
|
||||
@schema_extractor("effects")
|
||||
def validator(value):
|
||||
# pylint: disable=comparison-with-callable
|
||||
if value == jschema_extractor:
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return (allowed_effects, EFFECTS_REGISTRY)
|
||||
|
||||
value = cv.validate_registry("effect", EFFECTS_REGISTRY)(value)
|
||||
errors = []
|
||||
names = set()
|
||||
|
@ -203,7 +203,7 @@ class LightColorValues {
|
||||
*color_temperature =
|
||||
(this->color_temperature_ - color_temperature_cw) / (color_temperature_ww - color_temperature_cw);
|
||||
*white_brightness = gamma_correct(this->state_ * this->brightness_ * white_level, gamma);
|
||||
} else { // Probably wont get here but put this here anyway.
|
||||
} else { // Probably won't get here but put this here anyway.
|
||||
*white_brightness = 0;
|
||||
}
|
||||
}
|
||||
|
@ -121,7 +121,7 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO
|
||||
call.set_cold_white(float(color["c"]) / 255.0f);
|
||||
}
|
||||
if (color.containsKey("w")) {
|
||||
// the HA scheme is ambigious here, the same key is used for white channel in RGBW and warm
|
||||
// the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm
|
||||
// white channel in RGBWW.
|
||||
if (color.containsKey("c")) {
|
||||
call.set_warm_white(float(color["w"]) / 255.0f);
|
||||
|
@ -145,7 +145,6 @@ void LightState::loop() {
|
||||
}
|
||||
|
||||
float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
|
||||
uint32_t LightState::hash_base() { return 1114400283; }
|
||||
|
||||
void LightState::publish_state() { this->remote_values_callback_.call(); }
|
||||
|
||||
|
@ -150,8 +150,6 @@ class LightState : public EntityBase, public Component {
|
||||
friend LightCall;
|
||||
friend class AddressableLight;
|
||||
|
||||
uint32_t hash_base() override;
|
||||
|
||||
/// Internal method to start an effect with the given index
|
||||
void start_effect_(uint32_t effect_index);
|
||||
/// Internal method to get the currently active effect
|
||||
|
@ -57,7 +57,6 @@ void Lock::publish_state(LockState state) {
|
||||
}
|
||||
|
||||
void Lock::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
|
||||
uint32_t Lock::hash_base() { return 856245656UL; }
|
||||
|
||||
void LockCall::perform() {
|
||||
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
|
||||
|
@ -167,8 +167,6 @@ class Lock : public EntityBase {
|
||||
*/
|
||||
virtual void control(const LockCall &call) = 0;
|
||||
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void()> state_callback_{};
|
||||
Deduplicator<LockState> publish_dedup_;
|
||||
ESPPreferenceObject rtc_;
|
||||
|
@ -178,7 +178,8 @@ void Logger::pre_setup() {
|
||||
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
#endif
|
||||
break;
|
||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
case UART_SELECTION_UART2:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
|
@ -94,6 +94,14 @@ void MAX31865Sensor::read_data_() {
|
||||
const uint16_t rtd_resistance_register = this->read_register_16_(RTD_RESISTANCE_MSB_REG);
|
||||
this->write_config_(0b11000000, 0b00000000);
|
||||
|
||||
// Check for bad connection
|
||||
if (rtd_resistance_register == 0b0000000000000000 || rtd_resistance_register == 0b1111111111111111) {
|
||||
ESP_LOGE(TAG, "SPI bus read all 0 or all 1 (0x%04X), check MAX31865 wiring & power.", rtd_resistance_register);
|
||||
this->publish_state(NAN);
|
||||
this->status_set_error();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check faults
|
||||
const uint8_t faults = this->read_register_(FAULT_STATUS_REG);
|
||||
if ((has_fault_ = faults & 0b00111100)) {
|
||||
|
@ -11,6 +11,9 @@ from esphome.const import (
|
||||
UNIT_CELSIUS,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@DAVe3283"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
max31865_ns = cg.esphome_ns.namespace("max31865")
|
||||
MAX31865Sensor = max31865_ns.class_(
|
||||
"MAX31865Sensor", sensor.Sensor, cg.PollingComponent, spi.SPIDevice
|
||||
|
@ -93,6 +93,8 @@ class MAX7219Component : public PollingComponent,
|
||||
uint8_t strftimedigit(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0)));
|
||||
#endif
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
|
||||
|
||||
protected:
|
||||
void send_byte_(uint8_t a_register, uint8_t data);
|
||||
void send_to_all_(uint8_t a_register, uint8_t data);
|
||||
|
179
esphome/components/media_player/__init__.py
Normal file
179
esphome/components/media_player/__init__.py
Normal file
@ -0,0 +1,179 @@
|
||||
from esphome import automation
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
|
||||
from esphome.automation import maybe_simple_id
|
||||
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
|
||||
from esphome.core import CORE
|
||||
from esphome.coroutine import coroutine_with_priority
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
media_player_ns = cg.esphome_ns.namespace("media_player")
|
||||
|
||||
MediaPlayer = media_player_ns.class_("MediaPlayer")
|
||||
|
||||
PlayAction = media_player_ns.class_(
|
||||
"PlayAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
PlayMediaAction = media_player_ns.class_(
|
||||
"PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
ToggleAction = media_player_ns.class_(
|
||||
"ToggleAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
PauseAction = media_player_ns.class_(
|
||||
"PauseAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
StopAction = media_player_ns.class_(
|
||||
"StopAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
VolumeUpAction = media_player_ns.class_(
|
||||
"VolumeUpAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
VolumeDownAction = media_player_ns.class_(
|
||||
"VolumeDownAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
VolumeSetAction = media_player_ns.class_(
|
||||
"VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
|
||||
|
||||
CONF_VOLUME = "volume"
|
||||
CONF_ON_IDLE = "on_idle"
|
||||
CONF_ON_PLAY = "on_play"
|
||||
CONF_ON_PAUSE = "on_pause"
|
||||
CONF_MEDIA_URL = "media_url"
|
||||
|
||||
StateTrigger = media_player_ns.class_("StateTrigger", automation.Trigger.template())
|
||||
IdleTrigger = media_player_ns.class_("IdleTrigger", automation.Trigger.template())
|
||||
PlayTrigger = media_player_ns.class_("PlayTrigger", automation.Trigger.template())
|
||||
PauseTrigger = media_player_ns.class_("PauseTrigger", automation.Trigger.template())
|
||||
IsIdleCondition = media_player_ns.class_("IsIdleCondition", automation.Condition)
|
||||
IsPlayingCondition = media_player_ns.class_("IsPlayingCondition", automation.Condition)
|
||||
|
||||
|
||||
async def setup_media_player_core_(var, config):
|
||||
await setup_entity(var, config)
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_IDLE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_PLAY, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_PAUSE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
|
||||
async def register_media_player(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_media_player(var))
|
||||
await setup_media_player_core_(var, config)
|
||||
|
||||
|
||||
MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_IDLE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PLAY): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlayTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PAUSE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
MEDIA_PLAYER_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(MediaPlayer)})
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"media_player.play_media",
|
||||
PlayMediaAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(MediaPlayer),
|
||||
cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url),
|
||||
},
|
||||
key=CONF_MEDIA_URL,
|
||||
),
|
||||
)
|
||||
async def media_player_play_media_action(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
media_url = await cg.templatable(config[CONF_MEDIA_URL], args, cg.std_string)
|
||||
cg.add(var.set_media_url(media_url))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action("media_player.play", PlayAction, MEDIA_PLAYER_ACTION_SCHEMA)
|
||||
@automation.register_action(
|
||||
"media_player.toggle", ToggleAction, MEDIA_PLAYER_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action(
|
||||
"media_player.pause", PauseAction, MEDIA_PLAYER_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action("media_player.stop", StopAction, MEDIA_PLAYER_ACTION_SCHEMA)
|
||||
@automation.register_action(
|
||||
"media_player.volume_up", VolumeUpAction, MEDIA_PLAYER_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action(
|
||||
"media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_condition(
|
||||
"media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_condition(
|
||||
"media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_ACTION_SCHEMA
|
||||
)
|
||||
async def media_player_action(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"media_player.volume_set",
|
||||
VolumeSetAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(MediaPlayer),
|
||||
cv.Required(CONF_VOLUME): cv.templatable(cv.percentage),
|
||||
},
|
||||
key=CONF_VOLUME,
|
||||
),
|
||||
)
|
||||
async def media_player_volume_set_action(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
volume = await cg.templatable(config[CONF_VOLUME], args, float)
|
||||
cg.add(var.set_volume(volume))
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
async def to_code(config):
|
||||
cg.add_global(media_player_ns.using)
|
||||
cg.add_define("USE_MEDIA_PLAYER")
|
67
esphome/components/media_player/automation.h
Normal file
67
esphome/components/media_player/automation.h
Normal file
@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "media_player.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace media_player {
|
||||
|
||||
#define MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ACTION_CLASS, ACTION_COMMAND) \
|
||||
template<typename... Ts> class ACTION_CLASS : public Action<Ts...>, public Parented<MediaPlayer> { \
|
||||
void play(Ts... x) override { \
|
||||
this->parent_->make_call().set_command(MediaPlayerCommand::MEDIA_PLAYER_COMMAND_##ACTION_COMMAND).perform(); \
|
||||
} \
|
||||
};
|
||||
|
||||
#define MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(TRIGGER_CLASS, TRIGGER_STATE) \
|
||||
class TRIGGER_CLASS : public Trigger<> { \
|
||||
public: \
|
||||
explicit TRIGGER_CLASS(MediaPlayer *player) { \
|
||||
player->add_on_state_callback([this, player]() { \
|
||||
if (player->state == MediaPlayerState::MEDIA_PLAYER_STATE_##TRIGGER_STATE) \
|
||||
this->trigger(); \
|
||||
}); \
|
||||
} \
|
||||
};
|
||||
|
||||
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PlayAction, PLAY)
|
||||
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PauseAction, PAUSE)
|
||||
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(StopAction, STOP)
|
||||
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ToggleAction, TOGGLE)
|
||||
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeUpAction, VOLUME_UP)
|
||||
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeDownAction, VOLUME_DOWN)
|
||||
|
||||
template<typename... Ts> class PlayMediaAction : public Action<Ts...>, public Parented<MediaPlayer> {
|
||||
TEMPLATABLE_VALUE(std::string, media_url)
|
||||
void play(Ts... x) override { this->parent_->make_call().set_media_url(this->media_url_.value(x...)).perform(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Parented<MediaPlayer> {
|
||||
TEMPLATABLE_VALUE(float, volume)
|
||||
void play(Ts... x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); }
|
||||
};
|
||||
|
||||
class StateTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit StateTrigger(MediaPlayer *player) {
|
||||
player->add_on_state_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(IdleTrigger, IDLE)
|
||||
MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(PlayTrigger, PLAYING)
|
||||
MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(PauseTrigger, PAUSED)
|
||||
|
||||
template<typename... Ts> class IsIdleCondition : public Condition<Ts...>, public Parented<MediaPlayer> {
|
||||
public:
|
||||
bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_IDLE; }
|
||||
};
|
||||
|
||||
template<typename... Ts> class IsPlayingCondition : public Condition<Ts...>, public Parented<MediaPlayer> {
|
||||
public:
|
||||
bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; }
|
||||
};
|
||||
|
||||
} // namespace media_player
|
||||
} // namespace esphome
|
118
esphome/components/media_player/media_player.cpp
Normal file
118
esphome/components/media_player/media_player.cpp
Normal file
@ -0,0 +1,118 @@
|
||||
#include "media_player.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace media_player {
|
||||
|
||||
static const char *const TAG = "media_player";
|
||||
|
||||
const char *media_player_state_to_string(MediaPlayerState state) {
|
||||
switch (state) {
|
||||
case MEDIA_PLAYER_STATE_IDLE:
|
||||
return "IDLE";
|
||||
case MEDIA_PLAYER_STATE_PLAYING:
|
||||
return "PLAYING";
|
||||
case MEDIA_PLAYER_STATE_PAUSED:
|
||||
return "PAUSED";
|
||||
case MEDIA_PLAYER_STATE_NONE:
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
const char *media_player_command_to_string(MediaPlayerCommand command) {
|
||||
switch (command) {
|
||||
case MEDIA_PLAYER_COMMAND_PLAY:
|
||||
return "PLAY";
|
||||
case MEDIA_PLAYER_COMMAND_PAUSE:
|
||||
return "PAUSE";
|
||||
case MEDIA_PLAYER_COMMAND_STOP:
|
||||
return "STOP";
|
||||
case MEDIA_PLAYER_COMMAND_MUTE:
|
||||
return "MUTE";
|
||||
case MEDIA_PLAYER_COMMAND_UNMUTE:
|
||||
return "UNMUTE";
|
||||
case MEDIA_PLAYER_COMMAND_TOGGLE:
|
||||
return "TOGGLE";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
void MediaPlayerCall::validate_() {
|
||||
if (this->media_url_.has_value()) {
|
||||
if (this->command_.has_value()) {
|
||||
ESP_LOGW(TAG, "MediaPlayerCall: Setting both command and media_url is not needed.");
|
||||
this->command_.reset();
|
||||
}
|
||||
}
|
||||
if (this->volume_.has_value()) {
|
||||
if (this->volume_.value() < 0.0f || this->volume_.value() > 1.0f) {
|
||||
ESP_LOGW(TAG, "MediaPlayerCall: Volume must be between 0.0 and 1.0.");
|
||||
this->volume_.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MediaPlayerCall::perform() {
|
||||
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
|
||||
this->validate_();
|
||||
if (this->command_.has_value()) {
|
||||
const char *command_s = media_player_command_to_string(this->command_.value());
|
||||
ESP_LOGD(TAG, " Command: %s", command_s);
|
||||
}
|
||||
if (this->media_url_.has_value()) {
|
||||
ESP_LOGD(TAG, " Media URL: %s", this->media_url_.value().c_str());
|
||||
}
|
||||
if (this->volume_.has_value()) {
|
||||
ESP_LOGD(TAG, " Volume: %.2f", this->volume_.value());
|
||||
}
|
||||
this->parent_->control(*this);
|
||||
}
|
||||
|
||||
MediaPlayerCall &MediaPlayerCall::set_command(MediaPlayerCommand command) {
|
||||
this->command_ = command;
|
||||
return *this;
|
||||
}
|
||||
MediaPlayerCall &MediaPlayerCall::set_command(optional<MediaPlayerCommand> command) {
|
||||
this->command_ = command;
|
||||
return *this;
|
||||
}
|
||||
MediaPlayerCall &MediaPlayerCall::set_command(const std::string &command) {
|
||||
if (str_equals_case_insensitive(command, "PLAY")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_PLAY);
|
||||
} else if (str_equals_case_insensitive(command, "PAUSE")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_PAUSE);
|
||||
} else if (str_equals_case_insensitive(command, "STOP")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_STOP);
|
||||
} else if (str_equals_case_insensitive(command, "MUTE")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_MUTE);
|
||||
} else if (str_equals_case_insensitive(command, "UNMUTE")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_UNMUTE);
|
||||
} else if (str_equals_case_insensitive(command, "TOGGLE")) {
|
||||
this->set_command(MEDIA_PLAYER_COMMAND_TOGGLE);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command.c_str());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
MediaPlayerCall &MediaPlayerCall::set_media_url(const std::string &media_url) {
|
||||
this->media_url_ = media_url;
|
||||
return *this;
|
||||
}
|
||||
|
||||
MediaPlayerCall &MediaPlayerCall::set_volume(float volume) {
|
||||
this->volume_ = volume;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void MediaPlayer::add_on_state_callback(std::function<void()> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void MediaPlayer::publish_state() { this->state_callback_.call(); }
|
||||
|
||||
} // namespace media_player
|
||||
} // namespace esphome
|
93
esphome/components/media_player/media_player.h
Normal file
93
esphome/components/media_player/media_player.h
Normal file
@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace media_player {
|
||||
|
||||
enum MediaPlayerState : uint8_t {
|
||||
MEDIA_PLAYER_STATE_NONE = 0,
|
||||
MEDIA_PLAYER_STATE_IDLE = 1,
|
||||
MEDIA_PLAYER_STATE_PLAYING = 2,
|
||||
MEDIA_PLAYER_STATE_PAUSED = 3
|
||||
};
|
||||
const char *media_player_state_to_string(MediaPlayerState state);
|
||||
|
||||
enum MediaPlayerCommand : uint8_t {
|
||||
MEDIA_PLAYER_COMMAND_PLAY = 0,
|
||||
MEDIA_PLAYER_COMMAND_PAUSE = 1,
|
||||
MEDIA_PLAYER_COMMAND_STOP = 2,
|
||||
MEDIA_PLAYER_COMMAND_MUTE = 3,
|
||||
MEDIA_PLAYER_COMMAND_UNMUTE = 4,
|
||||
MEDIA_PLAYER_COMMAND_TOGGLE = 5,
|
||||
MEDIA_PLAYER_COMMAND_VOLUME_UP = 6,
|
||||
MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7,
|
||||
};
|
||||
const char *media_player_command_to_string(MediaPlayerCommand command);
|
||||
|
||||
class MediaPlayer;
|
||||
|
||||
class MediaPlayerTraits {
|
||||
public:
|
||||
MediaPlayerTraits() = default;
|
||||
|
||||
void set_supports_pause(bool supports_pause) { this->supports_pause_ = supports_pause; }
|
||||
|
||||
bool get_supports_pause() const { return this->supports_pause_; }
|
||||
|
||||
protected:
|
||||
bool supports_pause_{false};
|
||||
};
|
||||
|
||||
class MediaPlayerCall {
|
||||
public:
|
||||
MediaPlayerCall(MediaPlayer *parent) : parent_(parent) {}
|
||||
|
||||
MediaPlayerCall &set_command(MediaPlayerCommand command);
|
||||
MediaPlayerCall &set_command(optional<MediaPlayerCommand> command);
|
||||
MediaPlayerCall &set_command(const std::string &command);
|
||||
|
||||
MediaPlayerCall &set_media_url(const std::string &url);
|
||||
|
||||
MediaPlayerCall &set_volume(float volume);
|
||||
|
||||
void perform();
|
||||
|
||||
const optional<MediaPlayerCommand> &get_command() const { return command_; }
|
||||
const optional<std::string> &get_media_url() const { return media_url_; }
|
||||
const optional<float> &get_volume() const { return volume_; }
|
||||
|
||||
protected:
|
||||
void validate_();
|
||||
MediaPlayer *const parent_;
|
||||
optional<MediaPlayerCommand> command_;
|
||||
optional<std::string> media_url_;
|
||||
optional<float> volume_;
|
||||
};
|
||||
|
||||
class MediaPlayer : public EntityBase {
|
||||
public:
|
||||
MediaPlayerState state{MEDIA_PLAYER_STATE_NONE};
|
||||
float volume{1.0f};
|
||||
|
||||
MediaPlayerCall make_call() { return MediaPlayerCall(this); }
|
||||
|
||||
void publish_state();
|
||||
|
||||
void add_on_state_callback(std::function<void()> &&callback);
|
||||
|
||||
virtual bool is_muted() const { return false; }
|
||||
|
||||
virtual MediaPlayerTraits get_traits() = 0;
|
||||
|
||||
protected:
|
||||
friend MediaPlayerCall;
|
||||
|
||||
virtual void control(const MediaPlayerCall &call) = 0;
|
||||
|
||||
CallbackManager<void()> state_callback_{};
|
||||
};
|
||||
|
||||
} // namespace media_player
|
||||
} // namespace esphome
|
@ -76,7 +76,12 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
|
||||
// installed, but wait, there is the CRC, and if we get a hit there is a good
|
||||
// chance that this is a complete message ... admittedly there is a small chance is
|
||||
// isn't but that is quite small given the purpose of the CRC in the first place
|
||||
data_len = at;
|
||||
|
||||
// Fewer than 2 bytes can't calc CRC
|
||||
if (at < 2)
|
||||
return true;
|
||||
|
||||
data_len = at - 2;
|
||||
data_offset = 1;
|
||||
|
||||
uint16_t computed_crc = crc16(raw, data_offset + data_len);
|
||||
@ -95,7 +100,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
|
||||
}
|
||||
|
||||
// Error ( msb indicates error )
|
||||
// response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc
|
||||
// response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] exception code, Byte[3-4] crc
|
||||
if ((function_code & 0x80) == 0x80) {
|
||||
data_offset = 2;
|
||||
data_len = 1;
|
||||
|
@ -70,7 +70,7 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_
|
||||
auto ¤t_command = this->command_queue_.front();
|
||||
if (current_command != nullptr) {
|
||||
ESP_LOGE(TAG,
|
||||
"Modbus error - last command: function code=0x%X register adddress = 0x%X "
|
||||
"Modbus error - last command: function code=0x%X register address = 0x%X "
|
||||
"registers count=%d "
|
||||
"payload size=%zu",
|
||||
function_code, current_command->register_address, current_command->register_count,
|
||||
@ -105,7 +105,7 @@ void ModbusController::on_register_data(ModbusRegisterType register_type, uint16
|
||||
}
|
||||
|
||||
void ModbusController::queue_command(const ModbusCommandItem &command) {
|
||||
// check if this commmand is already qeued.
|
||||
// check if this command is already qeued.
|
||||
// not very effective but the queue is never really large
|
||||
for (auto &item : command_queue_) {
|
||||
if (item->register_address == command.register_address && item->register_count == command.register_count &&
|
||||
@ -299,7 +299,7 @@ void ModbusController::loop() {
|
||||
incoming_queue_.pop();
|
||||
|
||||
} else {
|
||||
// all messages processed send pending commmands
|
||||
// all messages processed send pending commands
|
||||
send_next_command_();
|
||||
}
|
||||
}
|
||||
|
@ -185,8 +185,8 @@ inline bool coil_from_vector(int coil, const std::vector<uint8_t> &data) {
|
||||
|
||||
/** Extract bits from value and shift right according to the bitmask
|
||||
* if the bitmask is 0x00F0 we want the values frrom bit 5 - 8.
|
||||
* the result is then shifted right by the postion if the first right set bit in the mask
|
||||
* Usefull for modbus data where more than one value is packed in a 16 bit register
|
||||
* the result is then shifted right by the position if the first right set bit in the mask
|
||||
* Useful for modbus data where more than one value is packed in a 16 bit register
|
||||
* Example: on Epever the "Length of night" register 0x9065 encodes values of the whole night length of time as
|
||||
* D15 - D8 = hour, D7 - D0 = minute
|
||||
* To get the hours use mask 0xFF00 and 0x00FF for the minute
|
||||
@ -447,7 +447,7 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
||||
void dump_sensors_();
|
||||
/// Collection of all sensors for this component
|
||||
SensorSet sensorset_;
|
||||
/// Continous range of modbus registers
|
||||
/// Continuous range of modbus registers
|
||||
std::vector<RegisterRange> register_ranges_;
|
||||
/// Hold the pending requests to be sent
|
||||
std::list<std::unique_ptr<ModbusCommandItem>> command_queue_;
|
||||
|
@ -68,7 +68,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
|
||||
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
|
||||
# 24 bits are the maximum value for fp32 before precison is lost
|
||||
# 24 bits are the maximum value for fp32 before precision is lost
|
||||
# 0x00FFFFFF = 16777215
|
||||
cv.Optional(CONF_MAX_VALUE, default=16777215.0): cv.float_,
|
||||
cv.Optional(CONF_MIN_VALUE, default=-16777215.0): cv.float_,
|
||||
|
@ -24,6 +24,8 @@ from esphome.const import (
|
||||
CONF_LOG_TOPIC,
|
||||
CONF_ON_JSON_MESSAGE,
|
||||
CONF_ON_MESSAGE,
|
||||
CONF_ON_CONNECT,
|
||||
CONF_ON_DISCONNECT,
|
||||
CONF_PASSWORD,
|
||||
CONF_PAYLOAD,
|
||||
CONF_PAYLOAD_AVAILABLE,
|
||||
@ -90,6 +92,10 @@ MQTTMessageTrigger = mqtt_ns.class_(
|
||||
MQTTJsonMessageTrigger = mqtt_ns.class_(
|
||||
"MQTTJsonMessageTrigger", automation.Trigger.template(cg.JsonObjectConst)
|
||||
)
|
||||
MQTTConnectTrigger = mqtt_ns.class_("MQTTConnectTrigger", automation.Trigger.template())
|
||||
MQTTDisconnectTrigger = mqtt_ns.class_(
|
||||
"MQTTDisconnectTrigger", automation.Trigger.template()
|
||||
)
|
||||
MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component)
|
||||
MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition)
|
||||
|
||||
@ -212,6 +218,18 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
CONF_REBOOT_TIMEOUT, default="15min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTConnectTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
MQTTDisconnectTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTMessageTrigger),
|
||||
@ -362,6 +380,14 @@ async def to_code(config):
|
||||
trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC], conf[CONF_QOS])
|
||||
await automation.build_automation(trig, [(cg.JsonObjectConst, "x")], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_CONNECT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_DISCONNECT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
|
||||
MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
|
@ -187,7 +187,7 @@ void MQTTClientComponent::start_connect_() {
|
||||
|
||||
this->mqtt_backend_.set_credentials(username, password);
|
||||
|
||||
this->mqtt_backend_.set_server((uint32_t) this->ip_, this->credentials_.port);
|
||||
this->mqtt_backend_.set_server(this->credentials_.address.c_str(), this->credentials_.port);
|
||||
if (!this->last_will_.topic.empty()) {
|
||||
this->mqtt_backend_.set_will(this->last_will_.topic.c_str(), this->last_will_.qos, this->last_will_.retain,
|
||||
this->last_will_.payload.c_str());
|
||||
@ -572,6 +572,14 @@ void MQTTClientComponent::on_shutdown() {
|
||||
this->mqtt_backend_.disconnect();
|
||||
}
|
||||
|
||||
void MQTTClientComponent::set_on_connect(mqtt_on_connect_callback_t &&callback) {
|
||||
this->mqtt_backend_.set_on_connect(std::forward<mqtt_on_connect_callback_t>(callback));
|
||||
}
|
||||
|
||||
void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&callback) {
|
||||
this->mqtt_backend_.set_on_disconnect(std::forward<mqtt_on_disconnect_callback_t>(callback));
|
||||
}
|
||||
|
||||
#if ASYNC_TCP_SSL_ENABLED
|
||||
void MQTTClientComponent::add_ssl_fingerprint(const std::array<uint8_t, SHA1_SIZE> &fingerprint) {
|
||||
this->mqtt_backend_.setSecure(true);
|
||||
|
@ -19,6 +19,11 @@
|
||||
namespace esphome {
|
||||
namespace mqtt {
|
||||
|
||||
/** Callback for MQTT events.
|
||||
*/
|
||||
using mqtt_on_connect_callback_t = std::function<MQTTBackend::on_connect_callback_t>;
|
||||
using mqtt_on_disconnect_callback_t = std::function<MQTTBackend::on_disconnect_callback_t>;
|
||||
|
||||
/** Callback for MQTT subscriptions.
|
||||
*
|
||||
* First parameter is the topic, the second one is the payload.
|
||||
@ -240,6 +245,8 @@ class MQTTClientComponent : public Component {
|
||||
void set_username(const std::string &username) { this->credentials_.username = username; }
|
||||
void set_password(const std::string &password) { this->credentials_.password = password; }
|
||||
void set_client_id(const std::string &client_id) { this->credentials_.client_id = client_id; }
|
||||
void set_on_connect(mqtt_on_connect_callback_t &&callback);
|
||||
void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback);
|
||||
|
||||
protected:
|
||||
/// Reconnect to the MQTT broker if not already connected.
|
||||
@ -328,6 +335,20 @@ class MQTTJsonMessageTrigger : public Trigger<JsonObjectConst> {
|
||||
}
|
||||
};
|
||||
|
||||
class MQTTConnectTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit MQTTConnectTrigger(MQTTClientComponent *&client) {
|
||||
client->set_on_connect([this](bool session_present) { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class MQTTDisconnectTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit MQTTDisconnectTrigger(MQTTClientComponent *&client) {
|
||||
client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class MQTTPublishAction : public Action<Ts...> {
|
||||
public:
|
||||
MQTTPublishAction(MQTTClientComponent *parent) : parent_(parent) {}
|
||||
|
@ -114,6 +114,7 @@ void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig
|
||||
if (this->state_->get_traits().supports_speed()) {
|
||||
root[MQTT_PERCENTAGE_COMMAND_TOPIC] = this->get_speed_level_command_topic();
|
||||
root[MQTT_PERCENTAGE_STATE_TOPIC] = this->get_speed_level_state_topic();
|
||||
root[MQTT_SPEED_RANGE_MAX] = this->state_->get_traits().supported_speed_count();
|
||||
}
|
||||
}
|
||||
bool MQTTFanComponent::publish_state() {
|
||||
|
@ -115,7 +115,7 @@ void Nextion::set_backlight_brightness(float brightness) {
|
||||
ESP_LOGD(TAG, "Brightness out of bounds, percentage range 0-1.0");
|
||||
return;
|
||||
}
|
||||
this->add_no_result_to_queue_with_set("backlight_brightness", "dim", static_cast<int>(brightness * 100));
|
||||
this->add_no_result_to_queue_with_printf_("backlight_brightness", "dim=%d", static_cast<int>(brightness * 100));
|
||||
}
|
||||
|
||||
void Nextion::set_auto_wake_on_touch(bool auto_wake) {
|
||||
|
@ -12,7 +12,7 @@ NdefRecord::NdefRecord(std::vector<uint8_t> payload_data) {
|
||||
std::vector<uint8_t> NdefRecord::encode(bool first, bool last) {
|
||||
std::vector<uint8_t> data;
|
||||
|
||||
// Get encoded payload, this is overriden by more specific record classes
|
||||
// Get encoded payload, this is overridden by more specific record classes
|
||||
std::vector<uint8_t> payload_data = get_encoded_payload();
|
||||
|
||||
size_t payload_length = payload_data.size();
|
||||
|
@ -6,8 +6,17 @@ namespace number {
|
||||
|
||||
static const char *const TAG = "number.automation";
|
||||
|
||||
union convert {
|
||||
float from;
|
||||
uint32_t to;
|
||||
};
|
||||
|
||||
void ValueRangeTrigger::setup() {
|
||||
this->rtc_ = global_preferences->make_preference<bool>(this->parent_->get_object_id_hash());
|
||||
float local_min = this->min_.value(0.0);
|
||||
float local_max = this->max_.value(0.0);
|
||||
convert hash = {.from = (local_max - local_min)};
|
||||
uint32_t myhash = hash.to ^ this->parent_->get_object_id_hash();
|
||||
this->rtc_ = global_preferences->make_preference<bool>(myhash);
|
||||
bool initial_state;
|
||||
if (this->rtc_.load(&initial_state)) {
|
||||
this->previous_in_range_ = initial_state;
|
||||
|
@ -17,7 +17,5 @@ void Number::add_on_state_callback(std::function<void(float)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
uint32_t Number::hash_base() { return 2282307003UL; }
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
|
@ -52,8 +52,6 @@ class Number : public EntityBase {
|
||||
*/
|
||||
virtual void control(float value) = 0;
|
||||
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void(float)> state_callback_;
|
||||
bool has_state_{false};
|
||||
};
|
||||
|
@ -52,6 +52,8 @@ class PCD8544 : public PollingComponent,
|
||||
this->initialize();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
@ -768,7 +768,7 @@ uint8_t Pipsolar::check_incoming_length_(uint8_t length) {
|
||||
|
||||
uint8_t Pipsolar::check_incoming_crc_() {
|
||||
uint16_t crc16;
|
||||
crc16 = calc_crc_(read_buffer_, read_pos_ - 3);
|
||||
crc16 = cal_crc_half_(read_buffer_, read_pos_ - 3);
|
||||
ESP_LOGD(TAG, "checking crc on incoming message");
|
||||
if (((uint8_t)((crc16) >> 8)) == read_buffer_[read_pos_ - 3] &&
|
||||
((uint8_t)((crc16) &0xff)) == read_buffer_[read_pos_ - 2]) {
|
||||
@ -797,7 +797,7 @@ uint8_t Pipsolar::send_next_command_() {
|
||||
this->command_start_millis_ = millis();
|
||||
this->empty_uart_buffer_();
|
||||
this->read_pos_ = 0;
|
||||
crc16 = calc_crc_(byte_command, length);
|
||||
crc16 = cal_crc_half_(byte_command, length);
|
||||
this->write_str(command);
|
||||
// checksum
|
||||
this->write(((uint8_t)((crc16) >> 8))); // highbyte
|
||||
@ -824,8 +824,8 @@ void Pipsolar::send_next_poll_() {
|
||||
this->command_start_millis_ = millis();
|
||||
this->empty_uart_buffer_();
|
||||
this->read_pos_ = 0;
|
||||
crc16 = calc_crc_(this->used_polling_commands_[this->last_polling_command_].command,
|
||||
this->used_polling_commands_[this->last_polling_command_].length);
|
||||
crc16 = cal_crc_half_(this->used_polling_commands_[this->last_polling_command_].command,
|
||||
this->used_polling_commands_[this->last_polling_command_].length);
|
||||
this->write_array(this->used_polling_commands_[this->last_polling_command_].command,
|
||||
this->used_polling_commands_[this->last_polling_command_].length);
|
||||
// checksum
|
||||
@ -892,29 +892,41 @@ void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand poll
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t Pipsolar::calc_crc_(uint8_t *msg, int n) {
|
||||
// Initial value. xmodem uses 0xFFFF but this example
|
||||
// requires an initial value of zero.
|
||||
uint16_t x = 0;
|
||||
while (n--) {
|
||||
x = crc_xmodem_update_(x, (uint16_t) *msg++);
|
||||
}
|
||||
return (x);
|
||||
}
|
||||
uint16_t Pipsolar::cal_crc_half_(uint8_t *msg, uint8_t len) {
|
||||
uint16_t crc;
|
||||
|
||||
// See bottom of this page: http://www.nongnu.org/avr-libc/user-manual/group__util__crc.html
|
||||
// Polynomial: x^16 + x^12 + x^5 + 1 (0x1021)
|
||||
uint16_t Pipsolar::crc_xmodem_update_(uint16_t crc, uint8_t data) {
|
||||
int i;
|
||||
crc = crc ^ ((uint16_t) data << 8);
|
||||
for (i = 0; i < 8; i++) {
|
||||
if (crc & 0x8000) {
|
||||
crc = (crc << 1) ^ 0x1021; //(polynomial = 0x1021)
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
uint8_t da;
|
||||
uint8_t *ptr;
|
||||
uint8_t b_crc_hign;
|
||||
uint8_t b_crc_low;
|
||||
|
||||
uint16_t crc_ta[16] = {0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
|
||||
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef};
|
||||
|
||||
ptr = msg;
|
||||
crc = 0;
|
||||
|
||||
while (len-- != 0) {
|
||||
da = ((uint8_t)(crc >> 8)) >> 4;
|
||||
crc <<= 4;
|
||||
crc ^= crc_ta[da ^ (*ptr >> 4)];
|
||||
da = ((uint8_t)(crc >> 8)) >> 4;
|
||||
crc <<= 4;
|
||||
crc ^= crc_ta[da ^ (*ptr & 0x0f)];
|
||||
ptr++;
|
||||
}
|
||||
return crc;
|
||||
|
||||
b_crc_low = crc;
|
||||
b_crc_hign = (uint8_t)(crc >> 8);
|
||||
|
||||
if (b_crc_low == 0x28 || b_crc_low == 0x0d || b_crc_low == 0x0a)
|
||||
b_crc_low++;
|
||||
if (b_crc_hign == 0x28 || b_crc_hign == 0x0d || b_crc_hign == 0x0a)
|
||||
b_crc_hign++;
|
||||
|
||||
crc = ((uint16_t) b_crc_hign) << 8;
|
||||
crc += b_crc_low;
|
||||
return (crc);
|
||||
}
|
||||
|
||||
} // namespace pipsolar
|
||||
|
@ -193,8 +193,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
||||
void empty_uart_buffer_();
|
||||
uint8_t check_incoming_crc_();
|
||||
uint8_t check_incoming_length_(uint8_t length);
|
||||
uint16_t calc_crc_(uint8_t *msg, int n);
|
||||
uint16_t crc_xmodem_update_(uint16_t crc, uint8_t data);
|
||||
uint16_t cal_crc_half_(uint8_t *msg, uint8_t len);
|
||||
uint8_t send_next_command_();
|
||||
void send_next_poll_();
|
||||
void queue_command_(const char *command, uint8_t length);
|
||||
|
@ -1,6 +1,9 @@
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INCLUDE_INTERNAL,
|
||||
)
|
||||
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
|
||||
from esphome.components import web_server_base
|
||||
|
||||
@ -15,6 +18,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(
|
||||
web_server_base.WebServerBase
|
||||
),
|
||||
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
|
||||
},
|
||||
cv.only_with_arduino,
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
@ -27,3 +31,5 @@ async def to_code(config):
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID], paren)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL]))
|
||||
|
@ -61,7 +61,7 @@ void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) {
|
||||
stream->print(F("#TYPE esphome_sensor_failed GAUGE\n"));
|
||||
}
|
||||
void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) {
|
||||
if (obj->is_internal())
|
||||
if (obj->is_internal() && !this->include_internal_)
|
||||
return;
|
||||
if (!std::isnan(obj->state)) {
|
||||
// We have a valid value, output this value
|
||||
@ -98,7 +98,7 @@ void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) {
|
||||
stream->print(F("#TYPE esphome_binary_sensor_failed GAUGE\n"));
|
||||
}
|
||||
void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj) {
|
||||
if (obj->is_internal())
|
||||
if (obj->is_internal() && !this->include_internal_)
|
||||
return;
|
||||
if (obj->has_state()) {
|
||||
// We have a valid value, output this value
|
||||
@ -134,7 +134,7 @@ void PrometheusHandler::fan_type_(AsyncResponseStream *stream) {
|
||||
stream->print(F("#TYPE esphome_fan_oscillation GAUGE\n"));
|
||||
}
|
||||
void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) {
|
||||
if (obj->is_internal())
|
||||
if (obj->is_internal() && !this->include_internal_)
|
||||
return;
|
||||
stream->print(F("esphome_fan_failed{id=\""));
|
||||
stream->print(obj->get_object_id().c_str());
|
||||
@ -179,7 +179,7 @@ void PrometheusHandler::light_type_(AsyncResponseStream *stream) {
|
||||
stream->print(F("#TYPE esphome_light_effect_active GAUGE\n"));
|
||||
}
|
||||
void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj) {
|
||||
if (obj->is_internal())
|
||||
if (obj->is_internal() && !this->include_internal_)
|
||||
return;
|
||||
// State
|
||||
stream->print(F("esphome_light_state{id=\""));
|
||||
@ -255,7 +255,7 @@ void PrometheusHandler::cover_type_(AsyncResponseStream *stream) {
|
||||
stream->print(F("#TYPE esphome_cover_failed GAUGE\n"));
|
||||
}
|
||||
void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) {
|
||||
if (obj->is_internal())
|
||||
if (obj->is_internal() && !this->include_internal_)
|
||||
return;
|
||||
if (!std::isnan(obj->position)) {
|
||||
// We have a valid value, output this value
|
||||
@ -298,7 +298,7 @@ void PrometheusHandler::switch_type_(AsyncResponseStream *stream) {
|
||||
stream->print(F("#TYPE esphome_switch_failed GAUGE\n"));
|
||||
}
|
||||
void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj) {
|
||||
if (obj->is_internal())
|
||||
if (obj->is_internal() && !this->include_internal_)
|
||||
return;
|
||||
stream->print(F("esphome_switch_failed{id=\""));
|
||||
stream->print(obj->get_object_id().c_str());
|
||||
@ -322,7 +322,7 @@ void PrometheusHandler::lock_type_(AsyncResponseStream *stream) {
|
||||
stream->print(F("#TYPE esphome_lock_failed GAUGE\n"));
|
||||
}
|
||||
void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) {
|
||||
if (obj->is_internal())
|
||||
if (obj->is_internal() && !this->include_internal_)
|
||||
return;
|
||||
stream->print(F("esphome_lock_failed{id=\""));
|
||||
stream->print(obj->get_object_id().c_str());
|
||||
|
@ -13,6 +13,13 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
|
||||
public:
|
||||
PrometheusHandler(web_server_base::WebServerBase *base) : base_(base) {}
|
||||
|
||||
/** Determine whether internal components should be exported as metrics.
|
||||
* Defaults to false.
|
||||
*
|
||||
* @param include_internal Whether internal components should be exported.
|
||||
*/
|
||||
void set_include_internal(bool include_internal) { include_internal_ = include_internal; }
|
||||
|
||||
bool canHandle(AsyncWebServerRequest *request) override {
|
||||
if (request->method() == HTTP_GET) {
|
||||
if (request->url() == "/metrics")
|
||||
@ -84,6 +91,7 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
|
||||
#endif
|
||||
|
||||
web_server_base::WebServerBase *base_;
|
||||
bool include_internal_{false};
|
||||
};
|
||||
|
||||
} // namespace prometheus
|
||||
|
@ -32,7 +32,7 @@ from esphome.const import (
|
||||
CONF_LEVEL,
|
||||
)
|
||||
from esphome.core import coroutine
|
||||
from esphome.jsonschema import jschema_extractor
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.util import Registry, SimpleRegistry
|
||||
|
||||
AUTO_LOAD = ["binary_sensor"]
|
||||
@ -195,14 +195,14 @@ def validate_dumpers(value):
|
||||
def validate_triggers(base_schema):
|
||||
assert isinstance(base_schema, cv.Schema)
|
||||
|
||||
@jschema_extractor("triggers")
|
||||
@schema_extractor("triggers")
|
||||
def validator(config):
|
||||
added_keys = {}
|
||||
for key, (_, valid) in TRIGGER_REGISTRY.items():
|
||||
added_keys[cv.Optional(key)] = valid
|
||||
new_schema = base_schema.extend(added_keys)
|
||||
# pylint: disable=comparison-with-callable
|
||||
if config == jschema_extractor:
|
||||
|
||||
if config == SCHEMA_EXTRACT:
|
||||
return new_schema
|
||||
return new_schema(config)
|
||||
|
||||
@ -728,6 +728,48 @@ async def rc5_action(var, config, args):
|
||||
cg.add(var.set_command(template_))
|
||||
|
||||
|
||||
# RC6
|
||||
RC6Data, RC6BinarySensor, RC6Trigger, RC6Action, RC6Dumper = declare_protocol("RC6")
|
||||
RC6_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ADDRESS): cv.hex_uint8_t,
|
||||
cv.Required(CONF_COMMAND): cv.hex_uint8_t,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@register_binary_sensor("rc6", RC6BinarySensor, RC6_SCHEMA)
|
||||
def rc6_binary_sensor(var, config):
|
||||
cg.add(
|
||||
var.set_data(
|
||||
cg.StructInitializer(
|
||||
RC6Data,
|
||||
("device", config[CONF_DEVICE]),
|
||||
("address", config[CONF_ADDRESS]),
|
||||
("command", config[CONF_COMMAND]),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@register_trigger("rc6", RC6Trigger, RC6Data)
|
||||
def rc6_trigger(var, config):
|
||||
pass
|
||||
|
||||
|
||||
@register_dumper("rc6", RC6Dumper)
|
||||
def rc6_dumper(var, config):
|
||||
pass
|
||||
|
||||
|
||||
@register_action("rc6", RC6Action, RC6_SCHEMA)
|
||||
async def rc6_action(var, config, args):
|
||||
template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
|
||||
cg.add(var.set_address(template_))
|
||||
template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8)
|
||||
cg.add(var.set_command(template_))
|
||||
|
||||
|
||||
# RC Switch Raw
|
||||
RC_SWITCH_TIMING_SCHEMA = cv.All([cv.uint8_t], cv.Length(min=2, max=2))
|
||||
|
||||
|
@ -106,7 +106,7 @@ optional<NexaData> NexaProtocol::decode(RemoteReceiveData src) {
|
||||
SHHHH HHHH HHHH HHHH HHHH HHHH HHGO EE BB DDDD 0 P
|
||||
|
||||
S = Sync bit.
|
||||
H = The first 26 bits are transmitter unique codes, and it is this code that the reciever "learns" to recognize.
|
||||
H = The first 26 bits are transmitter unique codes, and it is this code that the receiver "learns" to recognize.
|
||||
G = Group code, set to one for the whole group.
|
||||
O = On/Off bit. Set to 1 for on, 0 for off.
|
||||
E = Unit to be turned on or off. The code is inverted, i.e. '11' equals 1, '00' equals 4.
|
||||
|
181
esphome/components/remote_base/rc6_protocol.cpp
Normal file
181
esphome/components/remote_base/rc6_protocol.cpp
Normal file
@ -0,0 +1,181 @@
|
||||
#include "rc6_protocol.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace remote_base {
|
||||
|
||||
static const char *const RC6_TAG = "remote.rc6";
|
||||
|
||||
static const uint16_t RC6_FREQ = 36000;
|
||||
static const uint16_t RC6_UNIT = 444;
|
||||
static const uint16_t RC6_HEADER_MARK = (6 * RC6_UNIT);
|
||||
static const uint16_t RC6_HEADER_SPACE = (2 * RC6_UNIT);
|
||||
static const uint16_t RC6_MODE_MASK = 0x07;
|
||||
|
||||
void RC6Protocol::encode(RemoteTransmitData *dst, const RC6Data &data) {
|
||||
dst->reserve(44);
|
||||
dst->set_carrier_frequency(RC6_FREQ);
|
||||
|
||||
// Encode header
|
||||
dst->item(RC6_HEADER_MARK, RC6_HEADER_SPACE);
|
||||
|
||||
int32_t next{0};
|
||||
|
||||
// Encode startbit+mode
|
||||
uint8_t header{static_cast<uint8_t>((1 << 3) | data.mode)};
|
||||
|
||||
for (uint8_t mask = 0x8; mask; mask >>= 1) {
|
||||
if (header & mask) {
|
||||
if (next < 0) {
|
||||
dst->space(-next);
|
||||
next = 0;
|
||||
}
|
||||
if (next >= 0) {
|
||||
next = next + RC6_UNIT;
|
||||
dst->mark(next);
|
||||
next = -RC6_UNIT;
|
||||
}
|
||||
} else {
|
||||
if (next > 0) {
|
||||
dst->mark(next);
|
||||
next = 0;
|
||||
}
|
||||
if (next <= 0) {
|
||||
next = next - RC6_UNIT;
|
||||
dst->space(-next);
|
||||
next = RC6_UNIT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle
|
||||
if (data.toggle) {
|
||||
if (next < 0) {
|
||||
dst->space(-next);
|
||||
next = 0;
|
||||
}
|
||||
if (next >= 0) {
|
||||
next = next + RC6_UNIT * 2;
|
||||
dst->mark(next);
|
||||
next = -RC6_UNIT * 2;
|
||||
}
|
||||
} else {
|
||||
if (next > 0) {
|
||||
dst->mark(next);
|
||||
next = 0;
|
||||
}
|
||||
if (next <= 0) {
|
||||
next = next - RC6_UNIT * 2;
|
||||
dst->space(-next);
|
||||
next = RC6_UNIT * 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Encode data
|
||||
uint16_t raw{static_cast<uint16_t>((data.address << 8) | data.command)};
|
||||
|
||||
for (uint16_t mask = 0x8000; mask; mask >>= 1) {
|
||||
if (raw & mask) {
|
||||
if (next < 0) {
|
||||
dst->space(-next);
|
||||
next = 0;
|
||||
}
|
||||
if (next >= 0) {
|
||||
next = next + RC6_UNIT;
|
||||
dst->mark(next);
|
||||
next = -RC6_UNIT;
|
||||
}
|
||||
} else {
|
||||
if (next > 0) {
|
||||
dst->mark(next);
|
||||
next = 0;
|
||||
}
|
||||
if (next <= 0) {
|
||||
next = next - RC6_UNIT;
|
||||
dst->space(-next);
|
||||
next = RC6_UNIT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (next > 0) {
|
||||
dst->mark(next);
|
||||
} else {
|
||||
dst->space(-next);
|
||||
}
|
||||
}
|
||||
|
||||
optional<RC6Data> RC6Protocol::decode(RemoteReceiveData src) {
|
||||
RC6Data data{
|
||||
.mode = 0,
|
||||
.toggle = 0,
|
||||
.address = 0,
|
||||
.command = 0,
|
||||
};
|
||||
|
||||
// Check if header matches
|
||||
if (!src.expect_item(RC6_HEADER_MARK, RC6_HEADER_SPACE)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
uint8_t bit{1};
|
||||
uint8_t offset{0};
|
||||
uint8_t header{0};
|
||||
uint32_t buffer{0};
|
||||
|
||||
// Startbit + mode
|
||||
while (offset < 4) {
|
||||
bit = src.peek() > 0;
|
||||
header = header + (bit << (3 - offset++));
|
||||
src.advance();
|
||||
|
||||
if (src.peek_mark(RC6_UNIT) || src.peek_space(RC6_UNIT)) {
|
||||
src.advance();
|
||||
} else if (offset == 4) {
|
||||
break;
|
||||
} else if (!src.peek_mark(RC6_UNIT * 2) && !src.peek_space(RC6_UNIT * 2)) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
data.mode = header & RC6_MODE_MASK;
|
||||
|
||||
if (data.mode != 0) {
|
||||
return {}; // I dont have a device to test other modes
|
||||
}
|
||||
|
||||
// Toggle
|
||||
data.toggle = src.peek() > 0;
|
||||
src.advance();
|
||||
if (src.peek_mark(RC6_UNIT * 2) || src.peek_space(RC6_UNIT * 2)) {
|
||||
src.advance();
|
||||
}
|
||||
|
||||
// Data
|
||||
offset = 0;
|
||||
while (offset < 16) {
|
||||
bit = src.peek() > 0;
|
||||
buffer = buffer + (bit << (15 - offset++));
|
||||
src.advance();
|
||||
|
||||
if (offset == 16) {
|
||||
break;
|
||||
} else if (src.peek_mark(RC6_UNIT) || src.peek_space(RC6_UNIT)) {
|
||||
src.advance();
|
||||
} else if (!src.peek_mark(RC6_UNIT * 2) && !src.peek_space(RC6_UNIT * 2)) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
data.address = (0xFF00 & buffer) >> 8;
|
||||
data.command = (0x00FF & buffer);
|
||||
return data;
|
||||
}
|
||||
|
||||
void RC6Protocol::dump(const RC6Data &data) {
|
||||
ESP_LOGD(RC6_TAG, "Received RC6: mode=0x%X, address=0x%02X, command=0x%02X, toggle=0x%X", data.mode, data.address,
|
||||
data.command, data.toggle);
|
||||
}
|
||||
|
||||
} // namespace remote_base
|
||||
} // namespace esphome
|
46
esphome/components/remote_base/rc6_protocol.h
Normal file
46
esphome/components/remote_base/rc6_protocol.h
Normal file
@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "remote_base.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace remote_base {
|
||||
|
||||
struct RC6Data {
|
||||
uint8_t mode : 3;
|
||||
uint8_t toggle : 1;
|
||||
uint8_t address;
|
||||
uint8_t command;
|
||||
|
||||
bool operator==(const RC6Data &rhs) const { return address == rhs.address && command == rhs.command; }
|
||||
};
|
||||
|
||||
class RC6Protocol : public RemoteProtocol<RC6Data> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const RC6Data &data) override;
|
||||
optional<RC6Data> decode(RemoteReceiveData src) override;
|
||||
void dump(const RC6Data &data) override;
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(RC6)
|
||||
|
||||
template<typename... Ts> class RC6Action : public RemoteTransmitterActionBase<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint8_t, address)
|
||||
TEMPLATABLE_VALUE(uint8_t, command)
|
||||
|
||||
void encode(RemoteTransmitData *dst, Ts... x) {
|
||||
RC6Data data{};
|
||||
data.mode = 0;
|
||||
data.toggle = this->toggle_;
|
||||
data.address = this->address_.value(x...);
|
||||
data.command = this->command_.value(x...);
|
||||
RC6Protocol().encode(dst, data);
|
||||
this->toggle_ = !this->toggle_;
|
||||
}
|
||||
|
||||
protected:
|
||||
uint8_t toggle_{0};
|
||||
};
|
||||
|
||||
} // namespace remote_base
|
||||
} // namespace esphome
|
@ -102,7 +102,7 @@ bool RCSwitchBase::expect_sync(RemoteReceiveData &src) const {
|
||||
if (!src.peek_space(this->sync_low_, 1))
|
||||
return false;
|
||||
} else {
|
||||
// We cant peek a space at the beginning because signals starts with a low to high transition.
|
||||
// We can't peek a space at the beginning because signals starts with a low to high transition.
|
||||
// this long space at the beginning is the separation between the transmissions itself, so it is actually
|
||||
// added at the end kind of artificially (by the value given to "idle:" option by the user in the yaml)
|
||||
if (!src.peek_mark(this->sync_low_))
|
||||
|
@ -103,7 +103,7 @@ void IRAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensorStore
|
||||
rotation_dir = -1;
|
||||
}
|
||||
|
||||
if (rotation_dir != 0) {
|
||||
if (rotation_dir != 0 && !arg->first_read) {
|
||||
auto *first_zero = std::find(arg->rotation_events.begin(), arg->rotation_events.end(), 0); // find first zero
|
||||
if (first_zero == arg->rotation_events.begin() // are we at the start (first event this loop iteration)
|
||||
|| std::signbit(*std::prev(first_zero)) !=
|
||||
@ -119,6 +119,7 @@ void IRAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensorStore
|
||||
*std::prev(first_zero) += rotation_dir; // store the rotation into the previous slot
|
||||
}
|
||||
}
|
||||
arg->first_read = false;
|
||||
|
||||
arg->state = new_state;
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ struct RotaryEncoderSensorStore {
|
||||
int32_t max_value{INT32_MAX};
|
||||
int32_t last_read{0};
|
||||
uint8_t state{0};
|
||||
bool first_read{true};
|
||||
|
||||
std::array<int8_t, 8> rotation_events{};
|
||||
bool rotation_events_overflow{false};
|
||||
|
@ -205,7 +205,7 @@ void SCD4XComponent::update() {
|
||||
bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentration) {
|
||||
/*
|
||||
Operate the SCD4x in the operation mode later used in normal sensor operation (periodic measurement, low power
|
||||
periodic measurement or single shot) for > 3 minutes in an environment with homogenous and constant CO2
|
||||
periodic measurement or single shot) for > 3 minutes in an environment with homogeneous and constant CO2
|
||||
concentration before performing a forced recalibration.
|
||||
*/
|
||||
if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
|
||||
@ -217,7 +217,7 @@ bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentrati
|
||||
ESP_LOGD(TAG, "setting forced calibration Co2 level %d ppm", current_co2_concentration);
|
||||
// frc takes 400 ms
|
||||
// because this method will be used very rarly
|
||||
// the simple aproach with delay is ok
|
||||
// the simple approach with delay is ok
|
||||
delay(400); // NOLINT'
|
||||
if (!this->start_measurement_()) {
|
||||
return false;
|
||||
|
@ -40,7 +40,7 @@ void SDP3XComponent::setup() {
|
||||
}
|
||||
|
||||
uint16_t data[6];
|
||||
if (this->read_data(data, 6) != i2c::ERROR_OK) {
|
||||
if (!this->read_data(data, 6)) {
|
||||
ESP_LOGE(TAG, "Read ID SDP3X failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
@ -78,8 +78,7 @@ void SDP3XComponent::setup() {
|
||||
}
|
||||
}
|
||||
|
||||
if (this->write_command(measurement_mode_ == DP_AVG ? SDP3X_START_DP_AVG : SDP3X_START_MASS_FLOW_AVG) !=
|
||||
i2c::ERROR_OK) {
|
||||
if (!this->write_command(measurement_mode_ == DP_AVG ? SDP3X_START_DP_AVG : SDP3X_START_MASS_FLOW_AVG)) {
|
||||
ESP_LOGE(TAG, "Start Measurements SDP3X failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
@ -98,7 +97,7 @@ void SDP3XComponent::dump_config() {
|
||||
|
||||
void SDP3XComponent::read_pressure_() {
|
||||
uint16_t data[3];
|
||||
if (this->read_data(data, 3) != i2c::ERROR_OK) {
|
||||
if (!this->read_data(data, 3)) {
|
||||
ESP_LOGW(TAG, "Couldn't read SDP3X data!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
|
@ -58,7 +58,5 @@ optional<std::string> Select::at(size_t index) const {
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Select::hash_base() { return 2812997003UL; }
|
||||
|
||||
} // namespace select
|
||||
} // namespace esphome
|
||||
|
@ -65,8 +65,6 @@ class Select : public EntityBase {
|
||||
*/
|
||||
virtual void control(const std::string &value) = 0;
|
||||
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void(std::string, size_t)> state_callback_;
|
||||
bool has_state_{false};
|
||||
};
|
||||
|
@ -33,7 +33,7 @@ bool SensirionI2CDevice::read_data(uint16_t *data, uint8_t len) {
|
||||
}
|
||||
/***
|
||||
* write command with parameters and insert crc
|
||||
* use stack array for less than 4 paramaters. Most sensirion i2c commands have less parameters
|
||||
* use stack array for less than 4 parameters. Most sensirion i2c commands have less parameters
|
||||
*/
|
||||
bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len, const uint16_t *data,
|
||||
uint8_t data_len) {
|
||||
@ -63,7 +63,7 @@ bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len
|
||||
temp[raw_idx++] = command >> 8;
|
||||
#endif
|
||||
}
|
||||
// add parameters folllowed by crc
|
||||
// add parameters followed by crc
|
||||
// skipped if len == 0
|
||||
for (size_t i = 0; i < data_len; i++) {
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
|
@ -20,13 +20,13 @@ class SensirionI2CDevice : public i2c::I2CDevice {
|
||||
* handles crc check used by Sensirion sensors
|
||||
* @param data pointer to raw result
|
||||
* @param len number of words to read
|
||||
* @return true if reading succeded
|
||||
* @return true if reading succeeded
|
||||
*/
|
||||
bool read_data(uint16_t *data, uint8_t len);
|
||||
|
||||
/** Read 1 data word from i2c device.
|
||||
* @param data reference to raw result
|
||||
* @return true if reading succeded
|
||||
* @return true if reading succeeded
|
||||
*/
|
||||
bool read_data(uint16_t &data) { return this->read_data(&data, 1); }
|
||||
|
||||
@ -35,8 +35,8 @@ class SensirionI2CDevice : public i2c::I2CDevice {
|
||||
* @param i2c register
|
||||
* @param data pointer to raw result
|
||||
* @param len number of words to read
|
||||
* @param delay milliseconds to to wait between sending the i2c commmand and reading the result
|
||||
* @return true if reading succeded
|
||||
* @param delay milliseconds to to wait between sending the i2c command and reading the result
|
||||
* @return true if reading succeeded
|
||||
*/
|
||||
bool get_register(uint16_t command, uint16_t *data, uint8_t len, uint8_t delay = 0) {
|
||||
return get_register_(command, ADDR_16_BIT, data, len, delay);
|
||||
@ -44,8 +44,8 @@ class SensirionI2CDevice : public i2c::I2CDevice {
|
||||
/** Read 1 data word from 16 bit i2c register.
|
||||
* @param i2c register
|
||||
* @param data reference to raw result
|
||||
* @param delay milliseconds to to wait between sending the i2c commmand and reading the result
|
||||
* @return true if reading succeded
|
||||
* @param delay milliseconds to to wait between sending the i2c command and reading the result
|
||||
* @return true if reading succeeded
|
||||
*/
|
||||
bool get_register(uint16_t i2c_register, uint16_t &data, uint8_t delay = 0) {
|
||||
return this->get_register_(i2c_register, ADDR_16_BIT, &data, 1, delay);
|
||||
@ -56,8 +56,8 @@ class SensirionI2CDevice : public i2c::I2CDevice {
|
||||
* @param i2c register
|
||||
* @param data pointer to raw result
|
||||
* @param len number of words to read
|
||||
* @param delay milliseconds to to wait between sending the i2c commmand and reading the result
|
||||
* @return true if reading succeded
|
||||
* @param delay milliseconds to to wait between sending the i2c command and reading the result
|
||||
* @return true if reading succeeded
|
||||
*/
|
||||
bool get_8bit_register(uint8_t i2c_register, uint16_t *data, uint8_t len, uint8_t delay = 0) {
|
||||
return get_register_(i2c_register, ADDR_8_BIT, data, len, delay);
|
||||
@ -66,8 +66,8 @@ class SensirionI2CDevice : public i2c::I2CDevice {
|
||||
/** Read 1 data word from 8 bit i2c register.
|
||||
* @param i2c register
|
||||
* @param data reference to raw result
|
||||
* @param delay milliseconds to to wait between sending the i2c commmand and reading the result
|
||||
* @return true if reading succeded
|
||||
* @param delay milliseconds to to wait between sending the i2c command and reading the result
|
||||
* @return true if reading succeeded
|
||||
*/
|
||||
bool get_8bit_register(uint8_t i2c_register, uint16_t &data, uint8_t delay = 0) {
|
||||
return this->get_register_(i2c_register, ADDR_8_BIT, &data, 1, delay);
|
||||
@ -75,21 +75,21 @@ class SensirionI2CDevice : public i2c::I2CDevice {
|
||||
|
||||
/** Write a command to the i2c device.
|
||||
* @param command i2c command to send
|
||||
* @return true if reading succeded
|
||||
* @return true if reading succeeded
|
||||
*/
|
||||
template<class T> bool write_command(T i2c_register) { return write_command(i2c_register, nullptr, 0); }
|
||||
|
||||
/** Write a command and one data word to the i2c device .
|
||||
* @param command i2c command to send
|
||||
* @param data argument for the i2c command
|
||||
* @return true if reading succeded
|
||||
* @return true if reading succeeded
|
||||
*/
|
||||
template<class T> bool write_command(T i2c_register, uint16_t data) { return write_command(i2c_register, &data, 1); }
|
||||
|
||||
/** Write a command with arguments as words
|
||||
* @param i2c_register i2c command to send - an be uint8_t or uint16_t
|
||||
* @param data vector<uint16> arguments for the i2c command
|
||||
* @return true if reading succeded
|
||||
* @return true if reading succeeded
|
||||
*/
|
||||
template<class T> bool write_command(T i2c_register, const std::vector<uint16_t> &data) {
|
||||
return write_command_(i2c_register, sizeof(T), data.data(), data.size());
|
||||
@ -99,7 +99,7 @@ class SensirionI2CDevice : public i2c::I2CDevice {
|
||||
* @param i2c_register i2c command to send - an be uint8_t or uint16_t
|
||||
* @param data arguments for the i2c command
|
||||
* @param len number of arguments (words)
|
||||
* @return true if reading succeded
|
||||
* @return true if reading succeeded
|
||||
*/
|
||||
template<class T> bool write_command(T i2c_register, const uint16_t *data, uint8_t len) {
|
||||
// limit to 8 or 16 bit only
|
||||
@ -115,7 +115,7 @@ class SensirionI2CDevice : public i2c::I2CDevice {
|
||||
* @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes
|
||||
* @param data arguments for the i2c command
|
||||
* @param data_len number of arguments (words)
|
||||
* @return true if reading succeded
|
||||
* @return true if reading succeeded
|
||||
*/
|
||||
bool write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, uint8_t data_len);
|
||||
|
||||
@ -125,8 +125,8 @@ class SensirionI2CDevice : public i2c::I2CDevice {
|
||||
* @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes
|
||||
* @param data pointer to raw result
|
||||
* @param len number of words to read
|
||||
* @param delay milliseconds to to wait between sending the i2c commmand and reading the result
|
||||
* @return true if reading succeded
|
||||
* @param delay milliseconds to to wait between sending the i2c command and reading the result
|
||||
* @return true if reading succeeded
|
||||
*/
|
||||
bool get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, uint8_t delay);
|
||||
|
||||
|
@ -31,12 +31,15 @@ from esphome.const import (
|
||||
CONF_FORCE_UPDATE,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_CARBON_MONOXIDE,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_DATE,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_FREQUENCY,
|
||||
DEVICE_CLASS_GAS,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
@ -51,6 +54,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_REACTIVE_POWER,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
DEVICE_CLASS_SULPHUR_DIOXIDE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@ -66,13 +70,16 @@ from esphome.util import Registry
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_CARBON_MONOXIDE,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_DATE,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_FREQUENCY,
|
||||
DEVICE_CLASS_GAS,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
@ -87,6 +94,7 @@ DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_REACTIVE_POWER,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
DEVICE_CLASS_SULPHUR_DIOXIDE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
|
@ -126,7 +126,6 @@ void Sensor::internal_send_state_to_frontend(float state) {
|
||||
this->callback_.call(state);
|
||||
}
|
||||
bool Sensor::has_state() const { return this->has_state_; }
|
||||
uint32_t Sensor::hash_base() { return 2455723294UL; }
|
||||
|
||||
} // namespace sensor
|
||||
} // namespace esphome
|
||||
|
@ -174,8 +174,6 @@ class Sensor : public EntityBase {
|
||||
*/
|
||||
virtual StateClass state_class(); // NOLINT
|
||||
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void(float)> raw_callback_; ///< Storage for raw state callbacks.
|
||||
CallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
|
||||
|
||||
|
@ -140,5 +140,7 @@ async def to_code(config):
|
||||
)
|
||||
)
|
||||
cg.add_library(
|
||||
None, None, "https://github.com/Sensirion/arduino-gas-index-algorithm.git"
|
||||
None,
|
||||
None,
|
||||
"https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1",
|
||||
)
|
||||
|
@ -39,7 +39,7 @@ void SGP4xComponent::setup() {
|
||||
ESP_LOGE(TAG, "Measuring NOx requires a SGP41 sensor but a SGP40 sensor is detected");
|
||||
// disable the sensor
|
||||
this->nox_sensor_->set_disabled_by_default(true);
|
||||
// make sure it's not visiable in HA
|
||||
// make sure it's not visible in HA
|
||||
this->nox_sensor_->set_internal(true);
|
||||
this->nox_sensor_->state = NAN;
|
||||
// remove pointer to sensor
|
||||
@ -104,8 +104,8 @@ void SGP4xComponent::setup() {
|
||||
https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit resilient to slight
|
||||
timing variations so the software timer should be accurate enough for this.
|
||||
|
||||
This block starts sampling from the sensor at 1Hz, and is done seperately from the call
|
||||
to the update method. This seperation is to support getting accurate measurements but
|
||||
This block starts sampling from the sensor at 1Hz, and is done separately from the call
|
||||
to the update method. This separation is to support getting accurate measurements but
|
||||
limit the amount of communication done over wifi for power consumption or to keep the
|
||||
number of records reported from being overwhelming.
|
||||
*/
|
||||
@ -170,8 +170,8 @@ bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) {
|
||||
// much
|
||||
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
|
||||
voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
|
||||
if ((uint32_t) abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF ||
|
||||
(uint32_t) abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) {
|
||||
if (std::abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF ||
|
||||
std::abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) {
|
||||
this->seconds_since_last_store_ = 0;
|
||||
this->voc_baselines_storage_.state0 = this->voc_state0_;
|
||||
this->voc_baselines_storage_.state1 = this->voc_state1_;
|
||||
@ -236,9 +236,9 @@ bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) {
|
||||
}
|
||||
uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100));
|
||||
uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175);
|
||||
// first paramater are the relative humidity ticks
|
||||
// first parameter are the relative humidity ticks
|
||||
data[0] = rhticks;
|
||||
// secomd paramater are the temperature ticks
|
||||
// secomd parameter are the temperature ticks
|
||||
data[1] = tempticks;
|
||||
|
||||
if (!this->write_command(command, data, 2)) {
|
||||
|
@ -49,7 +49,7 @@ static const uint16_t SPG41_SELFTEST_TIME = 320; // 320 ms for self test
|
||||
static const uint16_t SGP40_MEASURE_TIME = 30;
|
||||
static const uint16_t SGP41_MEASURE_TIME = 55;
|
||||
// Store anyway if the baseline difference exceeds the max storage diff value
|
||||
const uint32_t MAXIMUM_STORAGE_DIFF = 50;
|
||||
const float MAXIMUM_STORAGE_DIFF = 50.0f;
|
||||
|
||||
class SGP4xComponent;
|
||||
|
||||
@ -120,8 +120,8 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se
|
||||
sensor::Sensor *voc_sensor_{nullptr};
|
||||
VOCGasIndexAlgorithm voc_algorithm_;
|
||||
optional<GasTuning> voc_tuning_params_;
|
||||
int32_t voc_state0_;
|
||||
int32_t voc_state1_;
|
||||
float voc_state0_;
|
||||
float voc_state1_;
|
||||
int32_t voc_index_ = 0;
|
||||
|
||||
sensor::Sensor *nox_sensor_{nullptr};
|
||||
|
@ -49,7 +49,7 @@ constexpr float POWER_SCALING_FACTOR = 880373;
|
||||
constexpr float VOLTAGE_SCALING_FACTOR = 347800;
|
||||
constexpr float CURRENT_SCALING_FACTOR = 1448;
|
||||
|
||||
// Esentially std::size() for pre c++17
|
||||
// Essentially std::size() for pre c++17
|
||||
template<typename T, size_t N> constexpr size_t size(const T (&/*unused*/)[N]) noexcept { return N; }
|
||||
|
||||
} // Anonymous namespace
|
||||
|
@ -42,12 +42,12 @@ void SlowPWMOutput::loop() {
|
||||
uint32_t now = millis();
|
||||
float scaled_state = this->state_ * this->period_;
|
||||
|
||||
if (now >= this->period_start_time_ + this->period_) {
|
||||
if (now - this->period_start_time_ >= this->period_) {
|
||||
ESP_LOGVV(TAG, "End of period. State: %f, Scaled state: %f", this->state_, scaled_state);
|
||||
this->period_start_time_ += this->period_;
|
||||
}
|
||||
|
||||
this->set_output_state_(now < this->period_start_time_ + scaled_state);
|
||||
this->set_output_state_(scaled_state > now - this->period_start_time_);
|
||||
}
|
||||
|
||||
void SlowPWMOutput::dump_config() {
|
||||
|
@ -161,7 +161,7 @@ bool SonoffD1Output::write_command_(uint8_t *cmd, const size_t len, bool needs_a
|
||||
return false;
|
||||
}
|
||||
if ((cmd[5] + 7 /*mandatory header + suffix length*/) != len) {
|
||||
ESP_LOGW(TAG, "[%04d] Payload length field does not match packet lenght (%d, expected %d)", this->write_count_,
|
||||
ESP_LOGW(TAG, "[%04d] Payload length field does not match packet length (%d, expected %d)", this->write_count_,
|
||||
cmd[5], len - 7);
|
||||
return false;
|
||||
}
|
||||
|
@ -48,6 +48,8 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer {
|
||||
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;
|
||||
|
@ -30,6 +30,8 @@ class SSD1322 : public PollingComponent, public display::DisplayBuffer {
|
||||
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_GRAYSCALE; }
|
||||
|
||||
protected:
|
||||
virtual void command(uint8_t value) = 0;
|
||||
virtual void data(uint8_t value) = 0;
|
||||
|
@ -35,6 +35,8 @@ class SSD1325 : public PollingComponent, public display::DisplayBuffer {
|
||||
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;
|
||||
|
@ -30,6 +30,8 @@ class SSD1327 : public PollingComponent, public display::DisplayBuffer {
|
||||
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_GRAYSCALE; }
|
||||
|
||||
protected:
|
||||
virtual void command(uint8_t value) = 0;
|
||||
virtual void write_display_data() = 0;
|
||||
|
@ -25,6 +25,8 @@ class SSD1331 : public PollingComponent, public display::DisplayBuffer {
|
||||
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_COLOR; }
|
||||
|
||||
protected:
|
||||
virtual void command(uint8_t value) = 0;
|
||||
virtual void write_display_data() = 0;
|
||||
|
@ -31,6 +31,8 @@ class SSD1351 : public PollingComponent, public display::DisplayBuffer {
|
||||
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_COLOR; }
|
||||
|
||||
protected:
|
||||
virtual void command(uint8_t value) = 0;
|
||||
virtual void data(uint8_t value) = 0;
|
||||
|
@ -53,6 +53,8 @@ class ST7735 : public PollingComponent,
|
||||
void set_dc_pin(GPIOPin *value) { dc_pin_ = value; }
|
||||
size_t get_buffer_length();
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
protected:
|
||||
void sendcommand_(uint8_t cmd, const uint8_t *data_bytes, uint8_t num_data_bytes);
|
||||
void senddata_(const uint8_t *data_bytes, uint8_t num_data_bytes);
|
||||
|
@ -126,6 +126,8 @@ class ST7789V : public PollingComponent,
|
||||
|
||||
void write_display_data();
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
protected:
|
||||
GPIOPin *dc_pin_;
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
|
@ -29,6 +29,8 @@ class ST7920 : public PollingComponent,
|
||||
void fill(Color color) override;
|
||||
void write_display_data();
|
||||
|
||||
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;
|
||||
int get_height_internal() override;
|
||||
|
@ -48,7 +48,7 @@ VARIABLE_PROG = re.compile(
|
||||
)
|
||||
|
||||
|
||||
def _expand_substitutions(substitutions, value, path):
|
||||
def _expand_substitutions(substitutions, value, path, ignore_missing):
|
||||
if "$" not in value:
|
||||
return value
|
||||
|
||||
@ -66,13 +66,14 @@ def _expand_substitutions(substitutions, value, path):
|
||||
if name.startswith("{") and name.endswith("}"):
|
||||
name = name[1:-1]
|
||||
if name not in substitutions:
|
||||
_LOGGER.warning(
|
||||
"Found '%s' (see %s) which looks like a substitution, but '%s' was "
|
||||
"not declared",
|
||||
orig_value,
|
||||
"->".join(str(x) for x in path),
|
||||
name,
|
||||
)
|
||||
if not ignore_missing:
|
||||
_LOGGER.warning(
|
||||
"Found '%s' (see %s) which looks like a substitution, but '%s' was "
|
||||
"not declared",
|
||||
orig_value,
|
||||
"->".join(str(x) for x in path),
|
||||
name,
|
||||
)
|
||||
i = j
|
||||
continue
|
||||
|
||||
@ -92,37 +93,37 @@ def _expand_substitutions(substitutions, value, path):
|
||||
return value
|
||||
|
||||
|
||||
def _substitute_item(substitutions, item, path):
|
||||
def _substitute_item(substitutions, item, path, ignore_missing):
|
||||
if isinstance(item, list):
|
||||
for i, it in enumerate(item):
|
||||
sub = _substitute_item(substitutions, it, path + [i])
|
||||
sub = _substitute_item(substitutions, it, path + [i], ignore_missing)
|
||||
if sub is not None:
|
||||
item[i] = sub
|
||||
elif isinstance(item, dict):
|
||||
replace_keys = []
|
||||
for k, v in item.items():
|
||||
if path or k != CONF_SUBSTITUTIONS:
|
||||
sub = _substitute_item(substitutions, k, path + [k])
|
||||
sub = _substitute_item(substitutions, k, path + [k], ignore_missing)
|
||||
if sub is not None:
|
||||
replace_keys.append((k, sub))
|
||||
sub = _substitute_item(substitutions, v, path + [k])
|
||||
sub = _substitute_item(substitutions, v, path + [k], ignore_missing)
|
||||
if sub is not None:
|
||||
item[k] = sub
|
||||
for old, new in replace_keys:
|
||||
item[new] = merge_config(item.get(old), item.get(new))
|
||||
del item[old]
|
||||
elif isinstance(item, str):
|
||||
sub = _expand_substitutions(substitutions, item, path)
|
||||
sub = _expand_substitutions(substitutions, item, path, ignore_missing)
|
||||
if sub != item:
|
||||
return sub
|
||||
elif isinstance(item, core.Lambda):
|
||||
sub = _expand_substitutions(substitutions, item.value, path)
|
||||
sub = _expand_substitutions(substitutions, item.value, path, ignore_missing)
|
||||
if sub != item:
|
||||
item.value = sub
|
||||
return None
|
||||
|
||||
|
||||
def do_substitution_pass(config, command_line_substitutions):
|
||||
def do_substitution_pass(config, command_line_substitutions, ignore_missing=False):
|
||||
if CONF_SUBSTITUTIONS not in config and not command_line_substitutions:
|
||||
return
|
||||
|
||||
@ -151,4 +152,4 @@ def do_substitution_pass(config, command_line_substitutions):
|
||||
config[CONF_SUBSTITUTIONS] = substitutions
|
||||
# Move substitutions to the first place to replace substitutions in them correctly
|
||||
config.move_to_end(CONF_SUBSTITUTIONS, False)
|
||||
_substitute_item(substitutions, config, [])
|
||||
_substitute_item(substitutions, config, [], ignore_missing)
|
||||
|
@ -43,7 +43,6 @@ void Switch::add_on_state_callback(std::function<void(bool)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
void Switch::set_inverted(bool inverted) { this->inverted_ = inverted; }
|
||||
uint32_t Switch::hash_base() { return 3129890955UL; }
|
||||
bool Switch::is_inverted() const { return this->inverted_; }
|
||||
|
||||
std::string Switch::get_device_class() {
|
||||
|
@ -107,8 +107,6 @@ class Switch : public EntityBase {
|
||||
*/
|
||||
virtual void write_state(bool state) = 0;
|
||||
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void(bool)> state_callback_{};
|
||||
bool inverted_{false};
|
||||
Deduplicator<bool> publish_dedup_;
|
||||
|
@ -70,7 +70,6 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) {
|
||||
|
||||
std::string TextSensor::unique_id() { return ""; }
|
||||
bool TextSensor::has_state() { return this->has_state_; }
|
||||
uint32_t TextSensor::hash_base() { return 334300109UL; }
|
||||
|
||||
} // namespace text_sensor
|
||||
} // namespace esphome
|
||||
|
@ -59,8 +59,6 @@ class TextSensor : public EntityBase {
|
||||
void internal_send_state_to_frontend(const std::string &state);
|
||||
|
||||
protected:
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void(std::string)> raw_callback_; ///< Storage for raw state callbacks.
|
||||
CallbackManager<void(std::string)> callback_; ///< Storage for filtered state callbacks.
|
||||
|
||||
|
@ -14,6 +14,7 @@ from esphome.const import (
|
||||
CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
|
||||
CONF_DRY_ACTION,
|
||||
CONF_DRY_MODE,
|
||||
CONF_FAN_MODE,
|
||||
CONF_FAN_MODE_ON_ACTION,
|
||||
CONF_FAN_MODE_OFF_ACTION,
|
||||
CONF_FAN_MODE_AUTO_ACTION,
|
||||
@ -37,6 +38,7 @@ from esphome.const import (
|
||||
CONF_IDLE_ACTION,
|
||||
CONF_MAX_COOLING_RUN_TIME,
|
||||
CONF_MAX_HEATING_RUN_TIME,
|
||||
CONF_MAX_TEMPERATURE,
|
||||
CONF_MIN_COOLING_OFF_TIME,
|
||||
CONF_MIN_COOLING_RUN_TIME,
|
||||
CONF_MIN_FAN_MODE_SWITCHING_TIME,
|
||||
@ -45,7 +47,11 @@ from esphome.const import (
|
||||
CONF_MIN_HEATING_OFF_TIME,
|
||||
CONF_MIN_HEATING_RUN_TIME,
|
||||
CONF_MIN_IDLE_TIME,
|
||||
CONF_MIN_TEMPERATURE,
|
||||
CONF_NAME,
|
||||
CONF_MODE,
|
||||
CONF_OFF_MODE,
|
||||
CONF_PRESET,
|
||||
CONF_SENSOR,
|
||||
CONF_SET_POINT_MINIMUM_DIFFERENTIAL,
|
||||
CONF_STARTUP_DELAY,
|
||||
@ -55,11 +61,15 @@ from esphome.const import (
|
||||
CONF_SUPPLEMENTAL_HEATING_DELTA,
|
||||
CONF_SWING_BOTH_ACTION,
|
||||
CONF_SWING_HORIZONTAL_ACTION,
|
||||
CONF_SWING_MODE,
|
||||
CONF_SWING_OFF_ACTION,
|
||||
CONF_SWING_VERTICAL_ACTION,
|
||||
CONF_TARGET_TEMPERATURE_CHANGE_ACTION,
|
||||
CONF_VISUAL,
|
||||
)
|
||||
|
||||
CONF_PRESET_CHANGE = "preset_change"
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
|
||||
climate_ns = cg.esphome_ns.namespace("climate")
|
||||
@ -82,6 +92,38 @@ CLIMATE_MODES = {
|
||||
}
|
||||
validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True)
|
||||
|
||||
ClimatePreset = climate_ns.enum("ClimatePreset")
|
||||
|
||||
PRESET_CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ThermostatClimateTargetTempConfig),
|
||||
cv.Required(CONF_NAME): cv.string_strict,
|
||||
cv.Optional(CONF_MODE): validate_climate_mode,
|
||||
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
|
||||
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
|
||||
cv.Optional(CONF_FAN_MODE): cv.templatable(climate.validate_climate_fan_mode),
|
||||
cv.Optional(CONF_SWING_MODE): cv.templatable(
|
||||
climate.validate_climate_swing_mode
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def validate_temperature_preset(preset, root_config, name, requirements):
|
||||
# verify temperature settings for the provided preset / default / away configuration
|
||||
for config_temp, req_actions in requirements.items():
|
||||
for req_action in req_actions:
|
||||
# verify corresponding default target temperature exists when a given climate action exists
|
||||
if config_temp not in preset and req_action in root_config:
|
||||
raise cv.Invalid(
|
||||
f"{config_temp} must be defined in {name} config when using {req_action}"
|
||||
)
|
||||
# if a given climate action is NOT defined, it should not have a default target temperature
|
||||
if config_temp in preset and req_action not in root_config:
|
||||
raise cv.Invalid(
|
||||
f"{config_temp} is defined in {name} config with no {req_action}"
|
||||
)
|
||||
|
||||
|
||||
def validate_thermostat(config):
|
||||
# verify corresponding action(s) exist(s) for any defined climate mode or action
|
||||
@ -235,33 +277,22 @@ def validate_thermostat(config):
|
||||
CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION],
|
||||
}
|
||||
|
||||
for config_temp, req_actions in requirements.items():
|
||||
for req_action in req_actions:
|
||||
# verify corresponding default target temperature exists when a given climate action exists
|
||||
if config_temp not in config and req_action in config:
|
||||
raise cv.Invalid(
|
||||
f"{config_temp} must be defined when using {req_action}"
|
||||
)
|
||||
# if a given climate action is NOT defined, it should not have a default target temperature
|
||||
if config_temp in config and req_action not in config:
|
||||
raise cv.Invalid(f"{config_temp} is defined with no {req_action}")
|
||||
# Validate temperature requirements for default configuraation
|
||||
validate_temperature_preset(config, config, "default", requirements)
|
||||
|
||||
# Validate temperature requirements for away configuration
|
||||
if CONF_AWAY_CONFIG in config:
|
||||
away = config[CONF_AWAY_CONFIG]
|
||||
for config_temp, req_actions in requirements.items():
|
||||
for req_action in req_actions:
|
||||
# verify corresponding default target temperature exists when a given climate action exists
|
||||
if config_temp not in away and req_action in config:
|
||||
raise cv.Invalid(
|
||||
f"{config_temp} must be defined in away configuration when using {req_action}"
|
||||
)
|
||||
# if a given climate action is NOT defined, it should not have a default target temperature
|
||||
if config_temp in away and req_action not in config:
|
||||
raise cv.Invalid(
|
||||
f"{config_temp} is defined in away configuration with no {req_action}"
|
||||
)
|
||||
validate_temperature_preset(away, config, "away", requirements)
|
||||
|
||||
# verify default climate mode is valid given above configuration
|
||||
# Validate temperature requirements for presets
|
||||
if CONF_PRESET in config:
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
validate_temperature_preset(
|
||||
preset_config, config, preset_config[CONF_NAME], requirements
|
||||
)
|
||||
|
||||
# Verify default climate mode is valid given above configuration
|
||||
default_mode = config[CONF_DEFAULT_MODE]
|
||||
requirements = {
|
||||
"HEAT_COOL": [CONF_COOL_ACTION, CONF_HEAT_ACTION],
|
||||
@ -270,13 +301,108 @@ def validate_thermostat(config):
|
||||
"DRY": [CONF_DRY_ACTION],
|
||||
"FAN_ONLY": [CONF_FAN_ONLY_ACTION],
|
||||
"AUTO": [CONF_COOL_ACTION, CONF_HEAT_ACTION],
|
||||
}.get(default_mode, [])
|
||||
for req in requirements:
|
||||
"OFF": [],
|
||||
}
|
||||
actions_for_default_mode = requirements.get(default_mode, [])
|
||||
for req in actions_for_default_mode:
|
||||
if req not in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_DEFAULT_MODE} is set to {default_mode} but {req} is not present in the configuration"
|
||||
)
|
||||
|
||||
# Verify that the modes for presets are valid given the configuration
|
||||
if CONF_PRESET in config:
|
||||
# Preset temperature vs Visual temperature validation
|
||||
|
||||
# Default visual configuration from climate_traits.h
|
||||
visual_min_temperature = 10.0
|
||||
visual_max_temperature = 30.0
|
||||
if CONF_VISUAL in config:
|
||||
visual_config = config[CONF_VISUAL]
|
||||
|
||||
if CONF_MIN_TEMPERATURE in visual_config:
|
||||
visual_min_temperature = visual_config[CONF_MIN_TEMPERATURE]
|
||||
|
||||
if CONF_MAX_TEMPERATURE in visual_config:
|
||||
visual_max_temperature = visual_config[CONF_MAX_TEMPERATURE]
|
||||
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in preset_config:
|
||||
preset_min_temperature = preset_config[
|
||||
CONF_DEFAULT_TARGET_TEMPERATURE_LOW
|
||||
]
|
||||
if preset_min_temperature < visual_min_temperature:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_DEFAULT_TARGET_TEMPERATURE_LOW} for {preset_config[CONF_NAME]} is set to {preset_min_temperature} which is less than the visual minimum temperature of {visual_min_temperature}"
|
||||
)
|
||||
|
||||
if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in preset_config:
|
||||
preset_max_temperature = preset_config[
|
||||
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH
|
||||
]
|
||||
if preset_max_temperature > visual_max_temperature:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_DEFAULT_TARGET_TEMPERATURE_HIGH} for {preset_config[CONF_NAME]} is set to {preset_max_temperature} which is more than the visual maximum temperature of {visual_max_temperature}"
|
||||
)
|
||||
|
||||
# Mode validation
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
if CONF_MODE not in preset_config:
|
||||
continue
|
||||
|
||||
mode = preset_config[CONF_MODE]
|
||||
|
||||
for req in requirements[mode]:
|
||||
if req not in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_MODE} is set to {mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration"
|
||||
)
|
||||
|
||||
# Fan mode requirements
|
||||
requirements = {
|
||||
"ON": [CONF_FAN_MODE_ON_ACTION],
|
||||
"OFF": [CONF_FAN_MODE_OFF_ACTION],
|
||||
"AUTO": [CONF_FAN_MODE_AUTO_ACTION],
|
||||
"LOW": [CONF_FAN_MODE_LOW_ACTION],
|
||||
"MEDIUM": [CONF_FAN_MODE_MEDIUM_ACTION],
|
||||
"HIGH": [CONF_FAN_MODE_HIGH_ACTION],
|
||||
"MIDDLE": [CONF_FAN_MODE_MIDDLE_ACTION],
|
||||
"FOCUS": [CONF_FAN_MODE_FOCUS_ACTION],
|
||||
"DIFFUSE": [CONF_FAN_MODE_DIFFUSE_ACTION],
|
||||
}
|
||||
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
if CONF_FAN_MODE not in preset_config:
|
||||
continue
|
||||
|
||||
fan_mode = preset_config[CONF_FAN_MODE]
|
||||
|
||||
for req in requirements[fan_mode]:
|
||||
if req not in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_FAN_MODE} is set to {fan_mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration"
|
||||
)
|
||||
|
||||
# Swing mode requirements
|
||||
requirements = {
|
||||
"OFF": [CONF_SWING_OFF_ACTION],
|
||||
"BOTH": [CONF_SWING_BOTH_ACTION],
|
||||
"VERTICAL": [CONF_SWING_VERTICAL_ACTION],
|
||||
"HORIZONTAL": [CONF_SWING_HORIZONTAL_ACTION],
|
||||
}
|
||||
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
if CONF_SWING_MODE not in preset_config:
|
||||
continue
|
||||
|
||||
swing_mode = preset_config[CONF_SWING_MODE]
|
||||
|
||||
for req in requirements[swing_mode]:
|
||||
if req not in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_SWING_MODE} is set to {swing_mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration"
|
||||
)
|
||||
|
||||
if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_COOLING}"
|
||||
@ -415,6 +541,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA),
|
||||
cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_at_least_one_key(
|
||||
@ -531,7 +661,7 @@ async def to_code(config):
|
||||
cg.add(var.set_supports_fan_with_heating(config[CONF_FAN_WITH_HEATING]))
|
||||
|
||||
cg.add(var.set_use_startup_delay(config[CONF_STARTUP_DELAY]))
|
||||
cg.add(var.set_normal_config(normal_config))
|
||||
cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_HOME, normal_config))
|
||||
|
||||
await automation.build_automation(
|
||||
var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION]
|
||||
@ -694,4 +824,55 @@ async def to_code(config):
|
||||
away_config = ThermostatClimateTargetTempConfig(
|
||||
away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW]
|
||||
)
|
||||
cg.add(var.set_away_config(away_config))
|
||||
cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_AWAY, away_config))
|
||||
|
||||
if CONF_PRESET in config:
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
|
||||
name = preset_config[CONF_NAME]
|
||||
standard_preset = None
|
||||
if name.upper() in climate.CLIMATE_PRESETS:
|
||||
standard_preset = climate.CLIMATE_PRESETS[name.upper()]
|
||||
|
||||
if two_points_available is True:
|
||||
preset_target_config = ThermostatClimateTargetTempConfig(
|
||||
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
|
||||
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH],
|
||||
)
|
||||
elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in preset_config:
|
||||
preset_target_config = ThermostatClimateTargetTempConfig(
|
||||
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH]
|
||||
)
|
||||
elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in preset_config:
|
||||
preset_target_config = ThermostatClimateTargetTempConfig(
|
||||
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW]
|
||||
)
|
||||
|
||||
preset_target_variable = cg.new_variable(
|
||||
preset_config[CONF_ID], preset_target_config
|
||||
)
|
||||
|
||||
if CONF_MODE in preset_config:
|
||||
cg.add(preset_target_variable.set_mode(preset_config[CONF_MODE]))
|
||||
|
||||
if CONF_FAN_MODE in preset_config:
|
||||
cg.add(
|
||||
preset_target_variable.set_fan_mode(preset_config[CONF_FAN_MODE])
|
||||
)
|
||||
|
||||
if CONF_SWING_MODE in preset_config:
|
||||
cg.add(
|
||||
preset_target_variable.set_swing_mode(
|
||||
preset_config[CONF_SWING_MODE]
|
||||
)
|
||||
)
|
||||
|
||||
if standard_preset is not None:
|
||||
cg.add(var.set_preset_config(standard_preset, preset_target_variable))
|
||||
else:
|
||||
cg.add(var.set_custom_preset_config(name, preset_target_variable))
|
||||
|
||||
if CONF_PRESET_CHANGE in config:
|
||||
await automation.build_automation(
|
||||
var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE]
|
||||
)
|
||||
|
@ -32,7 +32,7 @@ void ThermostatClimate::setup() {
|
||||
} else {
|
||||
// restore from defaults, change_away handles temps for us
|
||||
this->mode = this->default_mode_;
|
||||
this->change_away_(false);
|
||||
this->change_preset_(climate::CLIMATE_PRESET_HOME);
|
||||
}
|
||||
// refresh the climate action based on the restored settings, we'll publish_state() later
|
||||
this->switch_to_action_(this->compute_action_(), false);
|
||||
@ -162,11 +162,20 @@ void ThermostatClimate::control(const climate::ClimateCall &call) {
|
||||
if (call.get_preset().has_value()) {
|
||||
// setup_complete_ blocks modifying/resetting the temps immediately after boot
|
||||
if (this->setup_complete_) {
|
||||
this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
|
||||
this->change_preset_(*call.get_preset());
|
||||
} else {
|
||||
this->preset = *call.get_preset();
|
||||
}
|
||||
}
|
||||
if (call.get_custom_preset().has_value()) {
|
||||
// setup_complete_ blocks modifying/resetting the temps immediately after boot
|
||||
if (this->setup_complete_) {
|
||||
this->change_custom_preset_(*call.get_custom_preset());
|
||||
} else {
|
||||
this->custom_preset = *call.get_custom_preset();
|
||||
}
|
||||
}
|
||||
|
||||
if (call.get_mode().has_value())
|
||||
this->mode = *call.get_mode();
|
||||
if (call.get_fan_mode().has_value())
|
||||
@ -236,8 +245,12 @@ climate::ClimateTraits ThermostatClimate::traits() {
|
||||
if (supports_swing_mode_vertical_)
|
||||
traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL);
|
||||
|
||||
if (supports_away_)
|
||||
traits.set_supported_presets({climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_AWAY});
|
||||
for (auto &it : this->preset_config_) {
|
||||
traits.add_supported_preset(it.first);
|
||||
}
|
||||
for (auto &it : this->custom_preset_config_) {
|
||||
traits.add_supported_custom_preset(it.first);
|
||||
}
|
||||
|
||||
traits.set_supports_two_point_target_temperature(this->supports_two_points_);
|
||||
traits.set_supports_action(true);
|
||||
@ -910,30 +923,112 @@ bool ThermostatClimate::supplemental_heating_required_() {
|
||||
(this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING));
|
||||
}
|
||||
|
||||
void ThermostatClimate::change_away_(bool away) {
|
||||
if (!away) {
|
||||
void ThermostatClimate::dump_preset_config_(const std::string &preset,
|
||||
const ThermostatClimateTargetTempConfig &config) {
|
||||
const auto *preset_name = preset.c_str();
|
||||
|
||||
if (this->supports_heat_) {
|
||||
if (this->supports_two_points_) {
|
||||
this->target_temperature_low = this->normal_config_.default_temperature_low;
|
||||
this->target_temperature_high = this->normal_config_.default_temperature_high;
|
||||
} else
|
||||
this->target_temperature = this->normal_config_.default_temperature;
|
||||
} else {
|
||||
if (this->supports_two_points_) {
|
||||
this->target_temperature_low = this->away_config_.default_temperature_low;
|
||||
this->target_temperature_high = this->away_config_.default_temperature_high;
|
||||
} else
|
||||
this->target_temperature = this->away_config_.default_temperature;
|
||||
ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name,
|
||||
config.default_temperature_low);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name, config.default_temperature);
|
||||
}
|
||||
}
|
||||
if ((this->supports_cool_) || (this->supports_fan_only_)) {
|
||||
if (this->supports_two_points_) {
|
||||
ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name,
|
||||
config.default_temperature_high);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name, config.default_temperature);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.mode_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " %s Default Mode: %s", preset_name,
|
||||
LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_)));
|
||||
}
|
||||
if (config.fan_mode_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " %s Default Fan Mode: %s", preset_name,
|
||||
LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_)));
|
||||
}
|
||||
if (config.swing_mode_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " %s Default Swing Mode: %s", preset_name,
|
||||
LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_)));
|
||||
}
|
||||
this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
|
||||
}
|
||||
|
||||
void ThermostatClimate::set_normal_config(const ThermostatClimateTargetTempConfig &normal_config) {
|
||||
this->normal_config_ = normal_config;
|
||||
void ThermostatClimate::change_preset_(climate::ClimatePreset preset) {
|
||||
auto config = this->preset_config_.find(preset);
|
||||
|
||||
if (config != this->preset_config_.end()) {
|
||||
ESP_LOGI(TAG, "Switching to preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
|
||||
this->change_preset_internal_(config->second);
|
||||
|
||||
this->custom_preset.reset();
|
||||
this->preset = preset;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Preset %s is not configured, ignoring.", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
|
||||
}
|
||||
}
|
||||
|
||||
void ThermostatClimate::set_away_config(const ThermostatClimateTargetTempConfig &away_config) {
|
||||
this->supports_away_ = true;
|
||||
this->away_config_ = away_config;
|
||||
void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) {
|
||||
auto config = this->custom_preset_config_.find(custom_preset);
|
||||
|
||||
if (config != this->custom_preset_config_.end()) {
|
||||
ESP_LOGI(TAG, "Switching to custom preset %s", custom_preset.c_str());
|
||||
this->change_preset_internal_(config->second);
|
||||
|
||||
this->preset.reset();
|
||||
this->custom_preset = custom_preset;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Custom Preset %s is not configured, ignoring.", custom_preset.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTempConfig &config) {
|
||||
if (this->supports_two_points_) {
|
||||
this->target_temperature_low = config.default_temperature_low;
|
||||
this->target_temperature_high = config.default_temperature_high;
|
||||
} else {
|
||||
this->target_temperature = config.default_temperature;
|
||||
}
|
||||
|
||||
// Note: The mode, fan_mode, and swing_mode can all be defined on the preset but if the climate.control call
|
||||
// also specifies them then the control's version will override these for that call
|
||||
if (config.mode_.has_value()) {
|
||||
this->mode = *config.mode_;
|
||||
ESP_LOGV(TAG, "Setting mode to %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_)));
|
||||
}
|
||||
|
||||
if (config.fan_mode_.has_value()) {
|
||||
this->fan_mode = *config.fan_mode_;
|
||||
ESP_LOGV(TAG, "Setting fan mode to %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_)));
|
||||
}
|
||||
|
||||
if (config.swing_mode_.has_value()) {
|
||||
ESP_LOGV(TAG, "Setting swing mode to %s", LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_)));
|
||||
this->swing_mode = *config.swing_mode_;
|
||||
}
|
||||
|
||||
// Fire any preset changed trigger if defined
|
||||
if (this->preset != preset) {
|
||||
Trigger<> *trig = this->preset_change_trigger_;
|
||||
assert(trig != nullptr);
|
||||
trig->trigger();
|
||||
}
|
||||
|
||||
this->refresh();
|
||||
}
|
||||
|
||||
void ThermostatClimate::set_preset_config(climate::ClimatePreset preset,
|
||||
const ThermostatClimateTargetTempConfig &config) {
|
||||
this->preset_config_[preset] = config;
|
||||
}
|
||||
|
||||
void ThermostatClimate::set_custom_preset_config(const std::string &name,
|
||||
const ThermostatClimateTargetTempConfig &config) {
|
||||
this->custom_preset_config_[name] = config;
|
||||
}
|
||||
|
||||
ThermostatClimate::ThermostatClimate()
|
||||
@ -963,7 +1058,8 @@ ThermostatClimate::ThermostatClimate()
|
||||
swing_mode_off_trigger_(new Trigger<>()),
|
||||
swing_mode_horizontal_trigger_(new Trigger<>()),
|
||||
swing_mode_vertical_trigger_(new Trigger<>()),
|
||||
temperature_change_trigger_(new Trigger<>()) {}
|
||||
temperature_change_trigger_(new Trigger<>()),
|
||||
preset_change_trigger_(new Trigger<>()) {}
|
||||
|
||||
void ThermostatClimate::set_default_mode(climate::ClimateMode default_mode) { this->default_mode_ = default_mode; }
|
||||
void ThermostatClimate::set_set_point_minimum_differential(float differential) {
|
||||
@ -1112,23 +1208,11 @@ Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->
|
||||
Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; }
|
||||
Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; }
|
||||
Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; }
|
||||
Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->preset_change_trigger_; }
|
||||
|
||||
void ThermostatClimate::dump_config() {
|
||||
LOG_CLIMATE("", "Thermostat", this);
|
||||
if (this->supports_heat_) {
|
||||
if (this->supports_two_points_) {
|
||||
ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature);
|
||||
}
|
||||
}
|
||||
if ((this->supports_cool_) || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) {
|
||||
if (this->supports_two_points_) {
|
||||
ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->supports_two_points_)
|
||||
ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_);
|
||||
ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_));
|
||||
@ -1194,24 +1278,21 @@ void ThermostatClimate::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_));
|
||||
ESP_LOGCONFIG(TAG, " Supports SWING MODE VERTICAL: %s", YESNO(this->supports_swing_mode_vertical_));
|
||||
ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_));
|
||||
ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_));
|
||||
if (this->supports_away_) {
|
||||
if (this->supports_heat_) {
|
||||
if (this->supports_two_points_) {
|
||||
ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C",
|
||||
this->away_config_.default_temperature_low);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature);
|
||||
}
|
||||
}
|
||||
if ((this->supports_cool_) || (this->supports_fan_only_)) {
|
||||
if (this->supports_two_points_) {
|
||||
ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C",
|
||||
this->away_config_.default_temperature_high);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", this->away_config_.default_temperature);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Supported PRESETS: ");
|
||||
for (auto &it : this->preset_config_) {
|
||||
const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first));
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true));
|
||||
this->dump_preset_config_(preset_name, it.second);
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS: ");
|
||||
for (auto &it : this->custom_preset_config_) {
|
||||
const auto *preset_name = it.first.c_str();
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true));
|
||||
this->dump_preset_config_(preset_name, it.second);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include <map>
|
||||
|
||||
namespace esphome {
|
||||
namespace thermostat {
|
||||
@ -34,6 +35,10 @@ struct ThermostatClimateTargetTempConfig {
|
||||
ThermostatClimateTargetTempConfig(float default_temperature);
|
||||
ThermostatClimateTargetTempConfig(float default_temperature_low, float default_temperature_high);
|
||||
|
||||
void set_fan_mode(climate::ClimateFanMode fan_mode) { this->fan_mode_ = fan_mode; }
|
||||
void set_swing_mode(climate::ClimateSwingMode swing_mode) { this->swing_mode_ = swing_mode; }
|
||||
void set_mode(climate::ClimateMode mode) { this->mode_ = mode; }
|
||||
|
||||
float default_temperature{NAN};
|
||||
float default_temperature_low{NAN};
|
||||
float default_temperature_high{NAN};
|
||||
@ -41,6 +46,9 @@ struct ThermostatClimateTargetTempConfig {
|
||||
float cool_overrun_{NAN};
|
||||
float heat_deadband_{NAN};
|
||||
float heat_overrun_{NAN};
|
||||
optional<climate::ClimateFanMode> fan_mode_{};
|
||||
optional<climate::ClimateSwingMode> swing_mode_{};
|
||||
optional<climate::ClimateMode> mode_{};
|
||||
};
|
||||
|
||||
class ThermostatClimate : public climate::Climate, public Component {
|
||||
@ -94,8 +102,8 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical);
|
||||
void set_supports_two_points(bool supports_two_points);
|
||||
|
||||
void set_normal_config(const ThermostatClimateTargetTempConfig &normal_config);
|
||||
void set_away_config(const ThermostatClimateTargetTempConfig &away_config);
|
||||
void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config);
|
||||
void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config);
|
||||
|
||||
Trigger<> *get_cool_action_trigger() const;
|
||||
Trigger<> *get_supplemental_cool_action_trigger() const;
|
||||
@ -124,6 +132,7 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
Trigger<> *get_swing_mode_off_trigger() const;
|
||||
Trigger<> *get_swing_mode_vertical_trigger() const;
|
||||
Trigger<> *get_temperature_change_trigger() const;
|
||||
Trigger<> *get_preset_change_trigger() const;
|
||||
/// Get current hysteresis values
|
||||
float cool_deadband();
|
||||
float cool_overrun();
|
||||
@ -149,8 +158,14 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// Override control to change settings of the climate device.
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
|
||||
/// Change the away setting, will reset target temperatures to defaults.
|
||||
void change_away_(bool away);
|
||||
/// Change to a provided preset setting; will reset temperature, mode, fan, and swing modes accordingly
|
||||
void change_preset_(climate::ClimatePreset preset);
|
||||
/// Change to a provided custom preset setting; will reset temperature, mode, fan, and swing modes accordingly
|
||||
void change_custom_preset_(const std::string &custom_preset);
|
||||
|
||||
/// Applies the temperature, mode, fan, and swing modes of the provided config.
|
||||
/// This is agnostic of custom vs built in preset
|
||||
void change_preset_internal_(const ThermostatClimateTargetTempConfig &config);
|
||||
|
||||
/// Return the traits of this controller.
|
||||
climate::ClimateTraits traits() override;
|
||||
@ -210,6 +225,8 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
bool supplemental_cooling_required_();
|
||||
bool supplemental_heating_required_();
|
||||
|
||||
void dump_preset_config_(const std::string &preset_name, const ThermostatClimateTargetTempConfig &config);
|
||||
|
||||
/// The sensor used for getting the current temperature
|
||||
sensor::Sensor *sensor_{nullptr};
|
||||
|
||||
@ -267,11 +284,6 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// A false value means that the controller has no such support.
|
||||
bool supports_two_points_{false};
|
||||
|
||||
/// Whether the controller supports an "away" mode
|
||||
///
|
||||
/// A false value means that the controller has no such mode.
|
||||
bool supports_away_{false};
|
||||
|
||||
/// Flags indicating if maximum allowable run time was exceeded
|
||||
bool cooling_max_runtime_exceeded_{false};
|
||||
bool heating_max_runtime_exceeded_{false};
|
||||
@ -368,6 +380,9 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// The trigger to call when the target temperature(s) change(es).
|
||||
Trigger<> *temperature_change_trigger_{nullptr};
|
||||
|
||||
/// The triggr to call when the preset mode changes
|
||||
Trigger<> *preset_change_trigger_{nullptr};
|
||||
|
||||
/// A reference to the trigger that was previously active.
|
||||
///
|
||||
/// This is so that the previous trigger can be stopped before enabling a new one
|
||||
@ -409,10 +424,6 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// Minimum allowable duration in seconds for action timers
|
||||
const uint8_t min_timer_duration_{1};
|
||||
|
||||
/// Temperature data for normal/home and away modes
|
||||
ThermostatClimateTargetTempConfig normal_config_{};
|
||||
ThermostatClimateTargetTempConfig away_config_{};
|
||||
|
||||
/// Climate action timers
|
||||
std::vector<ThermostatClimateTimer> timer_{
|
||||
{"cool_run", false, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)},
|
||||
@ -425,6 +436,11 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
{"heat_off", false, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)},
|
||||
{"heat_on", false, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)},
|
||||
{"idle_on", false, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}};
|
||||
|
||||
/// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc)
|
||||
std::map<climate::ClimatePreset, ThermostatClimateTargetTempConfig> preset_config_{};
|
||||
/// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset")
|
||||
std::map<std::string, ThermostatClimateTargetTempConfig> custom_preset_config_{};
|
||||
};
|
||||
|
||||
} // namespace thermostat
|
||||
|
@ -126,10 +126,10 @@ def _parse_cron_part(part, min_value, max_value, special_mapping):
|
||||
)
|
||||
begin, end = data
|
||||
begin_n = _parse_cron_int(
|
||||
begin, special_mapping, "Number for time range must be integer, " "got {}"
|
||||
begin, special_mapping, "Number for time range must be integer, got {}"
|
||||
)
|
||||
end_n = _parse_cron_int(
|
||||
end, special_mapping, "Number for time range must be integer, " "got {}"
|
||||
end, special_mapping, "Number for time range must be integer, got {}"
|
||||
)
|
||||
if end_n < begin_n:
|
||||
return set(range(end_n, max_value + 1)) | set(range(min_value, begin_n + 1))
|
||||
@ -139,7 +139,7 @@ def _parse_cron_part(part, min_value, max_value, special_mapping):
|
||||
_parse_cron_int(
|
||||
part,
|
||||
special_mapping,
|
||||
"Number for time expression must be an " "integer, got {}",
|
||||
"Number for time expression must be an integer, got {}",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,6 @@ from esphome.core.entity_helpers import inherit_property_from
|
||||
DEPENDENCIES = ["time"]
|
||||
|
||||
CONF_POWER_ID = "power_id"
|
||||
CONF_MIN_SAVE_INTERVAL = "min_save_interval"
|
||||
total_daily_energy_ns = cg.esphome_ns.namespace("total_daily_energy")
|
||||
TotalDailyEnergyMethod = total_daily_energy_ns.enum("TotalDailyEnergyMethod")
|
||||
TOTAL_DAILY_ENERGY_METHODS = {
|
||||
@ -49,9 +48,9 @@ CONFIG_SCHEMA = (
|
||||
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor),
|
||||
cv.Optional(CONF_RESTORE, default=True): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_MIN_SAVE_INTERVAL, default="0s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional("min_save_interval"): cv.invalid(
|
||||
"`min_save_interval` was removed in 2022.6.0. Please use the `preferences` -> `flash_write_interval` to adjust."
|
||||
),
|
||||
cv.Optional(CONF_METHOD, default="right"): cv.enum(
|
||||
TOTAL_DAILY_ENERGY_METHODS, lower=True
|
||||
),
|
||||
@ -90,5 +89,4 @@ async def to_code(config):
|
||||
time_ = await cg.get_variable(config[CONF_TIME_ID])
|
||||
cg.add(var.set_time(time_))
|
||||
cg.add(var.set_restore(config[CONF_RESTORE]))
|
||||
cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL]))
|
||||
cg.add(var.set_method(config[CONF_METHOD]))
|
||||
|
@ -16,7 +16,6 @@ void TotalDailyEnergy::setup() {
|
||||
this->publish_state_and_save(initial_value);
|
||||
|
||||
this->last_update_ = millis();
|
||||
this->last_save_ = this->last_update_;
|
||||
|
||||
this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); });
|
||||
}
|
||||
@ -43,12 +42,9 @@ void TotalDailyEnergy::loop() {
|
||||
void TotalDailyEnergy::publish_state_and_save(float state) {
|
||||
this->total_energy_ = state;
|
||||
this->publish_state(state);
|
||||
const uint32_t now = millis();
|
||||
if (now - this->last_save_ < this->min_save_interval_) {
|
||||
return;
|
||||
if (this->restore_) {
|
||||
this->pref_.save(&state);
|
||||
}
|
||||
this->last_save_ = now;
|
||||
this->pref_.save(&state);
|
||||
}
|
||||
|
||||
void TotalDailyEnergy::process_new_state_(float state) {
|
||||
|
@ -18,7 +18,6 @@ enum TotalDailyEnergyMethod {
|
||||
class TotalDailyEnergy : public sensor::Sensor, public Component {
|
||||
public:
|
||||
void set_restore(bool restore) { restore_ = restore; }
|
||||
void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; }
|
||||
void set_time(time::RealTimeClock *time) { time_ = time; }
|
||||
void set_parent(Sensor *parent) { parent_ = parent; }
|
||||
void set_method(TotalDailyEnergyMethod method) { method_ = method; }
|
||||
@ -39,7 +38,6 @@ class TotalDailyEnergy : public sensor::Sensor, public Component {
|
||||
uint16_t last_day_of_year_{};
|
||||
uint32_t last_update_{0};
|
||||
uint32_t last_save_{0};
|
||||
uint32_t min_save_interval_{0};
|
||||
bool restore_;
|
||||
float total_energy_{0.0f};
|
||||
float last_power_state_{0.0f};
|
||||
|
@ -10,6 +10,11 @@ static const char *const TAG = "tuya.light";
|
||||
void TuyaLight::setup() {
|
||||
if (this->color_temperature_id_.has_value()) {
|
||||
this->parent_->register_listener(*this->color_temperature_id_, [this](const TuyaDatapoint &datapoint) {
|
||||
if (this->state_->current_values != this->state_->remote_values) {
|
||||
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
auto datapoint_value = datapoint.value_uint;
|
||||
if (this->color_temperature_invert_) {
|
||||
datapoint_value = this->color_temperature_max_value_ - datapoint_value;
|
||||
@ -23,6 +28,11 @@ void TuyaLight::setup() {
|
||||
}
|
||||
if (this->dimmer_id_.has_value()) {
|
||||
this->parent_->register_listener(*this->dimmer_id_, [this](const TuyaDatapoint &datapoint) {
|
||||
if (this->state_->current_values != this->state_->remote_values) {
|
||||
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
auto call = this->state_->make_call();
|
||||
call.set_brightness(float(datapoint.value_uint) / this->max_value_);
|
||||
call.perform();
|
||||
@ -30,6 +40,11 @@ void TuyaLight::setup() {
|
||||
}
|
||||
if (switch_id_.has_value()) {
|
||||
this->parent_->register_listener(*this->switch_id_, [this](const TuyaDatapoint &datapoint) {
|
||||
if (this->state_->current_values != this->state_->remote_values) {
|
||||
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
auto call = this->state_->make_call();
|
||||
call.set_state(datapoint.value_bool);
|
||||
call.perform();
|
||||
@ -41,6 +56,11 @@ void TuyaLight::setup() {
|
||||
auto green = parse_hex<uint8_t>(datapoint.value_string.substr(2, 2));
|
||||
auto blue = parse_hex<uint8_t>(datapoint.value_string.substr(4, 2));
|
||||
if (red.has_value() && green.has_value() && blue.has_value()) {
|
||||
if (this->state_->current_values != this->state_->remote_values) {
|
||||
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
auto call = this->state_->make_call();
|
||||
call.set_rgb(float(*red) / 255, float(*green) / 255, float(*blue) / 255);
|
||||
call.perform();
|
||||
@ -52,6 +72,11 @@ void TuyaLight::setup() {
|
||||
auto saturation = parse_hex<uint16_t>(datapoint.value_string.substr(4, 4));
|
||||
auto value = parse_hex<uint16_t>(datapoint.value_string.substr(8, 4));
|
||||
if (hue.has_value() && saturation.has_value() && value.has_value()) {
|
||||
if (this->state_->current_values != this->state_->remote_values) {
|
||||
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
float red, green, blue;
|
||||
hsv_to_rgb(*hue, float(*saturation) / 1000, float(*value) / 1000, red, green, blue);
|
||||
auto call = this->state_->make_call();
|
||||
|
@ -1,7 +1,7 @@
|
||||
#include "tuya.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
|
||||
|
@ -38,7 +38,7 @@ class UARTDebugger : public Component, public Trigger<UARTDirection, std::vector
|
||||
|
||||
/// Add a delimiter byte. This can be called multiple times to setup a
|
||||
/// multi-byte delimiter (a typical example would be '\r\n').
|
||||
/// When the constructued byte sequence is found in the data stream,
|
||||
/// When the constructed byte sequence is found in the data stream,
|
||||
/// logging will be triggered.
|
||||
void add_delimiter_byte(uint8_t byte) { this->after_delimiter_.push_back(byte); }
|
||||
|
||||
|
@ -257,6 +257,7 @@ void VL53L0XSensor::setup() {
|
||||
|
||||
ESP_LOGD(TAG, "'%s' - setup END", this->name_.c_str());
|
||||
}
|
||||
|
||||
void VL53L0XSensor::update() {
|
||||
if (this->initiated_read_ || this->waiting_for_interrupt_) {
|
||||
this->publish_state(NAN);
|
||||
@ -280,6 +281,7 @@ void VL53L0XSensor::update() {
|
||||
this->initiated_read_ = true;
|
||||
// wait for timeout
|
||||
}
|
||||
|
||||
void VL53L0XSensor::loop() {
|
||||
if (this->initiated_read_) {
|
||||
if (reg(0x00).get() & 0x01) {
|
||||
@ -311,5 +313,222 @@ void VL53L0XSensor::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t VL53L0XSensor::get_measurement_timing_budget_() {
|
||||
SequenceStepEnables enables{};
|
||||
SequenceStepTimeouts timeouts{};
|
||||
|
||||
uint16_t start_overhead = 1910;
|
||||
uint16_t end_overhead = 960;
|
||||
uint16_t msrc_overhead = 660;
|
||||
uint16_t tcc_overhead = 590;
|
||||
uint16_t dss_overhead = 690;
|
||||
uint16_t pre_range_overhead = 660;
|
||||
uint16_t final_range_overhead = 550;
|
||||
|
||||
// "Start and end overhead times always present"
|
||||
uint32_t budget_us = start_overhead + end_overhead;
|
||||
|
||||
get_sequence_step_enables_(&enables);
|
||||
get_sequence_step_timeouts_(&enables, &timeouts);
|
||||
|
||||
if (enables.tcc)
|
||||
budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead);
|
||||
|
||||
if (enables.dss) {
|
||||
budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead);
|
||||
} else if (enables.msrc) {
|
||||
budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead);
|
||||
}
|
||||
|
||||
if (enables.pre_range)
|
||||
budget_us += (timeouts.pre_range_us + pre_range_overhead);
|
||||
|
||||
if (enables.final_range)
|
||||
budget_us += (timeouts.final_range_us + final_range_overhead);
|
||||
|
||||
measurement_timing_budget_us_ = budget_us; // store for internal reuse
|
||||
return budget_us;
|
||||
}
|
||||
|
||||
bool VL53L0XSensor::set_measurement_timing_budget_(uint32_t budget_us) {
|
||||
SequenceStepEnables enables{};
|
||||
SequenceStepTimeouts timeouts{};
|
||||
|
||||
uint16_t start_overhead = 1320; // note that this is different than the value in get_
|
||||
uint16_t end_overhead = 960;
|
||||
uint16_t msrc_overhead = 660;
|
||||
uint16_t tcc_overhead = 590;
|
||||
uint16_t dss_overhead = 690;
|
||||
uint16_t pre_range_overhead = 660;
|
||||
uint16_t final_range_overhead = 550;
|
||||
|
||||
uint32_t min_timing_budget = 20000;
|
||||
|
||||
if (budget_us < min_timing_budget) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t used_budget_us = start_overhead + end_overhead;
|
||||
|
||||
get_sequence_step_enables_(&enables);
|
||||
get_sequence_step_timeouts_(&enables, &timeouts);
|
||||
|
||||
if (enables.tcc) {
|
||||
used_budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead);
|
||||
}
|
||||
|
||||
if (enables.dss) {
|
||||
used_budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead);
|
||||
} else if (enables.msrc) {
|
||||
used_budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead);
|
||||
}
|
||||
|
||||
if (enables.pre_range) {
|
||||
used_budget_us += (timeouts.pre_range_us + pre_range_overhead);
|
||||
}
|
||||
|
||||
if (enables.final_range) {
|
||||
used_budget_us += final_range_overhead;
|
||||
|
||||
// "Note that the final range timeout is determined by the timing
|
||||
// budget and the sum of all other timeouts within the sequence.
|
||||
// If there is no room for the final range timeout, then an error
|
||||
// will be set. Otherwise the remaining time will be applied to
|
||||
// the final range."
|
||||
|
||||
if (used_budget_us > budget_us) {
|
||||
// "Requested timeout too big."
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t final_range_timeout_us = budget_us - used_budget_us;
|
||||
|
||||
// set_sequence_step_timeout() begin
|
||||
// (SequenceStepId == VL53L0X_SEQUENCESTEP_FINAL_RANGE)
|
||||
|
||||
// "For the final range timeout, the pre-range timeout
|
||||
// must be added. To do this both final and pre-range
|
||||
// timeouts must be expressed in macro periods MClks
|
||||
// because they have different vcsel periods."
|
||||
|
||||
uint16_t final_range_timeout_mclks =
|
||||
timeout_microseconds_to_mclks_(final_range_timeout_us, timeouts.final_range_vcsel_period_pclks);
|
||||
|
||||
if (enables.pre_range) {
|
||||
final_range_timeout_mclks += timeouts.pre_range_mclks;
|
||||
}
|
||||
|
||||
write_byte_16(0x71, encode_timeout_(final_range_timeout_mclks));
|
||||
|
||||
// set_sequence_step_timeout() end
|
||||
|
||||
measurement_timing_budget_us_ = budget_us; // store for internal reuse
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void VL53L0XSensor::get_sequence_step_enables_(SequenceStepEnables *enables) {
|
||||
uint8_t sequence_config = reg(0x01).get();
|
||||
enables->tcc = (sequence_config >> 4) & 0x1;
|
||||
enables->dss = (sequence_config >> 3) & 0x1;
|
||||
enables->msrc = (sequence_config >> 2) & 0x1;
|
||||
enables->pre_range = (sequence_config >> 6) & 0x1;
|
||||
enables->final_range = (sequence_config >> 7) & 0x1;
|
||||
}
|
||||
|
||||
void VL53L0XSensor::get_sequence_step_timeouts_(SequenceStepEnables const *enables, SequenceStepTimeouts *timeouts) {
|
||||
timeouts->pre_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_PRE_RANGE);
|
||||
|
||||
timeouts->msrc_dss_tcc_mclks = reg(0x46).get() + 1;
|
||||
timeouts->msrc_dss_tcc_us =
|
||||
timeout_mclks_to_microseconds_(timeouts->msrc_dss_tcc_mclks, timeouts->pre_range_vcsel_period_pclks);
|
||||
|
||||
uint16_t value;
|
||||
read_byte_16(0x51, &value);
|
||||
timeouts->pre_range_mclks = decode_timeout_(value);
|
||||
timeouts->pre_range_us =
|
||||
timeout_mclks_to_microseconds_(timeouts->pre_range_mclks, timeouts->pre_range_vcsel_period_pclks);
|
||||
|
||||
timeouts->final_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_FINAL_RANGE);
|
||||
|
||||
read_byte_16(0x71, &value);
|
||||
timeouts->final_range_mclks = decode_timeout_(value);
|
||||
|
||||
if (enables->pre_range) {
|
||||
timeouts->final_range_mclks -= timeouts->pre_range_mclks;
|
||||
}
|
||||
|
||||
timeouts->final_range_us =
|
||||
timeout_mclks_to_microseconds_(timeouts->final_range_mclks, timeouts->final_range_vcsel_period_pclks);
|
||||
}
|
||||
|
||||
uint8_t VL53L0XSensor::get_vcsel_pulse_period_(VcselPeriodType type) {
|
||||
uint8_t vcsel;
|
||||
if (type == VCSEL_PERIOD_PRE_RANGE) {
|
||||
vcsel = reg(0x50).get();
|
||||
} else if (type == VCSEL_PERIOD_FINAL_RANGE) {
|
||||
vcsel = reg(0x70).get();
|
||||
} else {
|
||||
return 255;
|
||||
}
|
||||
|
||||
return (vcsel + 1) << 1;
|
||||
}
|
||||
|
||||
uint32_t VL53L0XSensor::get_macro_period_(uint8_t vcsel_period_pclks) {
|
||||
return ((2304UL * vcsel_period_pclks * 1655UL) + 500UL) / 1000UL;
|
||||
}
|
||||
|
||||
uint32_t VL53L0XSensor::timeout_mclks_to_microseconds_(uint16_t timeout_period_mclks, uint8_t vcsel_period_pclks) {
|
||||
uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks);
|
||||
return ((timeout_period_mclks * macro_period_ns) + (macro_period_ns / 2)) / 1000;
|
||||
}
|
||||
|
||||
uint32_t VL53L0XSensor::timeout_microseconds_to_mclks_(uint32_t timeout_period_us, uint8_t vcsel_period_pclks) {
|
||||
uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks);
|
||||
return (((timeout_period_us * 1000) + (macro_period_ns / 2)) / macro_period_ns);
|
||||
}
|
||||
|
||||
uint16_t VL53L0XSensor::decode_timeout_(uint16_t reg_val) {
|
||||
// format: "(LSByte * 2^MSByte) + 1"
|
||||
uint8_t msb = (reg_val >> 8) & 0xFF;
|
||||
uint8_t lsb = (reg_val >> 0) & 0xFF;
|
||||
return (uint16_t(lsb) << msb) + 1;
|
||||
}
|
||||
|
||||
uint16_t VL53L0XSensor::encode_timeout_(uint16_t timeout_mclks) {
|
||||
// format: "(LSByte * 2^MSByte) + 1"
|
||||
uint32_t ls_byte = 0;
|
||||
uint16_t ms_byte = 0;
|
||||
|
||||
if (timeout_mclks <= 0)
|
||||
return 0;
|
||||
|
||||
ls_byte = timeout_mclks - 1;
|
||||
|
||||
while ((ls_byte & 0xFFFFFF00) > 0) {
|
||||
ls_byte >>= 1;
|
||||
ms_byte++;
|
||||
}
|
||||
|
||||
return (ms_byte << 8) | (ls_byte & 0xFF);
|
||||
}
|
||||
|
||||
bool VL53L0XSensor::perform_single_ref_calibration_(uint8_t vhv_init_byte) {
|
||||
reg(0x00) = 0x01 | vhv_init_byte; // VL53L0X_REG_SYSRANGE_MODE_START_STOP
|
||||
|
||||
uint32_t start = millis();
|
||||
while ((reg(0x13).get() & 0x07) == 0) {
|
||||
if (millis() - start > 1000)
|
||||
return false;
|
||||
yield();
|
||||
}
|
||||
|
||||
reg(0x0B) = 0x01;
|
||||
reg(0x00) = 0x00;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace vl53l0x
|
||||
} // namespace esphome
|
||||
|
@ -21,6 +21,8 @@ struct SequenceStepTimeouts {
|
||||
uint32_t msrc_dss_tcc_us, pre_range_us, final_range_us;
|
||||
};
|
||||
|
||||
enum VcselPeriodType { VCSEL_PERIOD_PRE_RANGE, VCSEL_PERIOD_FINAL_RANGE };
|
||||
|
||||
class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
VL53L0XSensor();
|
||||
@ -39,222 +41,20 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c
|
||||
void set_enable_pin(GPIOPin *enable) { this->enable_pin_ = enable; }
|
||||
|
||||
protected:
|
||||
uint32_t get_measurement_timing_budget_() {
|
||||
SequenceStepEnables enables{};
|
||||
SequenceStepTimeouts timeouts{};
|
||||
uint32_t get_measurement_timing_budget_();
|
||||
bool set_measurement_timing_budget_(uint32_t budget_us);
|
||||
void get_sequence_step_enables_(SequenceStepEnables *enables);
|
||||
void get_sequence_step_timeouts_(SequenceStepEnables const *enables, SequenceStepTimeouts *timeouts);
|
||||
uint8_t get_vcsel_pulse_period_(VcselPeriodType type);
|
||||
uint32_t get_macro_period_(uint8_t vcsel_period_pclks);
|
||||
|
||||
uint16_t start_overhead = 1910;
|
||||
uint16_t end_overhead = 960;
|
||||
uint16_t msrc_overhead = 660;
|
||||
uint16_t tcc_overhead = 590;
|
||||
uint16_t dss_overhead = 690;
|
||||
uint16_t pre_range_overhead = 660;
|
||||
uint16_t final_range_overhead = 550;
|
||||
uint32_t timeout_mclks_to_microseconds_(uint16_t timeout_period_mclks, uint8_t vcsel_period_pclks);
|
||||
uint32_t timeout_microseconds_to_mclks_(uint32_t timeout_period_us, uint8_t vcsel_period_pclks);
|
||||
|
||||
// "Start and end overhead times always present"
|
||||
uint32_t budget_us = start_overhead + end_overhead;
|
||||
uint16_t decode_timeout_(uint16_t reg_val);
|
||||
uint16_t encode_timeout_(uint16_t timeout_mclks);
|
||||
|
||||
get_sequence_step_enables_(&enables);
|
||||
get_sequence_step_timeouts_(&enables, &timeouts);
|
||||
|
||||
if (enables.tcc)
|
||||
budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead);
|
||||
|
||||
if (enables.dss) {
|
||||
budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead);
|
||||
} else if (enables.msrc) {
|
||||
budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead);
|
||||
}
|
||||
|
||||
if (enables.pre_range)
|
||||
budget_us += (timeouts.pre_range_us + pre_range_overhead);
|
||||
|
||||
if (enables.final_range)
|
||||
budget_us += (timeouts.final_range_us + final_range_overhead);
|
||||
|
||||
measurement_timing_budget_us_ = budget_us; // store for internal reuse
|
||||
return budget_us;
|
||||
}
|
||||
|
||||
bool set_measurement_timing_budget_(uint32_t budget_us) {
|
||||
SequenceStepEnables enables{};
|
||||
SequenceStepTimeouts timeouts{};
|
||||
|
||||
uint16_t start_overhead = 1320; // note that this is different than the value in get_
|
||||
uint16_t end_overhead = 960;
|
||||
uint16_t msrc_overhead = 660;
|
||||
uint16_t tcc_overhead = 590;
|
||||
uint16_t dss_overhead = 690;
|
||||
uint16_t pre_range_overhead = 660;
|
||||
uint16_t final_range_overhead = 550;
|
||||
|
||||
uint32_t min_timing_budget = 20000;
|
||||
|
||||
if (budget_us < min_timing_budget) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t used_budget_us = start_overhead + end_overhead;
|
||||
|
||||
get_sequence_step_enables_(&enables);
|
||||
get_sequence_step_timeouts_(&enables, &timeouts);
|
||||
|
||||
if (enables.tcc) {
|
||||
used_budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead);
|
||||
}
|
||||
|
||||
if (enables.dss) {
|
||||
used_budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead);
|
||||
} else if (enables.msrc) {
|
||||
used_budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead);
|
||||
}
|
||||
|
||||
if (enables.pre_range) {
|
||||
used_budget_us += (timeouts.pre_range_us + pre_range_overhead);
|
||||
}
|
||||
|
||||
if (enables.final_range) {
|
||||
used_budget_us += final_range_overhead;
|
||||
|
||||
// "Note that the final range timeout is determined by the timing
|
||||
// budget and the sum of all other timeouts within the sequence.
|
||||
// If there is no room for the final range timeout, then an error
|
||||
// will be set. Otherwise the remaining time will be applied to
|
||||
// the final range."
|
||||
|
||||
if (used_budget_us > budget_us) {
|
||||
// "Requested timeout too big."
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t final_range_timeout_us = budget_us - used_budget_us;
|
||||
|
||||
// set_sequence_step_timeout() begin
|
||||
// (SequenceStepId == VL53L0X_SEQUENCESTEP_FINAL_RANGE)
|
||||
|
||||
// "For the final range timeout, the pre-range timeout
|
||||
// must be added. To do this both final and pre-range
|
||||
// timeouts must be expressed in macro periods MClks
|
||||
// because they have different vcsel periods."
|
||||
|
||||
uint16_t final_range_timeout_mclks =
|
||||
timeout_microseconds_to_mclks_(final_range_timeout_us, timeouts.final_range_vcsel_period_pclks);
|
||||
|
||||
if (enables.pre_range) {
|
||||
final_range_timeout_mclks += timeouts.pre_range_mclks;
|
||||
}
|
||||
|
||||
write_byte_16(0x71, encode_timeout_(final_range_timeout_mclks));
|
||||
|
||||
// set_sequence_step_timeout() end
|
||||
|
||||
measurement_timing_budget_us_ = budget_us; // store for internal reuse
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void get_sequence_step_enables_(SequenceStepEnables *enables) {
|
||||
uint8_t sequence_config = reg(0x01).get();
|
||||
enables->tcc = (sequence_config >> 4) & 0x1;
|
||||
enables->dss = (sequence_config >> 3) & 0x1;
|
||||
enables->msrc = (sequence_config >> 2) & 0x1;
|
||||
enables->pre_range = (sequence_config >> 6) & 0x1;
|
||||
enables->final_range = (sequence_config >> 7) & 0x1;
|
||||
}
|
||||
|
||||
enum VcselPeriodType { VCSEL_PERIOD_PRE_RANGE, VCSEL_PERIOD_FINAL_RANGE };
|
||||
|
||||
void get_sequence_step_timeouts_(SequenceStepEnables const *enables, SequenceStepTimeouts *timeouts) {
|
||||
timeouts->pre_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_PRE_RANGE);
|
||||
|
||||
timeouts->msrc_dss_tcc_mclks = reg(0x46).get() + 1;
|
||||
timeouts->msrc_dss_tcc_us =
|
||||
timeout_mclks_to_microseconds_(timeouts->msrc_dss_tcc_mclks, timeouts->pre_range_vcsel_period_pclks);
|
||||
|
||||
uint16_t value;
|
||||
read_byte_16(0x51, &value);
|
||||
timeouts->pre_range_mclks = decode_timeout_(value);
|
||||
timeouts->pre_range_us =
|
||||
timeout_mclks_to_microseconds_(timeouts->pre_range_mclks, timeouts->pre_range_vcsel_period_pclks);
|
||||
|
||||
timeouts->final_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_FINAL_RANGE);
|
||||
|
||||
read_byte_16(0x71, &value);
|
||||
timeouts->final_range_mclks = decode_timeout_(value);
|
||||
|
||||
if (enables->pre_range) {
|
||||
timeouts->final_range_mclks -= timeouts->pre_range_mclks;
|
||||
}
|
||||
|
||||
timeouts->final_range_us =
|
||||
timeout_mclks_to_microseconds_(timeouts->final_range_mclks, timeouts->final_range_vcsel_period_pclks);
|
||||
}
|
||||
|
||||
uint8_t get_vcsel_pulse_period_(VcselPeriodType type) {
|
||||
uint8_t vcsel;
|
||||
if (type == VCSEL_PERIOD_PRE_RANGE) {
|
||||
vcsel = reg(0x50).get();
|
||||
} else if (type == VCSEL_PERIOD_FINAL_RANGE) {
|
||||
vcsel = reg(0x70).get();
|
||||
} else {
|
||||
return 255;
|
||||
}
|
||||
|
||||
return (vcsel + 1) << 1;
|
||||
}
|
||||
|
||||
uint32_t get_macro_period_(uint8_t vcsel_period_pclks) {
|
||||
return ((2304UL * vcsel_period_pclks * 1655UL) + 500UL) / 1000UL;
|
||||
}
|
||||
|
||||
uint32_t timeout_mclks_to_microseconds_(uint16_t timeout_period_mclks, uint8_t vcsel_period_pclks) {
|
||||
uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks);
|
||||
return ((timeout_period_mclks * macro_period_ns) + (macro_period_ns / 2)) / 1000;
|
||||
}
|
||||
uint32_t timeout_microseconds_to_mclks_(uint32_t timeout_period_us, uint8_t vcsel_period_pclks) {
|
||||
uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks);
|
||||
return (((timeout_period_us * 1000) + (macro_period_ns / 2)) / macro_period_ns);
|
||||
}
|
||||
|
||||
uint16_t decode_timeout_(uint16_t reg_val) {
|
||||
// format: "(LSByte * 2^MSByte) + 1"
|
||||
uint8_t msb = (reg_val >> 8) & 0xFF;
|
||||
uint8_t lsb = (reg_val >> 0) & 0xFF;
|
||||
return (uint16_t(lsb) << msb) + 1;
|
||||
}
|
||||
uint16_t encode_timeout_(uint16_t timeout_mclks) {
|
||||
// format: "(LSByte * 2^MSByte) + 1"
|
||||
uint32_t ls_byte = 0;
|
||||
uint16_t ms_byte = 0;
|
||||
|
||||
if (timeout_mclks <= 0)
|
||||
return 0;
|
||||
|
||||
ls_byte = timeout_mclks - 1;
|
||||
|
||||
while ((ls_byte & 0xFFFFFF00) > 0) {
|
||||
ls_byte >>= 1;
|
||||
ms_byte++;
|
||||
}
|
||||
|
||||
return (ms_byte << 8) | (ls_byte & 0xFF);
|
||||
}
|
||||
|
||||
bool perform_single_ref_calibration_(uint8_t vhv_init_byte) {
|
||||
reg(0x00) = 0x01 | vhv_init_byte; // VL53L0X_REG_SYSRANGE_MODE_START_STOP
|
||||
|
||||
uint32_t start = millis();
|
||||
while ((reg(0x13).get() & 0x07) == 0) {
|
||||
if (millis() - start > 1000)
|
||||
return false;
|
||||
yield();
|
||||
}
|
||||
|
||||
reg(0x0B) = 0x01;
|
||||
reg(0x00) = 0x00;
|
||||
|
||||
return true;
|
||||
}
|
||||
bool perform_single_ref_calibration_(uint8_t vhv_init_byte);
|
||||
|
||||
float signal_rate_limit_;
|
||||
bool long_range_;
|
||||
|
@ -36,6 +36,8 @@ class WaveshareEPaper : public PollingComponent,
|
||||
|
||||
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;
|
||||
|
||||
|
@ -555,6 +555,7 @@ void WiFiComponent::check_connecting_finished() {
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "WiFi Unknown connection status %d", (int) status);
|
||||
this->retry_connect();
|
||||
}
|
||||
|
||||
void WiFiComponent::retry_connect() {
|
||||
|
@ -195,7 +195,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
|
||||
#if ESP_IDF_VERSION_MAJOR >= 4
|
||||
// Protected Management Frame
|
||||
// Device will prefer to connect in PMF mode if other device also advertizes PMF capability.
|
||||
// Device will prefer to connect in PMF mode if other device also advertises PMF capability.
|
||||
conf.sta.pmf_cfg.capable = true;
|
||||
conf.sta.pmf_cfg.required = false;
|
||||
#endif
|
||||
|
@ -303,7 +303,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
|
||||
#if ESP_IDF_VERSION_MAJOR >= 4
|
||||
// Protected Management Frame
|
||||
// Device will prefer to connect in PMF mode if other device also advertizes PMF capability.
|
||||
// Device will prefer to connect in PMF mode if other device also advertises PMF capability.
|
||||
conf.sta.pmf_cfg.capable = true;
|
||||
conf.sta.pmf_cfg.required = false;
|
||||
#endif
|
||||
|
@ -38,7 +38,7 @@ bool XiaomiMiscale::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
|
||||
if (this->impedance_ != nullptr) {
|
||||
if (res->version == 1) {
|
||||
ESP_LOGW(TAG, "Impedance is only supported on version 2. Your scale was identified as verison 1.");
|
||||
ESP_LOGW(TAG, "Impedance is only supported on version 2. Your scale was identified as version 1.");
|
||||
} else {
|
||||
if (res->impedance.has_value()) {
|
||||
this->impedance_->publish_state(*res->impedance);
|
||||
|
@ -679,6 +679,7 @@ def validate_config(config, command_line_substitutions) -> Config:
|
||||
result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS)
|
||||
try:
|
||||
substitutions.do_substitution_pass(config, command_line_substitutions)
|
||||
substitutions.do_substitution_pass(config, command_line_substitutions)
|
||||
except vol.Invalid as err:
|
||||
result.add_error(err)
|
||||
return result
|
||||
|
@ -57,11 +57,12 @@ from esphome.core import (
|
||||
TimePeriodMinutes,
|
||||
)
|
||||
from esphome.helpers import list_starts_with, add_class_to_obj
|
||||
from esphome.jsonschema import (
|
||||
jschema_list,
|
||||
jschema_extractor,
|
||||
jschema_registry,
|
||||
jschema_typed,
|
||||
from esphome.schema_extractors import (
|
||||
SCHEMA_EXTRACT,
|
||||
schema_extractor_list,
|
||||
schema_extractor,
|
||||
schema_extractor_registry,
|
||||
schema_extractor_typed,
|
||||
)
|
||||
from esphome.util import parse_esphome_version
|
||||
from esphome.voluptuous_schema import _Schema
|
||||
@ -327,7 +328,7 @@ def boolean(value):
|
||||
)
|
||||
|
||||
|
||||
@jschema_list
|
||||
@schema_extractor_list
|
||||
def ensure_list(*validators):
|
||||
"""Validate this configuration option to be a list.
|
||||
|
||||
@ -452,7 +453,11 @@ def validate_id_name(value):
|
||||
def use_id(type):
|
||||
"""Declare that this configuration option should point to an ID with the given type."""
|
||||
|
||||
@schema_extractor("use_id")
|
||||
def validator(value):
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return type
|
||||
|
||||
check_not_templatable(value)
|
||||
if value is None:
|
||||
return core.ID(None, is_declaration=False, type=type)
|
||||
@ -475,7 +480,11 @@ def declare_id(type):
|
||||
If two IDs with the same name exist, a validation error is thrown.
|
||||
"""
|
||||
|
||||
@schema_extractor("declare_id")
|
||||
def validator(value):
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return type
|
||||
|
||||
check_not_templatable(value)
|
||||
if value is None:
|
||||
return core.ID(None, is_declaration=True, type=type)
|
||||
@ -494,11 +503,11 @@ def templatable(other_validators):
|
||||
"""
|
||||
schema = Schema(other_validators)
|
||||
|
||||
@jschema_extractor("templatable")
|
||||
@schema_extractor("templatable")
|
||||
def validator(value):
|
||||
# pylint: disable=comparison-with-callable
|
||||
if value == jschema_extractor:
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return other_validators
|
||||
|
||||
if isinstance(value, Lambda):
|
||||
return returning_lambda(value)
|
||||
if isinstance(other_validators, dict):
|
||||
@ -963,9 +972,9 @@ def ipv4(value):
|
||||
elif isinstance(value, IPAddress):
|
||||
return value
|
||||
else:
|
||||
raise Invalid("IPv4 address must consist of either string or " "integer list")
|
||||
raise Invalid("IPv4 address must consist of either string or integer list")
|
||||
if len(parts) != 4:
|
||||
raise Invalid("IPv4 address must consist of four point-separated " "integers")
|
||||
raise Invalid("IPv4 address must consist of four point-separated integers")
|
||||
parts_ = list(map(int, parts))
|
||||
if not all(0 <= x < 256 for x in parts_):
|
||||
raise Invalid("IPv4 address parts must be in range from 0 to 255")
|
||||
@ -985,10 +994,10 @@ def _valid_topic(value):
|
||||
raise Invalid("MQTT topic name/filter must not be empty.")
|
||||
if len(raw_value) > 65535:
|
||||
raise Invalid(
|
||||
"MQTT topic name/filter must not be longer than " "65535 encoded bytes."
|
||||
"MQTT topic name/filter must not be longer than 65535 encoded bytes."
|
||||
)
|
||||
if "\0" in value:
|
||||
raise Invalid("MQTT topic name/filter must not contain null " "character.")
|
||||
raise Invalid("MQTT topic name/filter must not contain null character.")
|
||||
return value
|
||||
|
||||
|
||||
@ -1000,7 +1009,7 @@ def subscribe_topic(value):
|
||||
i < len(value) - 1 and value[i + 1] != "/"
|
||||
):
|
||||
raise Invalid(
|
||||
"Single-level wildcard must occupy an entire " "level of the filter"
|
||||
"Single-level wildcard must occupy an entire level of the filter"
|
||||
)
|
||||
|
||||
index = value.find("#")
|
||||
@ -1012,9 +1021,7 @@ def subscribe_topic(value):
|
||||
"character in the topic filter."
|
||||
)
|
||||
if len(value) > 1 and value[index - 1] != "/":
|
||||
raise Invalid(
|
||||
"Multi-level wildcard must be after a topic " "level separator."
|
||||
)
|
||||
raise Invalid("Multi-level wildcard must be after a topic level separator.")
|
||||
|
||||
return value
|
||||
|
||||
@ -1086,16 +1093,21 @@ def possibly_negative_percentage(value):
|
||||
except ValueError:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise Invalid("invalid number")
|
||||
if value > 1:
|
||||
msg = "Percentage must not be higher than 100%."
|
||||
if not has_percent_sign:
|
||||
msg += " Please put a percent sign after the number!"
|
||||
raise Invalid(msg)
|
||||
if value < -1:
|
||||
msg = "Percentage must not be smaller than -100%."
|
||||
if not has_percent_sign:
|
||||
msg += " Please put a percent sign after the number!"
|
||||
raise Invalid(msg)
|
||||
try:
|
||||
if value > 1:
|
||||
msg = "Percentage must not be higher than 100%."
|
||||
if not has_percent_sign:
|
||||
msg += " Please put a percent sign after the number!"
|
||||
raise Invalid(msg)
|
||||
if value < -1:
|
||||
msg = "Percentage must not be smaller than -100%."
|
||||
if not has_percent_sign:
|
||||
msg += " Please put a percent sign after the number!"
|
||||
raise Invalid(msg)
|
||||
except TypeError:
|
||||
raise Invalid( # pylint: disable=raise-missing-from
|
||||
"Expected percentage or float between -1.0 and 1.0"
|
||||
)
|
||||
return negative_one_to_one_float(value)
|
||||
|
||||
|
||||
@ -1172,10 +1184,9 @@ def one_of(*values, **kwargs):
|
||||
if kwargs:
|
||||
raise ValueError
|
||||
|
||||
@jschema_extractor("one_of")
|
||||
@schema_extractor("one_of")
|
||||
def validator(value):
|
||||
# pylint: disable=comparison-with-callable
|
||||
if value == jschema_extractor:
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return values
|
||||
|
||||
if string_:
|
||||
@ -1215,10 +1226,9 @@ def enum(mapping, **kwargs):
|
||||
assert isinstance(mapping, dict)
|
||||
one_of_validator = one_of(*mapping, **kwargs)
|
||||
|
||||
@jschema_extractor("enum")
|
||||
@schema_extractor("enum")
|
||||
def validator(value):
|
||||
# pylint: disable=comparison-with-callable
|
||||
if value == jschema_extractor:
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return mapping
|
||||
|
||||
value = one_of_validator(value)
|
||||
@ -1391,7 +1401,7 @@ def extract_keys(schema):
|
||||
return keys
|
||||
|
||||
|
||||
@jschema_typed
|
||||
@schema_extractor_typed
|
||||
def typed_schema(schemas, **kwargs):
|
||||
"""Create a schema that has a key to distinguish between schemas"""
|
||||
key = kwargs.pop("key", CONF_TYPE)
|
||||
@ -1505,7 +1515,7 @@ def validate_registry_entry(name, registry):
|
||||
)
|
||||
ignore_keys = extract_keys(base_schema)
|
||||
|
||||
@jschema_registry(registry)
|
||||
@schema_extractor_registry(registry)
|
||||
def validator(value):
|
||||
if isinstance(value, str):
|
||||
value = {value: {}}
|
||||
@ -1550,12 +1560,15 @@ def validate_registry(name, registry):
|
||||
return ensure_list(validate_registry_entry(name, registry))
|
||||
|
||||
|
||||
@jschema_list
|
||||
def maybe_simple_value(*validators, **kwargs):
|
||||
key = kwargs.pop("key", CONF_VALUE)
|
||||
validator = All(*validators)
|
||||
|
||||
@schema_extractor("maybe")
|
||||
def validate(value):
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return (validator, key)
|
||||
|
||||
if isinstance(value, dict) and key in value:
|
||||
return validator(value)
|
||||
return validator({key: value})
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Constants used by esphome."""
|
||||
|
||||
__version__ = "2022.6.0-dev"
|
||||
__version__ = "2022.7.0-dev"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
|
||||
@ -905,12 +905,15 @@ DEVICE_CLASS_BATTERY = "battery"
|
||||
DEVICE_CLASS_GAS = "gas"
|
||||
DEVICE_CLASS_POWER = "power"
|
||||
# device classes of sensor component
|
||||
DEVICE_CLASS_APPARENT_POWER = "apparent_power"
|
||||
DEVICE_CLASS_AQI = "aqi"
|
||||
DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide"
|
||||
DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide"
|
||||
DEVICE_CLASS_CURRENT = "current"
|
||||
DEVICE_CLASS_DATE = "date"
|
||||
DEVICE_CLASS_DURATION = "duration"
|
||||
DEVICE_CLASS_ENERGY = "energy"
|
||||
DEVICE_CLASS_FREQUENCY = "frequency"
|
||||
DEVICE_CLASS_HUMIDITY = "humidity"
|
||||
DEVICE_CLASS_ILLUMINANCE = "illuminance"
|
||||
DEVICE_CLASS_MONETARY = "monetary"
|
||||
@ -923,6 +926,7 @@ DEVICE_CLASS_PM10 = "pm10"
|
||||
DEVICE_CLASS_PM25 = "pm25"
|
||||
DEVICE_CLASS_POWER_FACTOR = "power_factor"
|
||||
DEVICE_CLASS_PRESSURE = "pressure"
|
||||
DEVICE_CLASS_REACTIVE_POWER = "reactive_power"
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength"
|
||||
DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide"
|
||||
DEVICE_CLASS_TEMPERATURE = "temperature"
|
||||
|
@ -651,7 +651,7 @@ class EsphomeCore:
|
||||
continue
|
||||
if other.repository is not None:
|
||||
if library.repository is None or other.repository == library.repository:
|
||||
# Other is using a/the same repository, takes precendence
|
||||
# Other is using a/the same repository, takes precedence
|
||||
break
|
||||
raise ValueError(
|
||||
f"Adding named Library with repository failed! Libraries {library} and {other} "
|
||||
|
@ -125,19 +125,26 @@ void IRAM_ATTR HOT Application::feed_wdt() {
|
||||
}
|
||||
void Application::reboot() {
|
||||
ESP_LOGI(TAG, "Forcing a reboot...");
|
||||
for (auto *comp : this->components_)
|
||||
comp->on_shutdown();
|
||||
for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) {
|
||||
(*it)->on_shutdown();
|
||||
}
|
||||
arch_restart();
|
||||
}
|
||||
void Application::safe_reboot() {
|
||||
ESP_LOGI(TAG, "Rebooting safely...");
|
||||
for (auto *comp : this->components_)
|
||||
comp->on_safe_shutdown();
|
||||
for (auto *comp : this->components_)
|
||||
comp->on_shutdown();
|
||||
run_safe_shutdown_hooks();
|
||||
arch_restart();
|
||||
}
|
||||
|
||||
void Application::run_safe_shutdown_hooks() {
|
||||
for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) {
|
||||
(*it)->on_safe_shutdown();
|
||||
}
|
||||
for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) {
|
||||
(*it)->on_shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
void Application::calculate_looping_components_() {
|
||||
for (auto *obj : this->components_) {
|
||||
if (obj->has_overridden_loop())
|
||||
|
@ -45,6 +45,9 @@
|
||||
#ifdef USE_LOCK
|
||||
#include "esphome/components/lock/lock.h"
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
#include "esphome/components/media_player/media_player.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@ -111,6 +114,10 @@ class Application {
|
||||
void register_lock(lock::Lock *a_lock) { this->locks_.push_back(a_lock); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void register_media_player(media_player::MediaPlayer *media_player) { this->media_players_.push_back(media_player); }
|
||||
#endif
|
||||
|
||||
/// Register the component in this Application instance.
|
||||
template<class C> C *register_component(C *c) {
|
||||
static_assert(std::is_base_of<Component, C>::value, "Only Component subclasses can be registered");
|
||||
@ -154,14 +161,7 @@ class Application {
|
||||
|
||||
void safe_reboot();
|
||||
|
||||
void run_safe_shutdown_hooks() {
|
||||
for (auto *comp : this->components_) {
|
||||
comp->on_safe_shutdown();
|
||||
}
|
||||
for (auto *comp : this->components_) {
|
||||
comp->on_shutdown();
|
||||
}
|
||||
}
|
||||
void run_safe_shutdown_hooks();
|
||||
|
||||
uint32_t get_app_state() const { return this->app_state_; }
|
||||
|
||||
@ -273,6 +273,15 @@ class Application {
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
const std::vector<media_player::MediaPlayer *> &get_media_players() { return this->media_players_; }
|
||||
media_player::MediaPlayer *get_media_player_by_key(uint32_t key, bool include_internal = false) {
|
||||
for (auto *obj : this->media_players_)
|
||||
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
|
||||
return obj;
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
Scheduler scheduler;
|
||||
|
||||
@ -324,6 +333,9 @@ class Application {
|
||||
#ifdef USE_LOCK
|
||||
std::vector<lock::Lock *> locks_{};
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
std::vector<media_player::MediaPlayer *> media_players_{};
|
||||
#endif
|
||||
|
||||
std::string name_;
|
||||
std::string compilation_time_;
|
||||
|
@ -231,6 +231,21 @@ void ComponentIterator::advance() {
|
||||
}
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
case IteratorState::MEDIA_PLAYER:
|
||||
if (this->at_ >= App.get_media_players().size()) {
|
||||
advance_platform = true;
|
||||
} else {
|
||||
auto *media_player = App.get_media_players()[this->at_];
|
||||
if (media_player->is_internal() && !this->include_internal_) {
|
||||
success = true;
|
||||
break;
|
||||
} else {
|
||||
success = this->on_media_player(media_player);
|
||||
}
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
case IteratorState::MAX:
|
||||
if (this->on_end()) {
|
||||
@ -254,4 +269,7 @@ bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
bool ComponentIterator::on_camera(esp32_camera::ESP32Camera *camera) { return true; }
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool ComponentIterator::on_media_player(media_player::MediaPlayer *media_player) { return true; }
|
||||
#endif
|
||||
} // namespace esphome
|
||||
|
@ -62,6 +62,9 @@ class ComponentIterator {
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
virtual bool on_lock(lock::Lock *a_lock) = 0;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
virtual bool on_media_player(media_player::MediaPlayer *media_player);
|
||||
#endif
|
||||
virtual bool on_end();
|
||||
|
||||
@ -110,6 +113,9 @@ class ComponentIterator {
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
LOCK,
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
MEDIA_PLAYER,
|
||||
#endif
|
||||
MAX,
|
||||
} state_{IteratorState::NONE};
|
||||
|
@ -73,6 +73,12 @@ void Controller::setup_controller(bool include_internal) {
|
||||
obj->add_on_state_callback([this, obj]() { this->on_lock_update(obj); });
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
for (auto *obj : App.get_media_players()) {
|
||||
if (include_internal || !obj->is_internal())
|
||||
obj->add_on_state_callback([this, obj]() { this->on_media_player_update(obj); });
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
@ -37,6 +37,9 @@
|
||||
#ifdef USE_LOCK
|
||||
#include "esphome/components/lock/lock.h"
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
#include "esphome/components/media_player/media_player.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@ -76,6 +79,9 @@ class Controller {
|
||||
#ifdef USE_LOCK
|
||||
virtual void on_lock_update(lock::Lock *obj){};
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
virtual void on_media_player_update(media_player::MediaPlayer *obj){};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
|
@ -29,6 +29,7 @@
|
||||
#define USE_LOCK
|
||||
#define USE_LOGGER
|
||||
#define USE_MDNS
|
||||
#define USE_MEDIA_PLAYER
|
||||
#define USE_MQTT
|
||||
#define USE_NUMBER
|
||||
#define USE_OTA_PASSWORD
|
||||
|
@ -46,7 +46,9 @@ class EntityBase {
|
||||
void set_icon(const std::string &name);
|
||||
|
||||
protected:
|
||||
virtual uint32_t hash_base() = 0;
|
||||
/// The hash_base() function has been deprecated. It is kept in this
|
||||
/// class for now, to prevent external components from not compiling.
|
||||
virtual uint32_t hash_base() { return 0L; }
|
||||
void calc_object_id_();
|
||||
|
||||
std::string name_;
|
||||
|
@ -9,7 +9,7 @@ namespace esphome {
|
||||
ESP_LOGCONFIG(TAG, prefix "%s", (pin)->dump_summary().c_str()); \
|
||||
}
|
||||
|
||||
// put GPIO flags in a namepsace to not pollute esphome namespace
|
||||
// put GPIO flags in a namespace to not pollute esphome namespace
|
||||
namespace gpio {
|
||||
|
||||
enum Flags : uint8_t {
|
||||
|
@ -258,10 +258,10 @@ inline std::string to_string(const std::string &val) { return val; }
|
||||
/// Truncate a string to a specific length.
|
||||
std::string str_truncate(const std::string &str, size_t length);
|
||||
|
||||
/// Extract the part of the string until either the first occurence of the specified character, or the end (requires str
|
||||
/// to be null-terminated).
|
||||
/// Extract the part of the string until either the first occurrence of the specified character, or the end
|
||||
/// (requires str to be null-terminated).
|
||||
std::string str_until(const char *str, char ch);
|
||||
/// Extract the part of the string until either the first occurence of the specified character, or the end.
|
||||
/// Extract the part of the string until either the first occurrence of the specified character, or the end.
|
||||
std::string str_until(const std::string &str, char ch);
|
||||
|
||||
/// Convert the string to lower case.
|
||||
@ -600,7 +600,7 @@ template<class T> class ExternalRAMAllocator {
|
||||
|
||||
ExternalRAMAllocator() = default;
|
||||
ExternalRAMAllocator(Flags flags) : flags_{flags} {}
|
||||
template<class U> constexpr ExternalRAMAllocator(const ExternalRAMAllocator<U> &other) : flags_{other.flags} {}
|
||||
template<class U> constexpr ExternalRAMAllocator(const ExternalRAMAllocator<U> &other) : flags_{other.flags_} {}
|
||||
|
||||
T *allocate(size_t n) {
|
||||
size_t size = n * sizeof(T);
|
||||
|
@ -60,7 +60,6 @@ class ProgressBar:
|
||||
sys.stderr.write(text)
|
||||
sys.stderr.flush()
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def done(self):
|
||||
sys.stderr.write("\n")
|
||||
sys.stderr.flush()
|
||||
|
@ -296,7 +296,7 @@ _TYPE_OVERLOADS = {
|
||||
int: type("EInt", (int,), {}),
|
||||
float: type("EFloat", (float,), {}),
|
||||
str: type("EStr", (str,), {}),
|
||||
dict: type("EDict", (str,), {}),
|
||||
dict: type("EDict", (dict,), {}),
|
||||
list: type("EList", (list,), {}),
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ def patch_structhash():
|
||||
# removed/added. This might have unintended consequences, but this improves compile
|
||||
# times greatly when adding/removing components and a simple clean build solves
|
||||
# all issues
|
||||
from platformio.commands.run import helpers, command
|
||||
from platformio.run import helpers, cli
|
||||
from os.path import join, isdir, getmtime
|
||||
from os import makedirs
|
||||
|
||||
@ -39,7 +39,7 @@ def patch_structhash():
|
||||
|
||||
# pylint: disable=protected-access
|
||||
helpers.clean_build_dir = patched_clean_build_dir
|
||||
command.clean_build_dir = patched_clean_build_dir
|
||||
cli.clean_build_dir = patched_clean_build_dir
|
||||
|
||||
|
||||
IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})"
|
||||
|
@ -9,9 +9,9 @@ However there is a property to further disable decorator
|
||||
impact."""
|
||||
|
||||
|
||||
# This is set to true by script/build_jsonschema.py
|
||||
# This is set to true by script/build_language_schema.py
|
||||
# only, so data is collected (again functionality is not modified)
|
||||
EnableJsonSchemaCollect = False
|
||||
EnableSchemaExtraction = False
|
||||
|
||||
extended_schemas = {}
|
||||
list_schemas = {}
|
||||
@ -19,9 +19,12 @@ registry_schemas = {}
|
||||
hidden_schemas = {}
|
||||
typed_schemas = {}
|
||||
|
||||
# This key is used to generate schema files of Esphome configuration.
|
||||
SCHEMA_EXTRACT = object()
|
||||
|
||||
def jschema_extractor(validator_name):
|
||||
if EnableJsonSchemaCollect:
|
||||
|
||||
def schema_extractor(validator_name):
|
||||
if EnableSchemaExtraction:
|
||||
|
||||
def decorator(func):
|
||||
hidden_schemas[repr(func)] = validator_name
|
||||
@ -35,8 +38,8 @@ def jschema_extractor(validator_name):
|
||||
return dummy
|
||||
|
||||
|
||||
def jschema_extended(func):
|
||||
if EnableJsonSchemaCollect:
|
||||
def schema_extractor_extended(func):
|
||||
if EnableSchemaExtraction:
|
||||
|
||||
def decorate(*args, **kwargs):
|
||||
ret = func(*args, **kwargs)
|
||||
@ -49,8 +52,8 @@ def jschema_extended(func):
|
||||
return func
|
||||
|
||||
|
||||
def jschema_list(func):
|
||||
if EnableJsonSchemaCollect:
|
||||
def schema_extractor_list(func):
|
||||
if EnableSchemaExtraction:
|
||||
|
||||
def decorate(*args, **kwargs):
|
||||
ret = func(*args, **kwargs)
|
||||
@ -63,8 +66,8 @@ def jschema_list(func):
|
||||
return func
|
||||
|
||||
|
||||
def jschema_registry(registry):
|
||||
if EnableJsonSchemaCollect:
|
||||
def schema_extractor_registry(registry):
|
||||
if EnableSchemaExtraction:
|
||||
|
||||
def decorator(func):
|
||||
registry_schemas[repr(func)] = registry
|
||||
@ -78,8 +81,8 @@ def jschema_registry(registry):
|
||||
return dummy
|
||||
|
||||
|
||||
def jschema_typed(func):
|
||||
if EnableJsonSchemaCollect:
|
||||
def schema_extractor_typed(func):
|
||||
if EnableSchemaExtraction:
|
||||
|
||||
def decorate(*args, **kwargs):
|
||||
ret = func(*args, **kwargs)
|
@ -152,7 +152,6 @@ class RedirectText:
|
||||
# any caller.
|
||||
return len(s)
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def isatty(self):
|
||||
return True
|
||||
|
||||
|
@ -2,7 +2,7 @@ import difflib
|
||||
import itertools
|
||||
|
||||
import voluptuous as vol
|
||||
from esphome.jsonschema import jschema_extended
|
||||
from esphome.schema_extractors import schema_extractor_extended
|
||||
|
||||
|
||||
class ExtraKeysInvalid(vol.Invalid):
|
||||
@ -203,7 +203,7 @@ class _Schema(vol.Schema):
|
||||
self._extra_schemas.append(validator)
|
||||
return self
|
||||
|
||||
@jschema_extended
|
||||
@schema_extractor_extended
|
||||
# pylint: disable=signature-differs
|
||||
def extend(self, *schemas, **kwargs):
|
||||
extra = kwargs.pop("extra", None)
|
||||
|
@ -343,7 +343,7 @@ def wizard(path):
|
||||
sleep(1)
|
||||
|
||||
safe_print_step(3, WIFI_BIG)
|
||||
safe_print("In this step, I'm going to create the configuration for " "WiFi.")
|
||||
safe_print("In this step, I'm going to create the configuration for WiFi.")
|
||||
safe_print()
|
||||
sleep(1)
|
||||
safe_print(
|
||||
|
@ -251,7 +251,49 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors
|
||||
|
||||
@_add_data_ref
|
||||
def construct_include(self, node):
|
||||
return _load_yaml_internal(self._rel_path(node.value))
|
||||
def extract_file_vars(node):
|
||||
fields = self.construct_yaml_map(node)
|
||||
file = fields.get("file")
|
||||
if file is None:
|
||||
raise yaml.MarkedYAMLError("Must include 'file'", node.start_mark)
|
||||
vars = fields.get("vars")
|
||||
if vars:
|
||||
vars = {k: str(v) for k, v in vars.items()}
|
||||
return file, vars
|
||||
|
||||
def substitute_vars(config, vars):
|
||||
from esphome.const import CONF_SUBSTITUTIONS
|
||||
from esphome.components import substitutions
|
||||
|
||||
org_subs = None
|
||||
result = config
|
||||
if not isinstance(config, dict):
|
||||
# when the included yaml contains a list or a scalar
|
||||
# wrap it into an OrderedDict because do_substitution_pass expects it
|
||||
result = OrderedDict([("yaml", config)])
|
||||
elif CONF_SUBSTITUTIONS in result:
|
||||
org_subs = result.pop(CONF_SUBSTITUTIONS)
|
||||
|
||||
result[CONF_SUBSTITUTIONS] = vars
|
||||
# Ignore missing vars that refer to the top level substitutions
|
||||
substitutions.do_substitution_pass(result, None, ignore_missing=True)
|
||||
result.pop(CONF_SUBSTITUTIONS)
|
||||
|
||||
if not isinstance(config, dict):
|
||||
result = result["yaml"] # unwrap the result
|
||||
elif org_subs:
|
||||
result[CONF_SUBSTITUTIONS] = org_subs
|
||||
return result
|
||||
|
||||
if isinstance(node, yaml.nodes.MappingNode):
|
||||
file, vars = extract_file_vars(node)
|
||||
else:
|
||||
file, vars = node.value, None
|
||||
|
||||
result = _load_yaml_internal(self._rel_path(file))
|
||||
if vars:
|
||||
result = substitute_vars(result, vars)
|
||||
return result
|
||||
|
||||
@_add_data_ref
|
||||
def construct_include_dir_list(self, node):
|
||||
|
@ -40,7 +40,7 @@ lib_deps =
|
||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
|
||||
; This is using the repository until a new release is published to PlatformIO
|
||||
https://github.com/Sensirion/arduino-gas-index-algorithm.git ; Sensirion Gas Index Algorithm Arduino Library
|
||||
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
|
||||
build_flags =
|
||||
-DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
src_filter =
|
||||
@ -120,10 +120,12 @@ lib_deps =
|
||||
HTTPClient ; http_request,nextion (Arduino built-in)
|
||||
ESPmDNS ; mdns (Arduino built-in)
|
||||
DNSServer ; captive_portal (Arduino built-in)
|
||||
esphome/ESP32-audioI2S@2.1.0 ; i2s_audio
|
||||
build_flags =
|
||||
${common:arduino.build_flags}
|
||||
-DUSE_ESP32
|
||||
-DUSE_ESP32_FRAMEWORK_ARDUINO
|
||||
-DAUDIO_NO_SD_FS ; i2s_audio
|
||||
extra_scripts = post:esphome/components/esp32/post_build.py.script
|
||||
|
||||
; This are common settings for the ESP32 (all variants) using IDF.
|
||||
|
1
pylintrc
1
pylintrc
@ -24,7 +24,6 @@ disable=
|
||||
undefined-loop-variable,
|
||||
useless-object-inheritance,
|
||||
stop-iteration-return,
|
||||
no-self-use,
|
||||
import-outside-toplevel,
|
||||
# Broken
|
||||
unsupported-membership-test,
|
||||
|
@ -1,17 +1,17 @@
|
||||
voluptuous==0.13.1
|
||||
PyYAML==6.0
|
||||
paho-mqtt==1.6.1
|
||||
colorama==0.4.4
|
||||
colorama==0.4.5
|
||||
tornado==6.1
|
||||
tzlocal==4.2 # from time
|
||||
tzdata>=2021.1 # from time
|
||||
pyserial==3.5
|
||||
platformio==5.2.5 # When updating platformio, also update Dockerfile
|
||||
platformio==6.0.2 # When updating platformio, also update Dockerfile
|
||||
esptool==3.3.1
|
||||
click==8.1.3
|
||||
esphome-dashboard==20220508.0
|
||||
aioesphomeapi==10.8.2
|
||||
zeroconf==0.38.4
|
||||
aioesphomeapi==10.10.0
|
||||
zeroconf==0.38.7
|
||||
|
||||
# esp-idf requires this, but doesn't bundle it by default
|
||||
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24
|
||||
|
@ -1,13 +1,13 @@
|
||||
pylint==2.13.9
|
||||
pylint==2.14.4
|
||||
flake8==4.0.1
|
||||
black==22.3.0
|
||||
pyupgrade==2.32.1
|
||||
black==22.6.0 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==2.34.0 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
# Unit tests
|
||||
pytest==7.1.1
|
||||
pytest-cov==3.0.0
|
||||
pytest-mock==3.7.0
|
||||
pytest-mock==3.8.1
|
||||
pytest-asyncio==0.18.3
|
||||
asyncmock==0.4.2
|
||||
hypothesis==5.49.0
|
||||
|
@ -1,828 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from esphome.cpp_generator import MockObj
|
||||
import json
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
import voluptuous as vol
|
||||
|
||||
# NOTE: Cannot import other esphome components globally as a modification in jsonschema
|
||||
# is needed before modules are loaded
|
||||
import esphome.jsonschema as ejs
|
||||
|
||||
ejs.EnableJsonSchemaCollect = True
|
||||
|
||||
DUMP_COMMENTS = False
|
||||
|
||||
JSC_ACTION = "automation.ACTION_REGISTRY"
|
||||
JSC_ALLOF = "allOf"
|
||||
JSC_ANYOF = "anyOf"
|
||||
JSC_COMMENT = "$comment"
|
||||
JSC_CONDITION = "automation.CONDITION_REGISTRY"
|
||||
JSC_DESCRIPTION = "description"
|
||||
JSC_ONEOF = "oneOf"
|
||||
JSC_PROPERTIES = "properties"
|
||||
JSC_REF = "$ref"
|
||||
|
||||
# this should be required, but YAML Language server completion does not work properly if required are specified.
|
||||
# still needed for other features / checks
|
||||
JSC_REQUIRED = "required_"
|
||||
|
||||
SIMPLE_AUTOMATION = "simple_automation"
|
||||
|
||||
schema_names = {}
|
||||
schema_registry = {}
|
||||
components = {}
|
||||
modules = {}
|
||||
registries = []
|
||||
pending_refs = []
|
||||
|
||||
definitions = {}
|
||||
base_props = {}
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--output", default="esphome.json", help="Output filename", type=os.path.abspath
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def get_ref(definition):
|
||||
return {JSC_REF: "#/definitions/" + definition}
|
||||
|
||||
|
||||
def is_ref(jschema):
|
||||
return isinstance(jschema, dict) and JSC_REF in jschema
|
||||
|
||||
|
||||
def unref(jschema):
|
||||
return definitions.get(jschema[JSC_REF][len("#/definitions/") :])
|
||||
|
||||
|
||||
def add_definition_array_or_single_object(ref):
|
||||
return {JSC_ANYOF: [{"type": "array", "items": ref}, ref]}
|
||||
|
||||
|
||||
def add_core():
|
||||
from esphome.core.config import CONFIG_SCHEMA
|
||||
|
||||
base_props["esphome"] = get_jschema("esphome", CONFIG_SCHEMA)
|
||||
|
||||
|
||||
def add_buses():
|
||||
# uart
|
||||
from esphome.components.uart import UART_DEVICE_SCHEMA
|
||||
|
||||
get_jschema("uart_bus", UART_DEVICE_SCHEMA)
|
||||
|
||||
# spi
|
||||
from esphome.components.spi import spi_device_schema
|
||||
|
||||
get_jschema("spi_bus", spi_device_schema(False))
|
||||
|
||||
# i2c
|
||||
from esphome.components.i2c import i2c_device_schema
|
||||
|
||||
get_jschema("i2c_bus", i2c_device_schema(None))
|
||||
|
||||
|
||||
def add_registries():
|
||||
for domain, module in modules.items():
|
||||
add_module_registries(domain, module)
|
||||
|
||||
|
||||
def add_module_registries(domain, module):
|
||||
from esphome.util import Registry
|
||||
|
||||
for c in dir(module):
|
||||
m = getattr(module, c)
|
||||
if isinstance(m, Registry):
|
||||
add_registry(domain + "." + c, m)
|
||||
|
||||
|
||||
def add_registry(registry_name, registry):
|
||||
validators = []
|
||||
registries.append((registry, registry_name))
|
||||
for name in registry.keys():
|
||||
schema = get_jschema(str(name), registry[name].schema, create_return_ref=False)
|
||||
if not schema:
|
||||
schema = {"type": "null"}
|
||||
o_schema = {"type": "object", JSC_PROPERTIES: {name: schema}}
|
||||
o_schema = create_ref(
|
||||
registry_name + "-" + name, str(registry[name].schema) + "x", o_schema
|
||||
)
|
||||
validators.append(o_schema)
|
||||
definitions[registry_name] = {JSC_ANYOF: validators}
|
||||
|
||||
|
||||
def get_registry_ref(registry):
|
||||
# we don't know yet
|
||||
ref = {JSC_REF: "pending"}
|
||||
pending_refs.append((ref, registry))
|
||||
return ref
|
||||
|
||||
|
||||
def solve_pending_refs():
|
||||
for ref, registry in pending_refs:
|
||||
for registry_match, name in registries:
|
||||
if registry == registry_match:
|
||||
ref[JSC_REF] = "#/definitions/" + name
|
||||
|
||||
|
||||
def add_module_schemas(name, module):
|
||||
import esphome.config_validation as cv
|
||||
|
||||
for c in dir(module):
|
||||
v = getattr(module, c)
|
||||
if isinstance(v, cv.Schema):
|
||||
get_jschema(name + "." + c, v)
|
||||
|
||||
|
||||
def get_dirs():
|
||||
from esphome.loader import CORE_COMPONENTS_PATH
|
||||
|
||||
dir_names = [
|
||||
d
|
||||
for d in os.listdir(CORE_COMPONENTS_PATH)
|
||||
if not d.startswith("__")
|
||||
and os.path.isdir(os.path.join(CORE_COMPONENTS_PATH, d))
|
||||
]
|
||||
return dir_names
|
||||
|
||||
|
||||
def get_logger_tags():
|
||||
from esphome.loader import CORE_COMPONENTS_PATH
|
||||
import glob
|
||||
|
||||
pattern = re.compile(r'^static const char(\*\s|\s\*)TAG = "(\w.*)";', re.MULTILINE)
|
||||
tags = [
|
||||
"app",
|
||||
"component",
|
||||
"esphal",
|
||||
"helpers",
|
||||
"preferences",
|
||||
"scheduler",
|
||||
"api.service",
|
||||
]
|
||||
for x in os.walk(CORE_COMPONENTS_PATH):
|
||||
for y in glob.glob(os.path.join(x[0], "*.cpp")):
|
||||
with open(y) as file:
|
||||
data = file.read()
|
||||
match = pattern.search(data)
|
||||
if match:
|
||||
tags.append(match.group(2))
|
||||
return tags
|
||||
|
||||
|
||||
def load_components():
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config import get_component
|
||||
|
||||
modules["cv"] = cv
|
||||
from esphome import automation
|
||||
|
||||
modules["automation"] = automation
|
||||
|
||||
for domain in get_dirs():
|
||||
components[domain] = get_component(domain)
|
||||
modules[domain] = components[domain].module
|
||||
|
||||
|
||||
def add_components():
|
||||
from esphome.config import get_platform
|
||||
|
||||
for domain, c in components.items():
|
||||
if c.is_platform_component:
|
||||
# this is a platform_component, e.g. binary_sensor
|
||||
platform_schema = [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"platform": {"type": "string"}},
|
||||
}
|
||||
]
|
||||
if domain not in ("output", "display"):
|
||||
# output bases are either FLOAT or BINARY so don't add common base for this
|
||||
# display bases are either simple or FULL so don't add common base for this
|
||||
platform_schema = [
|
||||
{"$ref": f"#/definitions/{domain}.{domain.upper()}_SCHEMA"}
|
||||
] + platform_schema
|
||||
|
||||
base_props[domain] = {"type": "array", "items": {"allOf": platform_schema}}
|
||||
|
||||
add_module_registries(domain, c.module)
|
||||
add_module_schemas(domain, c.module)
|
||||
|
||||
# need first to iterate all platforms then iterate components
|
||||
# a platform component can have other components as properties,
|
||||
# e.g. climate components usually have a temperature sensor
|
||||
|
||||
for domain, c in components.items():
|
||||
if (c.config_schema is not None) or c.is_platform_component:
|
||||
if c.is_platform_component:
|
||||
platform_schema = base_props[domain]["items"]["allOf"]
|
||||
for platform in get_dirs():
|
||||
p = get_platform(domain, platform)
|
||||
if p is not None:
|
||||
# this is a platform element, e.g.
|
||||
# - platform: gpio
|
||||
schema = get_jschema(
|
||||
domain + "-" + platform,
|
||||
p.config_schema,
|
||||
create_return_ref=False,
|
||||
)
|
||||
if (
|
||||
schema
|
||||
): # for invalid schemas, None is returned thus is deprecated
|
||||
platform_schema.append(
|
||||
{
|
||||
"if": {
|
||||
JSC_PROPERTIES: {
|
||||
"platform": {"const": platform}
|
||||
}
|
||||
},
|
||||
"then": schema,
|
||||
}
|
||||
)
|
||||
|
||||
elif c.config_schema is not None:
|
||||
# adds root components which are not platforms, e.g. api: logger:
|
||||
if c.multi_conf:
|
||||
schema = get_jschema(domain, c.config_schema)
|
||||
schema = add_definition_array_or_single_object(schema)
|
||||
else:
|
||||
schema = get_jschema(domain, c.config_schema, False)
|
||||
base_props[domain] = schema
|
||||
|
||||
|
||||
def get_automation_schema(name, vschema):
|
||||
from esphome.automation import AUTOMATION_SCHEMA
|
||||
|
||||
# ensure SIMPLE_AUTOMATION
|
||||
if SIMPLE_AUTOMATION not in definitions:
|
||||
simple_automation = add_definition_array_or_single_object(get_ref(JSC_ACTION))
|
||||
simple_automation[JSC_ANYOF].append(
|
||||
get_jschema(AUTOMATION_SCHEMA.__module__, AUTOMATION_SCHEMA)
|
||||
)
|
||||
|
||||
definitions[schema_names[str(AUTOMATION_SCHEMA)]][JSC_PROPERTIES][
|
||||
"then"
|
||||
] = add_definition_array_or_single_object(get_ref(JSC_ACTION))
|
||||
definitions[SIMPLE_AUTOMATION] = simple_automation
|
||||
|
||||
extra_vschema = None
|
||||
if AUTOMATION_SCHEMA == ejs.extended_schemas[str(vschema)][0]:
|
||||
extra_vschema = ejs.extended_schemas[str(vschema)][1]
|
||||
|
||||
if not extra_vschema:
|
||||
return get_ref(SIMPLE_AUTOMATION)
|
||||
|
||||
# add then property
|
||||
extra_jschema = get_jschema(name, extra_vschema, False)
|
||||
|
||||
if is_ref(extra_jschema):
|
||||
return extra_jschema
|
||||
|
||||
if not JSC_PROPERTIES in extra_jschema:
|
||||
# these are interval: and exposure_notifications, featuring automations a component
|
||||
extra_jschema[JSC_ALLOF][0][JSC_PROPERTIES][
|
||||
"then"
|
||||
] = add_definition_array_or_single_object(get_ref(JSC_ACTION))
|
||||
ref = create_ref(name, extra_vschema, extra_jschema)
|
||||
return add_definition_array_or_single_object(ref)
|
||||
|
||||
# automations can be either
|
||||
# * a single action,
|
||||
# * an array of action,
|
||||
# * an object with automation's schema and a then key
|
||||
# with again a single action or an array of actions
|
||||
|
||||
if len(extra_jschema[JSC_PROPERTIES]) == 0:
|
||||
return get_ref(SIMPLE_AUTOMATION)
|
||||
|
||||
extra_jschema[JSC_PROPERTIES]["then"] = add_definition_array_or_single_object(
|
||||
get_ref(JSC_ACTION)
|
||||
)
|
||||
# if there is a required element in extra_jschema then this automation does not support
|
||||
# directly a list of actions
|
||||
if JSC_REQUIRED in extra_jschema:
|
||||
return create_ref(name, extra_vschema, extra_jschema)
|
||||
|
||||
jschema = add_definition_array_or_single_object(get_ref(JSC_ACTION))
|
||||
jschema[JSC_ANYOF].append(extra_jschema)
|
||||
|
||||
return create_ref(name, extra_vschema, jschema)
|
||||
|
||||
|
||||
def get_entry(parent_key, vschema):
|
||||
from esphome.voluptuous_schema import _Schema as schema_type
|
||||
|
||||
entry = {}
|
||||
# annotate schema validator info
|
||||
if DUMP_COMMENTS:
|
||||
entry[JSC_COMMENT] = "entry: " + parent_key + "/" + str(vschema)
|
||||
|
||||
if isinstance(vschema, dict):
|
||||
entry = {"what": "is_this"}
|
||||
elif isinstance(vschema, list):
|
||||
ref = get_jschema(parent_key + "[]", vschema[0])
|
||||
entry = {"type": "array", "items": ref}
|
||||
elif isinstance(vschema, schema_type) and hasattr(vschema, "schema"):
|
||||
entry = get_jschema(parent_key, vschema, False)
|
||||
elif hasattr(vschema, "validators"):
|
||||
entry = get_jschema(parent_key, vschema, False)
|
||||
elif vschema in schema_registry:
|
||||
entry = schema_registry[vschema].copy()
|
||||
elif str(vschema) in ejs.registry_schemas:
|
||||
entry = get_registry_ref(ejs.registry_schemas[str(vschema)])
|
||||
elif str(vschema) in ejs.list_schemas:
|
||||
ref = get_jschema(parent_key, ejs.list_schemas[str(vschema)][0])
|
||||
entry = {JSC_ANYOF: [ref, {"type": "array", "items": ref}]}
|
||||
elif str(vschema) in ejs.typed_schemas:
|
||||
schema_types = [{"type": "object", "properties": {"type": {"type": "string"}}}]
|
||||
entry = {"allOf": schema_types}
|
||||
for schema_key, vschema_type in ejs.typed_schemas[str(vschema)][0][0].items():
|
||||
schema_types.append(
|
||||
{
|
||||
"if": {"properties": {"type": {"const": schema_key}}},
|
||||
"then": get_jschema(f"{parent_key}-{schema_key}", vschema_type),
|
||||
}
|
||||
)
|
||||
|
||||
elif str(vschema) in ejs.hidden_schemas:
|
||||
# get the schema from the automation schema
|
||||
type = ejs.hidden_schemas[str(vschema)]
|
||||
inner_vschema = vschema(ejs.jschema_extractor)
|
||||
if type == "automation":
|
||||
entry = get_automation_schema(parent_key, inner_vschema)
|
||||
elif type == "maybe":
|
||||
entry = get_jschema(parent_key, inner_vschema)
|
||||
elif type == "one_of":
|
||||
entry = {"enum": list(inner_vschema)}
|
||||
elif type == "enum":
|
||||
entry = {"enum": list(inner_vschema.keys())}
|
||||
elif type == "effects":
|
||||
# Like list schema but subset from list.
|
||||
subset_list = inner_vschema[0]
|
||||
# get_jschema('strobex', registry['strobe'].schema)
|
||||
registry_schemas = []
|
||||
for name in subset_list:
|
||||
registry_schemas.append(get_ref("light.EFFECTS_REGISTRY-" + name))
|
||||
|
||||
entry = {
|
||||
JSC_ANYOF: [{"type": "array", "items": {JSC_ANYOF: registry_schemas}}]
|
||||
}
|
||||
|
||||
else:
|
||||
raise ValueError("Unknown extracted schema type")
|
||||
elif str(vschema).startswith("<function invalid."):
|
||||
# deprecated options, don't list as valid schema
|
||||
return None
|
||||
else:
|
||||
# everything else just accept string and let ESPHome validate
|
||||
try:
|
||||
from esphome.core import ID
|
||||
from esphome.automation import Trigger, Automation
|
||||
|
||||
v = vschema(None)
|
||||
if isinstance(v, ID):
|
||||
if (
|
||||
v.type.base != "script::Script"
|
||||
and v.type.base != "switch_::Switch"
|
||||
and (v.type.inherits_from(Trigger) or v.type == Automation)
|
||||
):
|
||||
return None
|
||||
entry = {"type": "string", "id_type": v.type.base}
|
||||
elif isinstance(v, str):
|
||||
entry = {"type": "string"}
|
||||
elif isinstance(v, list):
|
||||
entry = {"type": "array"}
|
||||
else:
|
||||
entry = default_schema()
|
||||
except:
|
||||
entry = default_schema()
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
def default_schema():
|
||||
# Accept anything
|
||||
return {"type": ["null", "object", "string", "array", "number"]}
|
||||
|
||||
|
||||
def is_default_schema(jschema):
|
||||
if jschema is None:
|
||||
return False
|
||||
if is_ref(jschema):
|
||||
jschema = unref(jschema)
|
||||
if not jschema:
|
||||
return False
|
||||
return is_default_schema(jschema)
|
||||
return "type" in jschema and jschema["type"] == default_schema()["type"]
|
||||
|
||||
|
||||
def get_jschema(path, vschema, create_return_ref=True):
|
||||
name = schema_names.get(get_schema_str(vschema))
|
||||
if name:
|
||||
return get_ref(name)
|
||||
|
||||
jschema = convert_schema(path, vschema)
|
||||
|
||||
if jschema is None:
|
||||
return None
|
||||
|
||||
if is_ref(jschema):
|
||||
# this can happen when returned extended
|
||||
# schemas where all properties found in previous extended schema
|
||||
return jschema
|
||||
|
||||
if not create_return_ref:
|
||||
return jschema
|
||||
|
||||
return create_ref(path, vschema, jschema)
|
||||
|
||||
|
||||
def get_schema_str(vschema):
|
||||
# Hack on cs.use_id, in the future this can be improved by tracking which type is required by
|
||||
# the id, this information can be added somehow to schema (not supported by jsonschema) and
|
||||
# completion can be improved listing valid ids only Meanwhile it's a problem because it makes
|
||||
# all partial schemas with cv.use_id different, e.g. i2c
|
||||
|
||||
return re.sub(
|
||||
pattern="function use_id.<locals>.validator at 0[xX][0-9a-fA-F]+>",
|
||||
repl="function use_id.<locals>.validator<>",
|
||||
string=str(vschema),
|
||||
)
|
||||
|
||||
|
||||
def create_ref(name, vschema, jschema):
|
||||
if jschema is None:
|
||||
raise ValueError("Cannot create a ref with null jschema for " + name)
|
||||
|
||||
if name in schema_names:
|
||||
raise ValueError("Not supported")
|
||||
|
||||
schema_str = get_schema_str(vschema)
|
||||
|
||||
schema_names[schema_str] = name
|
||||
definitions[name] = jschema
|
||||
return get_ref(name)
|
||||
|
||||
|
||||
def get_all_properties(jschema):
|
||||
if JSC_PROPERTIES in jschema:
|
||||
return list(jschema[JSC_PROPERTIES].keys())
|
||||
if is_ref(jschema):
|
||||
return get_all_properties(unref(jschema))
|
||||
arr = jschema.get(JSC_ALLOF, jschema.get(JSC_ANYOF))
|
||||
props = []
|
||||
for x in arr:
|
||||
props = props + get_all_properties(x)
|
||||
|
||||
return props
|
||||
|
||||
|
||||
def merge(arr, element):
|
||||
# arr is an array of dicts, dicts can have keys like, properties, $ref, required:[], etc
|
||||
# element is a single dict which might have several keys to
|
||||
# the result should be an array with only one element containing properties, required, etc
|
||||
# and other elements for needed $ref elements
|
||||
# NOTE: json schema supports allof with properties in different elements, but that makes
|
||||
# complex for later adding docs to the schema
|
||||
for k, v in element.items():
|
||||
if k == JSC_PROPERTIES:
|
||||
props_found = False
|
||||
for a_dict in arr:
|
||||
if JSC_PROPERTIES in a_dict:
|
||||
# found properties
|
||||
arr_props = a_dict[JSC_PROPERTIES]
|
||||
for v_k, v_v in v.items():
|
||||
arr_props[v_k] = v_v # add or overwrite
|
||||
props_found = True
|
||||
if not props_found:
|
||||
arr.append(element)
|
||||
elif k == JSC_REF:
|
||||
ref_found = False
|
||||
for a_dict in arr:
|
||||
if k in a_dict and a_dict[k] == v:
|
||||
ref_found = True
|
||||
continue
|
||||
if not ref_found:
|
||||
arr.append(element)
|
||||
else:
|
||||
# TODO: Required might require special handling
|
||||
pass
|
||||
|
||||
|
||||
def convert_schema(path, vschema, un_extend=True):
|
||||
import esphome.config_validation as cv
|
||||
|
||||
# analyze input key, if it is not a Required or Optional, then it is an array
|
||||
output = {}
|
||||
|
||||
if str(vschema) in ejs.hidden_schemas:
|
||||
if ejs.hidden_schemas[str(vschema)] == "automation":
|
||||
vschema = vschema(ejs.jschema_extractor)
|
||||
jschema = get_jschema(path, vschema, True)
|
||||
return add_definition_array_or_single_object(jschema)
|
||||
else:
|
||||
vschema = vschema(ejs.jschema_extractor)
|
||||
|
||||
if un_extend:
|
||||
extended = ejs.extended_schemas.get(str(vschema))
|
||||
if extended:
|
||||
lhs = get_jschema(path, extended[0], False)
|
||||
|
||||
# The midea actions are extending an empty schema (resulted in the templatize not templatizing anything)
|
||||
# this causes a recursion in that this extended looks the same in extended schema as the extended[1]
|
||||
if ejs.extended_schemas.get(str(vschema)) == ejs.extended_schemas.get(
|
||||
str(extended[1])
|
||||
):
|
||||
assert path.startswith("midea_ac")
|
||||
return convert_schema(path, extended[1], False)
|
||||
|
||||
rhs = get_jschema(path, extended[1], False)
|
||||
|
||||
# check if we are not merging properties which are already in base component
|
||||
lprops = get_all_properties(lhs)
|
||||
rprops = get_all_properties(rhs)
|
||||
|
||||
if all(item in lprops for item in rprops):
|
||||
return lhs
|
||||
if all(item in rprops for item in lprops):
|
||||
return rhs
|
||||
|
||||
# merge
|
||||
if JSC_ALLOF in lhs and JSC_ALLOF in rhs:
|
||||
output = lhs
|
||||
for k in rhs[JSC_ALLOF]:
|
||||
merge(output[JSC_ALLOF], k)
|
||||
elif JSC_ALLOF in lhs:
|
||||
output = lhs
|
||||
merge(output[JSC_ALLOF], rhs)
|
||||
elif JSC_ALLOF in rhs:
|
||||
output = rhs
|
||||
merge(output[JSC_ALLOF], lhs)
|
||||
else:
|
||||
output = {JSC_ALLOF: [lhs]}
|
||||
merge(output[JSC_ALLOF], rhs)
|
||||
|
||||
return output
|
||||
|
||||
# When schema contains all, all also has a schema which points
|
||||
# back to the containing schema
|
||||
|
||||
if isinstance(vschema, MockObj):
|
||||
return output
|
||||
|
||||
while hasattr(vschema, "schema") and not hasattr(vschema, "validators"):
|
||||
vschema = vschema.schema
|
||||
|
||||
if hasattr(vschema, "validators"):
|
||||
output = default_schema()
|
||||
for v in vschema.validators:
|
||||
if v:
|
||||
# we should take the valid schema,
|
||||
# commonly all is used to validate a schema, and then a function which
|
||||
# is not a schema es also given, get_schema will then return a default_schema()
|
||||
if v == dict:
|
||||
continue # this is a dict in the SCHEMA of packages
|
||||
val_schema = get_jschema(path, v, False)
|
||||
if is_default_schema(val_schema):
|
||||
if not output:
|
||||
output = val_schema
|
||||
else:
|
||||
if is_default_schema(output):
|
||||
output = val_schema
|
||||
else:
|
||||
output = {**output, **val_schema}
|
||||
return output
|
||||
|
||||
if not vschema:
|
||||
return output
|
||||
|
||||
if not hasattr(vschema, "keys"):
|
||||
return get_entry(path, vschema)
|
||||
|
||||
key = list(vschema.keys())[0]
|
||||
|
||||
# used for platformio_options in core_config
|
||||
# pylint: disable=comparison-with-callable
|
||||
if key == cv.string_strict:
|
||||
output["type"] = "object"
|
||||
return output
|
||||
|
||||
props = output[JSC_PROPERTIES] = {}
|
||||
required = []
|
||||
|
||||
output["type"] = ["object", "null"]
|
||||
if DUMP_COMMENTS:
|
||||
output[JSC_COMMENT] = "converted: " + path + "/" + str(vschema)
|
||||
|
||||
if path == "logger-logs":
|
||||
tags = get_logger_tags()
|
||||
for k in tags:
|
||||
props[k] = {
|
||||
"enum": [
|
||||
"NONE",
|
||||
"ERROR",
|
||||
"WARN",
|
||||
"INFO",
|
||||
"DEBUG",
|
||||
"VERBOSE",
|
||||
"VERY_VERBOSE",
|
||||
]
|
||||
}
|
||||
|
||||
else:
|
||||
for k in vschema:
|
||||
if str(k).startswith("<function"):
|
||||
# generate all logger tags
|
||||
|
||||
# TODO handle key functions
|
||||
|
||||
continue
|
||||
|
||||
v = vschema[k]
|
||||
prop = {}
|
||||
|
||||
if isinstance(v, vol.Schema):
|
||||
prop = get_jschema(path + "-" + str(k), v.schema)
|
||||
elif hasattr(v, "validators"):
|
||||
prop = convert_schema(path + "-" + str(k), v, False)
|
||||
else:
|
||||
prop = get_entry(path + "-" + str(k), v)
|
||||
|
||||
if prop: # Deprecated (cv.Invalid) properties not added
|
||||
props[str(k)] = prop
|
||||
# TODO: see required, sometimes completions doesn't show up because of this...
|
||||
if isinstance(k, cv.Required):
|
||||
required.append(str(k))
|
||||
try:
|
||||
if str(k.default) != "...":
|
||||
default_value = k.default()
|
||||
# Yaml validator fails if `"default": null` ends up in the json schema
|
||||
if default_value is not None:
|
||||
if prop["type"] == "string":
|
||||
default_value = str(default_value)
|
||||
prop["default"] = default_value
|
||||
except:
|
||||
pass
|
||||
|
||||
if len(required) > 0:
|
||||
output[JSC_REQUIRED] = required
|
||||
return output
|
||||
|
||||
|
||||
def add_pin_schema():
|
||||
from esphome import pins
|
||||
|
||||
add_module_schemas("PIN", pins)
|
||||
|
||||
|
||||
def add_pin_registry():
|
||||
from esphome import pins
|
||||
|
||||
pin_registry = pins.PIN_SCHEMA_REGISTRY
|
||||
assert len(pin_registry) > 0
|
||||
# Here are schemas for pcf8574, mcp23xxx and other port expanders which add
|
||||
# gpio registers
|
||||
# ESPHome validates pins schemas if it founds a key in the pin configuration.
|
||||
# This key is added to a required in jsonschema, and all options are part of a
|
||||
# oneOf section, so only one is selected. Also internal schema adds number as required.
|
||||
|
||||
for mode in ("INPUT", "OUTPUT"):
|
||||
schema_name = f"PIN.GPIO_FULL_{mode}_PIN_SCHEMA"
|
||||
|
||||
# TODO: get pin definitions properly
|
||||
if schema_name not in definitions:
|
||||
definitions[schema_name] = {"type": ["object", "null"], JSC_PROPERTIES: {}}
|
||||
|
||||
internal = definitions[schema_name]
|
||||
definitions[schema_name]["additionalItems"] = False
|
||||
definitions[f"PIN.{mode}_INTERNAL"] = internal
|
||||
internal[JSC_PROPERTIES]["number"] = {"type": ["number", "string"]}
|
||||
schemas = [get_ref(f"PIN.{mode}_INTERNAL")]
|
||||
schemas[0]["required"] = ["number"]
|
||||
# accept string and object, for internal shorthand pin IO:
|
||||
definitions[schema_name] = {"oneOf": schemas, "type": ["string", "object"]}
|
||||
|
||||
for k, v in pin_registry.items():
|
||||
if isinstance(v[1], vol.validators.All):
|
||||
pin_jschema = get_jschema(f"PIN.{mode}_" + k, v[1])
|
||||
if unref(pin_jschema):
|
||||
pin_jschema["required"] = [k]
|
||||
schemas.append(pin_jschema)
|
||||
|
||||
|
||||
def dump_schema():
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import validate_potentially_and_condition
|
||||
from esphome import pins
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import write_file_if_changed
|
||||
from esphome.components import remote_base
|
||||
|
||||
# The root directory of the repo
|
||||
root = Path(__file__).parent.parent
|
||||
|
||||
# Fake some directory so that get_component works
|
||||
CORE.config_path = str(root)
|
||||
|
||||
file_path = args.output
|
||||
|
||||
schema_registry[cv.boolean] = {"type": "boolean"}
|
||||
|
||||
for v in [
|
||||
cv.int_,
|
||||
cv.int_range,
|
||||
cv.positive_int,
|
||||
cv.float_,
|
||||
cv.positive_float,
|
||||
cv.positive_float,
|
||||
cv.positive_not_null_int,
|
||||
cv.negative_one_to_one_float,
|
||||
cv.port,
|
||||
]:
|
||||
schema_registry[v] = {"type": "number"}
|
||||
|
||||
for v in [
|
||||
cv.string,
|
||||
cv.string_strict,
|
||||
cv.valid_name,
|
||||
cv.hex_int,
|
||||
cv.hex_int_range,
|
||||
pins.gpio_output_pin_schema,
|
||||
pins.gpio_input_pin_schema,
|
||||
pins.gpio_input_pullup_pin_schema,
|
||||
cv.float_with_unit,
|
||||
cv.subscribe_topic,
|
||||
cv.publish_topic,
|
||||
cv.mqtt_payload,
|
||||
cv.ssid,
|
||||
cv.percentage_int,
|
||||
cv.percentage,
|
||||
cv.possibly_negative_percentage,
|
||||
cv.positive_time_period,
|
||||
cv.positive_time_period_microseconds,
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.positive_time_period_minutes,
|
||||
cv.positive_time_period_seconds,
|
||||
]:
|
||||
schema_registry[v] = {"type": "string"}
|
||||
|
||||
schema_registry[validate_potentially_and_condition] = get_ref("condition_list")
|
||||
|
||||
for v in [pins.gpio_input_pin_schema, pins.gpio_input_pullup_pin_schema]:
|
||||
schema_registry[v] = get_ref("PIN.GPIO_FULL_INPUT_PIN_SCHEMA")
|
||||
for v in [pins.internal_gpio_input_pin_schema, pins.gpio_input_pin_schema]:
|
||||
schema_registry[v] = get_ref("PIN.INPUT_INTERNAL")
|
||||
|
||||
for v in [pins.gpio_output_pin_schema, pins.internal_gpio_output_pin_schema]:
|
||||
schema_registry[v] = get_ref("PIN.GPIO_FULL_OUTPUT_PIN_SCHEMA")
|
||||
for v in [pins.internal_gpio_output_pin_schema, pins.gpio_output_pin_schema]:
|
||||
schema_registry[v] = get_ref("PIN.OUTPUT_INTERNAL")
|
||||
|
||||
add_module_schemas("CONFIG", cv)
|
||||
get_jschema("POLLING_COMPONENT", cv.polling_component_schema("60s"))
|
||||
|
||||
add_pin_schema()
|
||||
|
||||
add_module_schemas("REMOTE_BASE", remote_base)
|
||||
add_module_schemas("AUTOMATION", automation)
|
||||
|
||||
load_components()
|
||||
add_registries()
|
||||
|
||||
definitions["condition_list"] = {
|
||||
JSC_ONEOF: [
|
||||
{"type": "array", "items": get_ref(JSC_CONDITION)},
|
||||
get_ref(JSC_CONDITION),
|
||||
]
|
||||
}
|
||||
|
||||
output = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"definitions": definitions,
|
||||
JSC_PROPERTIES: base_props,
|
||||
}
|
||||
|
||||
add_core()
|
||||
add_buses()
|
||||
add_components()
|
||||
|
||||
add_registries() # need second pass, e.g. climate.pid.autotune
|
||||
add_pin_registry()
|
||||
solve_pending_refs()
|
||||
|
||||
write_file_if_changed(file_path, json.dumps(output))
|
||||
print(f"Wrote {file_path}")
|
||||
|
||||
|
||||
dump_schema()
|
@ -1,18 +1,19 @@
|
||||
import inspect
|
||||
import json
|
||||
import argparse
|
||||
from operator import truediv
|
||||
import os
|
||||
import glob
|
||||
import re
|
||||
import voluptuous as vol
|
||||
|
||||
# NOTE: Cannot import other esphome components globally as a modification in jsonschema
|
||||
# NOTE: Cannot import other esphome components globally as a modification in vol_schema
|
||||
# is needed before modules are loaded
|
||||
import esphome.jsonschema as ejs
|
||||
import esphome.schema_extractors as ejs
|
||||
|
||||
ejs.EnableJsonSchemaCollect = True
|
||||
ejs.EnableSchemaExtraction = True
|
||||
|
||||
# schema format:
|
||||
# Schemas are splitted in several files in json format, one for core stuff, one for each platform (sensor, binary_sensor, etc) and
|
||||
# Schemas are split in several files in json format, one for core stuff, one for each platform (sensor, binary_sensor, etc) and
|
||||
# one for each component (dallas, sim800l, etc.) component can have schema for root component/hub and also for platform component,
|
||||
# e.g. dallas has hub component which has pin and then has the sensor platform which has sensor name, index, etc.
|
||||
# When files are loaded they are merged in a single object.
|
||||
@ -60,15 +61,6 @@ solve_registry = []
|
||||
|
||||
|
||||
def get_component_names():
|
||||
# return [
|
||||
# "esphome",
|
||||
# "esp32",
|
||||
# "esp8266",
|
||||
# "logger",
|
||||
# "sensor",
|
||||
# "remote_receiver",
|
||||
# "binary_sensor",
|
||||
# ]
|
||||
from esphome.loader import CORE_COMPONENTS_PATH
|
||||
|
||||
component_names = ["esphome", "sensor"]
|
||||
@ -100,7 +92,7 @@ from esphome import automation
|
||||
from esphome import pins
|
||||
from esphome.components import remote_base
|
||||
from esphome.const import CONF_TYPE
|
||||
from esphome.loader import get_platform
|
||||
from esphome.loader import get_platform, CORE_COMPONENTS_PATH
|
||||
from esphome.helpers import write_file_if_changed
|
||||
from esphome.util import Registry
|
||||
|
||||
@ -120,10 +112,12 @@ def write_file(name, obj):
|
||||
def register_module_schemas(key, module, manifest=None):
|
||||
for name, schema in module_schemas(module):
|
||||
register_known_schema(key, name, schema)
|
||||
if (
|
||||
manifest and manifest.multi_conf and S_CONFIG_SCHEMA in output[key][S_SCHEMAS]
|
||||
): # not sure about 2nd part of the if, might be useless config (e.g. as3935)
|
||||
output[key][S_SCHEMAS][S_CONFIG_SCHEMA]["is_list"] = True
|
||||
|
||||
if manifest:
|
||||
# Multi conf should allow list of components
|
||||
# not sure about 2nd part of the if, might be useless config (e.g. as3935)
|
||||
if manifest.multi_conf and S_CONFIG_SCHEMA in output[key][S_SCHEMAS]:
|
||||
output[key][S_SCHEMAS][S_CONFIG_SCHEMA]["is_list"] = True
|
||||
|
||||
|
||||
def register_known_schema(module, name, schema):
|
||||
@ -265,13 +259,58 @@ def do_esp8266():
|
||||
|
||||
|
||||
def fix_remote_receiver():
|
||||
output["remote_receiver.binary_sensor"]["schemas"]["CONFIG_SCHEMA"] = {
|
||||
remote_receiver_schema = output["remote_receiver.binary_sensor"]["schemas"]
|
||||
remote_receiver_schema["CONFIG_SCHEMA"] = {
|
||||
"type": "schema",
|
||||
"schema": {
|
||||
"extends": ["binary_sensor.BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"],
|
||||
"config_vars": output["remote_base"]["binary"],
|
||||
"config_vars": output["remote_base"].pop("binary"),
|
||||
},
|
||||
}
|
||||
remote_receiver_schema["CONFIG_SCHEMA"]["schema"]["config_vars"]["receiver_id"] = {
|
||||
"key": "GeneratedID",
|
||||
"use_id_type": "remote_base::RemoteReceiverBase",
|
||||
"type": "use_id",
|
||||
}
|
||||
|
||||
|
||||
def fix_script():
|
||||
output["script"][S_SCHEMAS][S_CONFIG_SCHEMA][S_TYPE] = S_SCHEMA
|
||||
config_schema = output["script"][S_SCHEMAS][S_CONFIG_SCHEMA]
|
||||
config_schema[S_SCHEMA][S_CONFIG_VARS]["id"]["id_type"] = {
|
||||
"class": "script::Script"
|
||||
}
|
||||
config_schema["is_list"] = True
|
||||
|
||||
|
||||
def get_logger_tags():
|
||||
pattern = re.compile(r'^static const char \*const TAG = "(\w.*)";', re.MULTILINE)
|
||||
# tags not in components dir
|
||||
tags = [
|
||||
"app",
|
||||
"component",
|
||||
"entity_base",
|
||||
"scheduler",
|
||||
"api.service",
|
||||
]
|
||||
for x in os.walk(CORE_COMPONENTS_PATH):
|
||||
for y in glob.glob(os.path.join(x[0], "*.cpp")):
|
||||
with open(y, encoding="utf-8") as file:
|
||||
data = file.read()
|
||||
match = pattern.search(data)
|
||||
if match:
|
||||
tags.append(match.group(1))
|
||||
return tags
|
||||
|
||||
|
||||
def add_logger_tags():
|
||||
tags = get_logger_tags()
|
||||
logs = output["logger"]["schemas"]["CONFIG_SCHEMA"]["schema"]["config_vars"][
|
||||
"logs"
|
||||
]["schema"]["config_vars"]
|
||||
for t in tags:
|
||||
logs[t] = logs["string"].copy()
|
||||
logs.pop("string")
|
||||
|
||||
|
||||
def add_referenced_recursive(referenced_schemas, config_var, path, eat_schema=False):
|
||||
@ -401,7 +440,7 @@ def shrink():
|
||||
else:
|
||||
print("expected extends here!" + x)
|
||||
arr_s = merge(key_s, arr_s)
|
||||
if arr_s[S_TYPE] == "enum":
|
||||
if arr_s[S_TYPE] in ["enum", "typed"]:
|
||||
arr_s.pop(S_SCHEMA)
|
||||
else:
|
||||
arr_s.pop(S_EXTENDS)
|
||||
@ -491,14 +530,20 @@ def build_schema():
|
||||
if domain not in platforms:
|
||||
if manifest.config_schema is not None:
|
||||
core_components[domain] = {}
|
||||
if len(manifest.dependencies) > 0:
|
||||
core_components[domain]["dependencies"] = manifest.dependencies
|
||||
register_module_schemas(domain, manifest.module, manifest)
|
||||
|
||||
for platform in platforms:
|
||||
platform_manifest = get_platform(domain=platform, platform=domain)
|
||||
if platform_manifest is not None:
|
||||
output[platform][S_COMPONENTS][domain] = {}
|
||||
if len(platform_manifest.dependencies) > 0:
|
||||
output[platform][S_COMPONENTS][domain][
|
||||
"dependencies"
|
||||
] = platform_manifest.dependencies
|
||||
register_module_schemas(
|
||||
f"{domain}.{platform}", platform_manifest.module
|
||||
f"{domain}.{platform}", platform_manifest.module, platform_manifest
|
||||
)
|
||||
|
||||
# Do registries
|
||||
@ -517,6 +562,8 @@ def build_schema():
|
||||
do_esp8266()
|
||||
do_esp32()
|
||||
fix_remote_receiver()
|
||||
fix_script()
|
||||
add_logger_tags()
|
||||
shrink()
|
||||
|
||||
# aggregate components, so all component info is in same file, otherwise we have dallas.json, dallas.sensor.json, etc.
|
||||
@ -585,7 +632,7 @@ def convert_1(schema, config_var, path):
|
||||
assert S_EXTENDS not in config_var
|
||||
if not S_TYPE in config_var:
|
||||
config_var[S_TYPE] = S_SCHEMA
|
||||
assert config_var[S_TYPE] == S_SCHEMA
|
||||
# assert config_var[S_TYPE] == S_SCHEMA
|
||||
|
||||
if S_SCHEMA not in config_var:
|
||||
config_var[S_SCHEMA] = {}
|
||||
@ -662,7 +709,7 @@ def convert_1(schema, config_var, path):
|
||||
elif repr_schema in ejs.hidden_schemas:
|
||||
schema_type = ejs.hidden_schemas[repr_schema]
|
||||
|
||||
data = schema(ejs.jschema_extractor)
|
||||
data = schema(ejs.SCHEMA_EXTRACT)
|
||||
|
||||
# enums, e.g. esp32/variant
|
||||
if schema_type == "one_of":
|
||||
@ -672,8 +719,9 @@ def convert_1(schema, config_var, path):
|
||||
config_var[S_TYPE] = "enum"
|
||||
config_var["values"] = list(data.keys())
|
||||
elif schema_type == "maybe":
|
||||
config_var[S_TYPE] = "maybe"
|
||||
config_var["schema"] = convert_config(data, path + "/maybe")["schema"]
|
||||
config_var[S_TYPE] = S_SCHEMA
|
||||
config_var["maybe"] = data[1]
|
||||
config_var["schema"] = convert_config(data[0], path + "/maybe")["schema"]
|
||||
# esphome/on_boot
|
||||
elif schema_type == "automation":
|
||||
extra_schema = None
|
||||
@ -717,8 +765,50 @@ def convert_1(schema, config_var, path):
|
||||
elif schema_type == "sensor":
|
||||
schema = data
|
||||
convert_1(data, config_var, path + "/trigger")
|
||||
elif schema_type == "declare_id":
|
||||
# pylint: disable=protected-access
|
||||
parents = data._parents
|
||||
|
||||
config_var["id_type"] = {
|
||||
"class": str(data.base),
|
||||
"parents": [str(x.base) for x in parents]
|
||||
if isinstance(parents, list)
|
||||
else None,
|
||||
}
|
||||
elif schema_type == "use_id":
|
||||
if inspect.ismodule(data):
|
||||
m_attr_obj = getattr(data, "CONFIG_SCHEMA")
|
||||
use_schema = known_schemas.get(repr(m_attr_obj))
|
||||
if use_schema:
|
||||
[output_module, output_name] = use_schema[0][1].split(".")
|
||||
use_id_config = output[output_module][S_SCHEMAS][output_name]
|
||||
config_var["use_id_type"] = use_id_config["schema"]["config_vars"][
|
||||
"id"
|
||||
]["id_type"]["class"]
|
||||
config_var[S_TYPE] = "use_id"
|
||||
else:
|
||||
print("TODO deferred?")
|
||||
else:
|
||||
if isinstance(data, str):
|
||||
# TODO: Figure out why pipsolar does this
|
||||
config_var["use_id_type"] = data
|
||||
else:
|
||||
config_var["use_id_type"] = str(data.base)
|
||||
config_var[S_TYPE] = "use_id"
|
||||
else:
|
||||
raise Exception("Unknown extracted schema type")
|
||||
elif config_var.get("key") == "GeneratedID":
|
||||
if path == "i2c/CONFIG_SCHEMA/extL/all/id":
|
||||
config_var["id_type"] = {"class": "i2c::I2CBus", "parents": ["Component"]}
|
||||
elif path == "uart/CONFIG_SCHEMA/val 1/extL/all/id":
|
||||
config_var["id_type"] = {
|
||||
"class": "uart::UARTComponent",
|
||||
"parents": ["Component"],
|
||||
}
|
||||
elif path == "pins/esp32/val 1/id":
|
||||
config_var["id_type"] = "pin"
|
||||
else:
|
||||
raise Exception("Cannot determine id_type for " + path)
|
||||
|
||||
elif repr_schema in ejs.registry_schemas:
|
||||
solve_registry.append((ejs.registry_schemas[repr_schema], config_var))
|
||||
@ -787,7 +877,13 @@ def convert_keys(converted, schema, path):
|
||||
result["key"] = "Optional"
|
||||
else:
|
||||
converted["key"] = "String"
|
||||
converted["key_dump"] = str(k)
|
||||
key_string_match = re.search(
|
||||
r"<function (\w*) at \w*>", str(k), re.IGNORECASE
|
||||
)
|
||||
if key_string_match:
|
||||
converted["key_type"] = key_string_match.group(1)
|
||||
else:
|
||||
converted["key_type"] = str(k)
|
||||
|
||||
esphome_core.CORE.data = {
|
||||
esphome_core.KEY_CORE: {esphome_core.KEY_TARGET_PLATFORM: "esp8266"}
|
||||
@ -808,6 +904,12 @@ def convert_keys(converted, schema, path):
|
||||
if base_k in result and base_v == result[base_k]:
|
||||
result.pop(base_k)
|
||||
converted["schema"][S_CONFIG_VARS][str(k)] = result
|
||||
if "key" in converted and converted["key"] == "String":
|
||||
config_vars = converted["schema"]["config_vars"]
|
||||
assert len(config_vars) == 1
|
||||
key = list(config_vars.keys())[0]
|
||||
assert key.startswith("<")
|
||||
config_vars["string"] = config_vars.pop(key)
|
||||
|
||||
|
||||
build_schema()
|
||||
|
@ -452,7 +452,7 @@ def lint_no_removed_in_idf_conversions(fname, match):
|
||||
replacement = IDF_CONVERSION_FORBIDDEN[match.group(1)]
|
||||
return (
|
||||
f"The macro {highlight(match.group(1))} can no longer be used in ESPHome directly. "
|
||||
f"Plese use {highlight(replacement)} instead."
|
||||
f"Please use {highlight(replacement)} instead."
|
||||
)
|
||||
|
||||
|
||||
|
@ -168,6 +168,13 @@ mqtt:
|
||||
id: uart0
|
||||
data: !lambda |-
|
||||
return {};
|
||||
on_connect:
|
||||
- light.turn_on: ${roomname}_lights
|
||||
- mqtt.publish:
|
||||
topic: some/topic
|
||||
payload: Hello
|
||||
on_disconnect:
|
||||
- light.turn_off: ${roomname}_lights
|
||||
|
||||
i2c:
|
||||
sda: 21
|
||||
@ -593,7 +600,6 @@ sensor:
|
||||
sensor: hlw8012_power
|
||||
name: "Integration Sensor lazy"
|
||||
time_unit: s
|
||||
min_save_interval: 60s
|
||||
- platform: hmc5883l
|
||||
address: 0x68
|
||||
field_strength_x:
|
||||
@ -1886,6 +1892,7 @@ climate:
|
||||
- platform: bedjet
|
||||
name: My Bedjet
|
||||
ble_client_id: my_bedjet_ble_client
|
||||
heat_mode: extended
|
||||
|
||||
script:
|
||||
- id: climate_custom
|
||||
|
@ -80,6 +80,11 @@ sx1509:
|
||||
mcp3204:
|
||||
cs_pin: GPIO23
|
||||
|
||||
dac7678:
|
||||
address: 0x4A
|
||||
id: dac7678_hub1
|
||||
internal_reference: true
|
||||
|
||||
sensor:
|
||||
- platform: homeassistant
|
||||
entity_id: sensor.hello_world
|
||||
@ -518,6 +523,38 @@ output:
|
||||
pipsolar_id: inverter0
|
||||
battery_recharge_voltage:
|
||||
id: inverter0_battery_recharge_voltage_out
|
||||
- platform: dac7678
|
||||
dac7678_id: 'dac7678_hub1'
|
||||
channel: 0
|
||||
id: 'dac7678_1_ch0'
|
||||
- platform: dac7678
|
||||
dac7678_id: 'dac7678_hub1'
|
||||
channel: 1
|
||||
id: 'dac7678_1_ch1'
|
||||
- platform: dac7678
|
||||
dac7678_id: 'dac7678_hub1'
|
||||
channel: 2
|
||||
id: 'dac7678_1_ch2'
|
||||
- platform: dac7678
|
||||
dac7678_id: 'dac7678_hub1'
|
||||
channel: 3
|
||||
id: 'dac7678_1_ch3'
|
||||
- platform: dac7678
|
||||
dac7678_id: 'dac7678_hub1'
|
||||
channel: 4
|
||||
id: 'dac7678_1_ch4'
|
||||
- platform: dac7678
|
||||
dac7678_id: 'dac7678_hub1'
|
||||
channel: 5
|
||||
id: 'dac7678_1_ch5'
|
||||
- platform: dac7678
|
||||
dac7678_id: 'dac7678_hub1'
|
||||
channel: 6
|
||||
id: 'dac7678_1_ch6'
|
||||
- platform: dac7678
|
||||
dac7678_id: 'dac7678_hub1'
|
||||
channel: 7
|
||||
id: 'dac7678_1_ch7'
|
||||
esp32_camera:
|
||||
name: ESP-32 Camera
|
||||
data_pins: [GPIO17, GPIO35, GPIO34, GPIO5, GPIO39, GPIO18, GPIO36, GPIO19]
|
||||
@ -604,3 +641,29 @@ touchscreen:
|
||||
- logger.log:
|
||||
format: Touch at (%d, %d)
|
||||
args: ["touch.x", "touch.y"]
|
||||
|
||||
media_player:
|
||||
- platform: i2s_audio
|
||||
name: ${friendly_name}
|
||||
dac_type: external
|
||||
i2s_lrclk_pin: GPIO26
|
||||
i2s_dout_pin: GPIO25
|
||||
i2s_bclk_pin: GPIO27
|
||||
mute_pin: GPIO14
|
||||
on_state:
|
||||
- media_player.play:
|
||||
- media_player.play_media: http://localhost/media.mp3
|
||||
- media_player.play_media: !lambda 'return "http://localhost/media.mp3";'
|
||||
on_idle:
|
||||
- media_player.pause:
|
||||
on_play:
|
||||
- media_player.stop:
|
||||
on_pause:
|
||||
- media_player.toggle:
|
||||
- wait_until:
|
||||
media_player.is_idle:
|
||||
- wait_until:
|
||||
media_player.is_playing:
|
||||
- media_player.volume_up:
|
||||
- media_player.volume_down:
|
||||
- media_player.volume_set: 50%
|
||||
|
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