Compare commits
391 Commits
host-targe
...
2022.5.0b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff72d6a146 | ||
|
|
603d0d0c7c | ||
|
|
28883f711b | ||
|
|
e914828add | ||
|
|
c1480029fb | ||
|
|
40f622949e | ||
|
|
63096ac2bc | ||
|
|
c2a59cb476 | ||
|
|
d6e039a1d1 | ||
|
|
0f1a7c2b69 | ||
|
|
40ad9f4911 | ||
|
|
4116caff6a | ||
|
|
0b69f72315 | ||
|
|
c569f5ddcf | ||
|
|
62f9e181e0 | ||
|
|
235a97ea10 | ||
|
|
e541ae400c | ||
|
|
4822abde86 | ||
|
|
b7e52812f8 | ||
|
|
69118120d9 | ||
|
|
7cba0c6fb0 | ||
|
|
5fac67ce15 | ||
|
|
98c733108e | ||
|
|
782186e13d | ||
|
|
4e1f6518e8 | ||
|
|
53e0fe8e51 | ||
|
|
0e547390da | ||
|
|
86b52df839 | ||
|
|
d685fdf54a | ||
|
|
d9caab4108 | ||
|
|
44b68f140e | ||
|
|
3a3d97dfa7 | ||
|
|
47898b527c | ||
|
|
a35f36ad39 | ||
|
|
d13a397f8e | ||
|
|
df999723f8 | ||
|
|
8236e840a7 | ||
|
|
e5b3625f73 | ||
|
|
2e4645310b | ||
|
|
50a32b387e | ||
|
|
2059283707 | ||
|
|
8e3af515c9 | ||
|
|
6f88f0ea3f | ||
|
|
d2f37cf3f9 | ||
|
|
7c30d6254e | ||
|
|
64fb39a653 | ||
|
|
91895aa70c | ||
|
|
68dfaf238b | ||
|
|
ebf13a0ba0 | ||
|
|
2bff9937b7 | ||
|
|
256395c28d | ||
|
|
3346bc8bba | ||
|
|
6fe22a7e62 | ||
|
|
757b98748b | ||
|
|
7a778f3f33 | ||
|
|
41d9059a2f | ||
|
|
e26e0d7c01 | ||
|
|
ad41c07a1f | ||
|
|
9576d246ee | ||
|
|
988d3ea8ba | ||
|
|
0767b92b62 | ||
|
|
5732f3b044 | ||
|
|
712115b6ce | ||
|
|
9283559c6b | ||
|
|
6b393438e9 | ||
|
|
2064abe16d | ||
|
|
b605982f94 | ||
|
|
343b9ab455 | ||
|
|
dcb226b202 | ||
|
|
2243021b58 | ||
|
|
d5134e88b1 | ||
|
|
c59adf612f | ||
|
|
93b628d9a8 | ||
|
|
6bac551d9f | ||
|
|
70a35656e4 | ||
|
|
047c18eac0 | ||
|
|
b4a86ce6cf | ||
|
|
a82d8ea0c3 | ||
|
|
b778eed419 | ||
|
|
ad57faa9a9 | ||
|
|
a9b5e8d036 | ||
|
|
8be704e591 | ||
|
|
b622a8fa58 | ||
|
|
a519e5c475 | ||
|
|
d620b6dd5e | ||
|
|
99335d986e | ||
|
|
7895cd92cd | ||
|
|
8b2c032da6 | ||
|
|
da336247eb | ||
|
|
dabd27d4be | ||
|
|
fdda47db6e | ||
|
|
efa6fd03e5 | ||
|
|
9e3e34acf5 | ||
|
|
a2d0c1bf18 | ||
|
|
7663716ae8 | ||
|
|
c2cacb3478 | ||
|
|
84666b54b9 | ||
|
|
2b91c23bf3 | ||
|
|
3297267a16 | ||
|
|
a9e653724c | ||
|
|
5e79a1f500 | ||
|
|
d4ff98680a | ||
|
|
ba8d255cb4 | ||
|
|
06f4ad922c | ||
|
|
bff06e448b | ||
|
|
d48ffa2913 | ||
|
|
d97c3a7e01 | ||
|
|
0b1161f7ef | ||
|
|
061e1a471d | ||
|
|
a39d874600 | ||
|
|
de96376565 | ||
|
|
c54c20ab3c | ||
|
|
70fafa473b | ||
|
|
2e436eae6b | ||
|
|
fd7e861ff5 | ||
|
|
792108686c | ||
|
|
fa1b5117fd | ||
|
|
b0bd9e0a34 | ||
|
|
05dc97099a | ||
|
|
9de61fcf58 | ||
|
|
7f7175b184 | ||
|
|
cf5c640ae4 | ||
|
|
6b9371d105 | ||
|
|
9a82057303 | ||
|
|
48584e94c4 | ||
|
|
d8024a5928 | ||
|
|
2034ab4f6c | ||
|
|
58b70b42dd | ||
|
|
1496bc1b07 | ||
|
|
bfbf88b2ea | ||
|
|
e621b938e3 | ||
|
|
59e6e798dd | ||
|
|
e5c2dbc7ec | ||
|
|
756f71c382 | ||
|
|
b7535693fa | ||
|
|
06a3505698 | ||
|
|
0372d17a11 | ||
|
|
4525588116 | ||
|
|
68e957c147 | ||
|
|
99f5ed1461 | ||
|
|
59f67796dc | ||
|
|
aafdfa933e | ||
|
|
3208c8ed1e | ||
|
|
6bf733e24e | ||
|
|
65d3e8fbfc | ||
|
|
a29d65d47c | ||
|
|
efa8f0730d | ||
|
|
0af1edefff | ||
|
|
023d26f521 | ||
|
|
5068619f1b | ||
|
|
5b2457af0b | ||
|
|
900b4f1af9 | ||
|
|
4c22a98b0b | ||
|
|
3b8ca80900 | ||
|
|
b528f48417 | ||
|
|
ec7a79049a | ||
|
|
6ddad6b299 | ||
|
|
16dc7762f9 | ||
|
|
dc0ed8857f | ||
|
|
bb6b77bd98 | ||
|
|
dcc80f9032 | ||
|
|
dd554bcdf4 | ||
|
|
f376a39e55 | ||
|
|
8dcc9d6b66 | ||
|
|
a576c9f21f | ||
|
|
71a438e2cb | ||
|
|
272d6f2a8b | ||
|
|
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 |
@@ -2,7 +2,7 @@
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/ambv/black
|
||||
rev: 22.1.0
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
@@ -26,7 +26,7 @@ repos:
|
||||
- --branch=release
|
||||
- --branch=beta
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.31.0
|
||||
rev: v2.31.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
|
||||
15
CODEOWNERS
15
CODEOWNERS
@@ -28,8 +28,10 @@ esphome/components/atc_mithermometer/* @ahpohl
|
||||
esphome/components/b_parasite/* @rbaron
|
||||
esphome/components/ballu/* @bazuchan
|
||||
esphome/components/bang_bang/* @OttoWinter
|
||||
esphome/components/bedjet/* @jhansche
|
||||
esphome/components/bh1750/* @OttoWinter
|
||||
esphome/components/binary_sensor/* @esphome/core
|
||||
esphome/components/bl0939/* @ziceva
|
||||
esphome/components/bl0940/* @tobias-
|
||||
esphome/components/ble_client/* @buxtronix
|
||||
esphome/components/bme680_bsec/* @trvrnrth
|
||||
@@ -53,11 +55,13 @@ esphome/components/current_based/* @djwmarcx
|
||||
esphome/components/daly_bms/* @s1lvi0
|
||||
esphome/components/dashboard_import/* @esphome/core
|
||||
esphome/components/debug/* @OttoWinter
|
||||
esphome/components/delonghi/* @grob6000
|
||||
esphome/components/dfplayer/* @glmnet
|
||||
esphome/components/dht/* @OttoWinter
|
||||
esphome/components/ds1307/* @badbadc0ffee
|
||||
esphome/components/dsmr/* @glmnet @zuidwijk
|
||||
esphome/components/ektf2232/* @jesserockz
|
||||
esphome/components/ens210/* @itn3rd77
|
||||
esphome/components/esp32/* @esphome/core
|
||||
esphome/components/esp32_ble/* @jesserockz
|
||||
esphome/components/esp32_ble_server/* @jesserockz
|
||||
@@ -82,6 +86,7 @@ esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
esphome/components/homeassistant/* @OttoWinter
|
||||
esphome/components/honeywellabp/* @RubyBailey
|
||||
esphome/components/hrxl_maxsonar_wr/* @netmikey
|
||||
esphome/components/hydreon_rgxx/* @functionpointer
|
||||
esphome/components/i2c/* @esphome/core
|
||||
esphome/components/improv_serial/* @esphome/core
|
||||
esphome/components/ina260/* @MrEditor97
|
||||
@@ -151,6 +156,7 @@ esphome/components/preferences/* @esphome/core
|
||||
esphome/components/psram/* @esphome/core
|
||||
esphome/components/pulse_meter/* @cstaahl @stevebaxter
|
||||
esphome/components/pvvx_mithermometer/* @pasiz
|
||||
esphome/components/qmp6988/* @andrewpc
|
||||
esphome/components/qr_code/* @wjtje
|
||||
esphome/components/radon_eye_ble/* @jeffeb3
|
||||
esphome/components/radon_eye_rd200/* @jeffeb3
|
||||
@@ -162,20 +168,26 @@ esphome/components/rf_bridge/* @jesserockz
|
||||
esphome/components/rgbct/* @jesserockz
|
||||
esphome/components/rtttl/* @glmnet
|
||||
esphome/components/safe_mode/* @jsuanet @paulmonigatti
|
||||
esphome/components/scd4x/* @sjtrny
|
||||
esphome/components/scd4x/* @martgras @sjtrny
|
||||
esphome/components/script/* @esphome/core
|
||||
esphome/components/sdm_meter/* @jesserockz @polyfaces
|
||||
esphome/components/sdp3x/* @Azimath
|
||||
esphome/components/selec_meter/* @sourabhjaiswal
|
||||
esphome/components/select/* @esphome/core
|
||||
esphome/components/sen5x/* @martgras
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
esphome/components/sgp40/* @SenexCrenshaw
|
||||
esphome/components/shelly_dimmer/* @edge90 @rnauber
|
||||
esphome/components/sht4x/* @sjtrny
|
||||
esphome/components/shutdown/* @esphome/core @jsuanet
|
||||
esphome/components/sim800l/* @glmnet
|
||||
esphome/components/sm2135/* @BoukeHaarsma23
|
||||
esphome/components/sml/* @alengwenus
|
||||
esphome/components/socket/* @esphome/core
|
||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
|
||||
esphome/components/spi/* @esphome/core
|
||||
esphome/components/sps30/* @martgras
|
||||
esphome/components/ssd1322_base/* @kbx81
|
||||
esphome/components/ssd1322_spi/* @kbx81
|
||||
esphome/components/ssd1325_base/* @kbx81
|
||||
@@ -222,4 +234,5 @@ esphome/components/whirlpool/* @glmnet
|
||||
esphome/components/xiaomi_lywsd03mmc/* @ahpohl
|
||||
esphome/components/xiaomi_mhoc303/* @drug123
|
||||
esphome/components/xiaomi_mhoc401/* @vevsvevs
|
||||
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
|
||||
esphome/components/xpt2046/* @numo68
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
ARG BASEIMGTYPE=docker
|
||||
|
||||
# https://github.com/hassio-addons/addon-debian-base/releases
|
||||
FROM ghcr.io/hassio-addons/debian-base/amd64:5.2.3 AS base-hassio-amd64
|
||||
FROM ghcr.io/hassio-addons/debian-base/aarch64:5.2.3 AS base-hassio-arm64
|
||||
FROM ghcr.io/hassio-addons/debian-base/armv7:5.2.3 AS base-hassio-armv7
|
||||
FROM ghcr.io/hassio-addons/debian-base/amd64:5.3.0 AS base-hassio-amd64
|
||||
FROM ghcr.io/hassio-addons/debian-base/aarch64:5.3.0 AS base-hassio-arm64
|
||||
FROM ghcr.io/hassio-addons/debian-base/armv7:5.3.0 AS base-hassio-armv7
|
||||
# https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye
|
||||
FROM debian:bullseye-20220125-slim AS base-docker-amd64
|
||||
FROM debian:bullseye-20220125-slim AS base-docker-arm64
|
||||
FROM debian:bullseye-20220125-slim AS base-docker-armv7
|
||||
FROM debian:bullseye-20220328-slim AS base-docker-amd64
|
||||
FROM debian:bullseye-20220328-slim AS base-docker-arm64
|
||||
FROM debian:bullseye-20220328-slim AS base-docker-armv7
|
||||
|
||||
# Use TARGETARCH/TARGETVARIANT defined by docker
|
||||
# https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
|
||||
@@ -23,13 +23,14 @@ RUN \
|
||||
# Use pinned versions so that we get updates with build caching
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
python3=3.9.2-3 \
|
||||
python3-pip=20.3.4-4 \
|
||||
python3-pip=20.3.4-4+deb11u1 \
|
||||
python3-setuptools=52.0.0-4 \
|
||||
python3-pil=8.1.2+dfsg-0.3+deb11u1 \
|
||||
python3-cryptography=3.3.2-1 \
|
||||
iputils-ping=3:20210202-1 \
|
||||
git=1:2.30.2-1 \
|
||||
curl=7.74.0-1.3+deb11u1 \
|
||||
openssh-client=1:8.4p1-5 \
|
||||
&& rm -rf \
|
||||
/tmp/* \
|
||||
/var/{cache,log}/* \
|
||||
|
||||
@@ -2,6 +2,7 @@ import argparse
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
@@ -9,15 +10,18 @@ from esphome import const, writer, yaml_util
|
||||
import esphome.codegen as cg
|
||||
from esphome.config import iter_components, read_config, strip_default_ids
|
||||
from esphome.const import (
|
||||
ALLOWED_NAME_CHARS,
|
||||
CONF_BAUD_RATE,
|
||||
CONF_BROKER,
|
||||
CONF_DEASSERT_RTS_DTR,
|
||||
CONF_LOGGER,
|
||||
CONF_NAME,
|
||||
CONF_OTA,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_ESPHOME,
|
||||
CONF_PLATFORMIO_OPTIONS,
|
||||
CONF_SUBSTITUTIONS,
|
||||
SECRETS_FILES,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError, coroutine
|
||||
@@ -481,6 +485,98 @@ def command_idedata(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_rename(args, config):
|
||||
for c in args.name:
|
||||
if c not in ALLOWED_NAME_CHARS:
|
||||
print(
|
||||
color(
|
||||
Fore.BOLD_RED,
|
||||
f"'{c}' is an invalid character for names. Valid characters are: "
|
||||
f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
|
||||
)
|
||||
)
|
||||
return 1
|
||||
# Load existing yaml file
|
||||
with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file:
|
||||
raw_contents = raw_file.read()
|
||||
|
||||
yaml = yaml_util.load_yaml(CORE.config_path)
|
||||
if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
|
||||
print(
|
||||
color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.")
|
||||
)
|
||||
return 1
|
||||
old_name = yaml[CONF_ESPHOME][CONF_NAME]
|
||||
match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name)
|
||||
if match is None:
|
||||
new_raw = re.sub(
|
||||
rf"name:\s+[\"']?{old_name}[\"']?",
|
||||
f'name: "{args.name}"',
|
||||
raw_contents,
|
||||
)
|
||||
else:
|
||||
old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)]
|
||||
if (
|
||||
len(
|
||||
re.findall(
|
||||
rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?",
|
||||
raw_contents,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
)
|
||||
> 1
|
||||
):
|
||||
print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename"))
|
||||
return 1
|
||||
|
||||
new_raw = re.sub(
|
||||
rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?",
|
||||
f'\\1: "{args.name}"',
|
||||
raw_contents,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
|
||||
print(
|
||||
f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}"
|
||||
)
|
||||
print()
|
||||
|
||||
with open(new_path, mode="w", encoding="utf-8") as new_file:
|
||||
new_file.write(new_raw)
|
||||
|
||||
rc = run_external_process("esphome", "config", new_path)
|
||||
if rc != 0:
|
||||
print(color(Fore.BOLD_RED, "Rename failed. Reverting changes."))
|
||||
os.remove(new_path)
|
||||
return 1
|
||||
|
||||
cli_args = [
|
||||
"run",
|
||||
new_path,
|
||||
"--no-logs",
|
||||
"--device",
|
||||
CORE.address,
|
||||
]
|
||||
|
||||
if args.dashboard:
|
||||
cli_args.insert(0, "--dashboard")
|
||||
|
||||
try:
|
||||
rc = run_external_process("esphome", *cli_args)
|
||||
except KeyboardInterrupt:
|
||||
rc = 1
|
||||
if rc != 0:
|
||||
os.remove(new_path)
|
||||
return 1
|
||||
|
||||
os.remove(CORE.config_path)
|
||||
|
||||
print(color(Fore.BOLD_GREEN, "SUCCESS"))
|
||||
print()
|
||||
return 0
|
||||
|
||||
|
||||
PRE_CONFIG_ACTIONS = {
|
||||
"wizard": command_wizard,
|
||||
"version": command_version,
|
||||
@@ -499,6 +595,7 @@ POST_CONFIG_ACTIONS = {
|
||||
"mqtt-fingerprint": command_mqtt_fingerprint,
|
||||
"clean": command_clean,
|
||||
"idedata": command_idedata,
|
||||
"rename": command_rename,
|
||||
}
|
||||
|
||||
|
||||
@@ -681,6 +778,15 @@ def parse_args(argv):
|
||||
"configuration", help="Your YAML configuration file(s).", nargs=1
|
||||
)
|
||||
|
||||
parser_rename = subparsers.add_parser(
|
||||
"rename",
|
||||
help="Rename a device in YAML, compile the binary and upload it.",
|
||||
)
|
||||
parser_rename.add_argument(
|
||||
"configuration", help="Your YAML configuration file.", nargs=1
|
||||
)
|
||||
parser_rename.add_argument("name", help="The new name for the device.", type=str)
|
||||
|
||||
# Keep backward compatibility with the old command line format of
|
||||
# esphome <config> <command>.
|
||||
#
|
||||
|
||||
@@ -262,21 +262,16 @@ async def repeat_action_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
def validate_wait_until(value):
|
||||
schema = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CONDITION): validate_potentially_and_condition,
|
||||
cv.Optional(CONF_TIMEOUT): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
),
|
||||
}
|
||||
)
|
||||
if isinstance(value, dict) and CONF_CONDITION in value:
|
||||
return schema(value)
|
||||
return validate_wait_until({CONF_CONDITION: value})
|
||||
_validate_wait_until = cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_CONDITION): validate_potentially_and_condition,
|
||||
cv.Optional(CONF_TIMEOUT): cv.templatable(cv.positive_time_period_milliseconds),
|
||||
},
|
||||
key=CONF_CONDITION,
|
||||
)
|
||||
|
||||
|
||||
@register_action("wait_until", WaitUntilAction, validate_wait_until)
|
||||
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
|
||||
async def wait_until_action_to_code(config, action_id, template_arg, args):
|
||||
conditions = await build_condition(config[CONF_CONDITION], template_arg, args)
|
||||
var = cg.new_Pvariable(action_id, template_arg, conditions)
|
||||
|
||||
@@ -64,6 +64,7 @@ from esphome.cpp_types import ( # noqa
|
||||
uint64,
|
||||
int32,
|
||||
int64,
|
||||
size_t,
|
||||
const_char_ptr,
|
||||
NAN,
|
||||
esphome_ns,
|
||||
|
||||
@@ -51,8 +51,8 @@ void ADCSensor::setup() {
|
||||
}
|
||||
}
|
||||
|
||||
// adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
// adc_gpio_init doesn't exist on ESP32-S2, ESP32-C3 or ESP32-H2
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) && !defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_);
|
||||
#endif
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -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,6 +94,29 @@ async def to_code(config):
|
||||
data[pos] = pix[2]
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "RGB565":
|
||||
data = [0 for _ in range(height * width * 2 * frames)]
|
||||
pos = 0
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
frame = image.convert("RGB")
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
pixels = list(frame.getdata())
|
||||
if len(pixels) != height * width:
|
||||
raise core.EsphomeError(
|
||||
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
|
||||
)
|
||||
for pix in pixels:
|
||||
R = pix[0] >> 3
|
||||
G = pix[1] >> 2
|
||||
B = pix[2] >> 3
|
||||
rgb = (R << 11) | (G << 5) | B
|
||||
data[pos] = rgb >> 8
|
||||
pos += 1
|
||||
data[pos] = rgb & 255
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "BINARY":
|
||||
width8 = ((width + 7) // 8) * 8
|
||||
data = [0 for _ in range((height * width8 // 8) * frames)]
|
||||
|
||||
@@ -23,7 +23,7 @@ static const char *const TAG = "api.connection";
|
||||
static const int ESP32_CAMERA_STOP_STREAM = 5000;
|
||||
|
||||
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
|
||||
: parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) {
|
||||
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
|
||||
this->proto_write_buffer_.reserve(64);
|
||||
|
||||
#if defined(USE_API_PLAINTEXT)
|
||||
|
||||
@@ -255,7 +255,7 @@ void APIServer::on_number_update(number::Number *obj, float state) {
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
void APIServer::on_select_update(select::Select *obj, const std::string &state) {
|
||||
void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "api_pb2.h"
|
||||
#include "api_pb2_service.h"
|
||||
#include "util.h"
|
||||
#include "list_entities.h"
|
||||
#include "subscribe_state.h"
|
||||
#include "user_services.h"
|
||||
@@ -65,7 +64,7 @@ class APIServer : public Component, public Controller {
|
||||
void on_number_update(number::Number *obj, float state) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void on_select_update(select::Select *obj, const std::string &state) override;
|
||||
void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void on_lock_update(lock::Lock *obj) override;
|
||||
|
||||
@@ -40,8 +40,7 @@ bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { return this->client_->s
|
||||
#endif
|
||||
|
||||
bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); }
|
||||
ListEntitiesIterator::ListEntitiesIterator(APIServer *server, APIConnection *client)
|
||||
: ComponentIterator(server), client_(client) {}
|
||||
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
|
||||
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
||||
auto resp = service->encode_list_service_response();
|
||||
return this->client_->send_list_entities_services_response(resp);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/component_iterator.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "util.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
@@ -11,7 +11,7 @@ class APIConnection;
|
||||
|
||||
class ListEntitiesIterator : public ComponentIterator {
|
||||
public:
|
||||
ListEntitiesIterator(APIServer *server, APIConnection *client);
|
||||
ListEntitiesIterator(APIConnection *client);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override;
|
||||
#endif
|
||||
@@ -60,5 +60,3 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
|
||||
#include "api_server.h"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "proto.h"
|
||||
#include "util.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -195,6 +195,20 @@ class ProtoWriteBuffer {
|
||||
this->write((value >> 16) & 0xFF);
|
||||
this->write((value >> 24) & 0xFF);
|
||||
}
|
||||
void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
|
||||
this->encode_field_raw(field_id, 5);
|
||||
this->write((value >> 0) & 0xFF);
|
||||
this->write((value >> 8) & 0xFF);
|
||||
this->write((value >> 16) & 0xFF);
|
||||
this->write((value >> 24) & 0xFF);
|
||||
this->write((value >> 32) & 0xFF);
|
||||
this->write((value >> 40) & 0xFF);
|
||||
this->write((value >> 48) & 0xFF);
|
||||
this->write((value >> 56) & 0xFF);
|
||||
}
|
||||
template<typename T> void encode_enum(uint32_t field_id, T value, bool force = false) {
|
||||
this->encode_uint32(field_id, static_cast<uint32_t>(value), force);
|
||||
}
|
||||
@@ -229,6 +243,15 @@ class ProtoWriteBuffer {
|
||||
}
|
||||
this->encode_uint32(field_id, uvalue, force);
|
||||
}
|
||||
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
|
||||
uint64_t uvalue;
|
||||
if (value < 0) {
|
||||
uvalue = ~(value << 1);
|
||||
} else {
|
||||
uvalue = value << 1;
|
||||
}
|
||||
this->encode_uint64(field_id, uvalue, force);
|
||||
}
|
||||
template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) {
|
||||
this->encode_field_raw(field_id, 2);
|
||||
size_t begin = this->buffer_->size();
|
||||
|
||||
@@ -50,8 +50,7 @@ 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
|
||||
InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client)
|
||||
: ComponentIterator(server), client_(client) {}
|
||||
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/component_iterator.h"
|
||||
#include "esphome/core/controller.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "util.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
@@ -12,7 +12,7 @@ class APIConnection;
|
||||
|
||||
class InitialStateIterator : public ComponentIterator {
|
||||
public:
|
||||
InitialStateIterator(APIServer *server, APIConnection *client);
|
||||
InitialStateIterator(APIConnection *client);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override;
|
||||
#endif
|
||||
@@ -55,5 +55,3 @@ class InitialStateIterator : public ComponentIterator {
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
|
||||
#include "api_server.h"
|
||||
|
||||
@@ -38,7 +38,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
const auto &data = service_data.data;
|
||||
|
||||
const uint8_t protocol_version = data[0] >> 4;
|
||||
if (protocol_version != 1) {
|
||||
if (protocol_version != 1 && protocol_version != 2) {
|
||||
ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version);
|
||||
return false;
|
||||
}
|
||||
@@ -57,9 +57,15 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
uint16_t battery_millivolt = data[2] << 8 | data[3];
|
||||
float battery_voltage = battery_millivolt / 1000.0f;
|
||||
|
||||
// Temperature in 1000 * Celsius.
|
||||
uint16_t temp_millicelcius = data[4] << 8 | data[5];
|
||||
float temp_celcius = temp_millicelcius / 1000.0f;
|
||||
// Temperature in 1000 * Celsius (protocol v1) or 100 * Celsius (protocol v2).
|
||||
float temp_celsius;
|
||||
if (protocol_version == 1) {
|
||||
uint16_t temp_millicelsius = data[4] << 8 | data[5];
|
||||
temp_celsius = temp_millicelsius / 1000.0f;
|
||||
} else {
|
||||
int16_t temp_centicelsius = data[4] << 8 | data[5];
|
||||
temp_celsius = temp_centicelsius / 100.0f;
|
||||
}
|
||||
|
||||
// Relative air humidity in the range [0, 2^16).
|
||||
uint16_t humidity = data[6] << 8 | data[7];
|
||||
@@ -76,7 +82,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
battery_voltage_->publish_state(battery_voltage);
|
||||
}
|
||||
if (temperature_ != nullptr) {
|
||||
temperature_->publish_state(temp_celcius);
|
||||
temperature_->publish_state(temp_celsius);
|
||||
}
|
||||
if (humidity_ != nullptr) {
|
||||
humidity_->publish_state(humidity_percent);
|
||||
|
||||
1
esphome/components/bedjet/__init__.py
Normal file
1
esphome/components/bedjet/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@jhansche"]
|
||||
644
esphome/components/bedjet/bedjet.cpp
Normal file
644
esphome/components/bedjet/bedjet.cpp
Normal file
@@ -0,0 +1,644 @@
|
||||
#include "bedjet.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace bedjet {
|
||||
|
||||
using namespace esphome::climate;
|
||||
|
||||
/// Converts a BedJet temp step into degrees Celsius.
|
||||
float bedjet_temp_to_c(const uint8_t temp) {
|
||||
// BedJet temp is "C*2"; to get C, divide by 2.
|
||||
return temp / 2.0f;
|
||||
}
|
||||
|
||||
/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%.
|
||||
uint8_t bedjet_fan_step_to_speed(const uint8_t fan) {
|
||||
// 0 = 5%
|
||||
// 19 = 100%
|
||||
return 5 * fan + 5;
|
||||
}
|
||||
|
||||
static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
|
||||
if (fan_step >= 0 && fan_step <= 19)
|
||||
return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
|
||||
for (int i = 0; i < sizeof(BEDJET_FAN_STEP_NAME_STRINGS); i++) {
|
||||
if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void Bedjet::upgrade_firmware() {
|
||||
auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE);
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
}
|
||||
}
|
||||
|
||||
void Bedjet::dump_config() {
|
||||
LOG_CLIMATE("", "BedJet Climate", this);
|
||||
auto traits = this->get_traits();
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Supported modes:");
|
||||
for (auto mode : traits.get_supported_modes()) {
|
||||
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_mode_to_string(mode)));
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Supported fan modes:");
|
||||
for (const auto &mode : traits.get_supported_fan_modes()) {
|
||||
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
|
||||
}
|
||||
for (const auto &mode : traits.get_supported_custom_fan_modes()) {
|
||||
ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str());
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Supported presets:");
|
||||
for (auto preset : traits.get_supported_presets()) {
|
||||
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
|
||||
}
|
||||
for (const auto &preset : traits.get_supported_custom_presets()) {
|
||||
ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Bedjet::setup() {
|
||||
this->codec_ = make_unique<BedjetCodec>();
|
||||
|
||||
// restore set points
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
ESP_LOGI(TAG, "Restored previous saved state.");
|
||||
restore->apply(this);
|
||||
} else {
|
||||
// Initial status is unknown until we connect
|
||||
this->reset_state_();
|
||||
}
|
||||
|
||||
#ifdef USE_TIME
|
||||
this->setup_time_();
|
||||
#endif
|
||||
}
|
||||
|
||||
/** Resets states to defaults. */
|
||||
void Bedjet::reset_state_() {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
this->action = climate::CLIMATE_ACTION_IDLE;
|
||||
this->target_temperature = NAN;
|
||||
this->current_temperature = NAN;
|
||||
this->preset.reset();
|
||||
this->custom_preset.reset();
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
void Bedjet::loop() {}
|
||||
|
||||
void Bedjet::control(const ClimateCall &call) {
|
||||
ESP_LOGD(TAG, "Received Bedjet::control");
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "Not connected, cannot handle control call yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (call.get_mode().has_value()) {
|
||||
ClimateMode mode = *call.get_mode();
|
||||
BedjetPacket *pkt;
|
||||
switch (mode) {
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
pkt = this->codec_->get_button_request(BTN_OFF);
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
pkt = this->codec_->get_button_request(BTN_HEAT);
|
||||
break;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
pkt = this->codec_->get_button_request(BTN_COOL);
|
||||
break;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
pkt = this->codec_->get_button_request(BTN_DRY);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unsupported mode: %d", mode);
|
||||
return;
|
||||
}
|
||||
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
} else {
|
||||
this->force_refresh_ = true;
|
||||
this->mode = mode;
|
||||
// We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
|
||||
this->custom_preset.reset();
|
||||
this->preset.reset();
|
||||
}
|
||||
}
|
||||
|
||||
if (call.get_target_temperature().has_value()) {
|
||||
auto target_temp = *call.get_target_temperature();
|
||||
auto *pkt = this->codec_->get_set_target_temp_request(target_temp);
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
} else {
|
||||
this->target_temperature = target_temp;
|
||||
}
|
||||
}
|
||||
|
||||
if (call.get_preset().has_value()) {
|
||||
ClimatePreset preset = *call.get_preset();
|
||||
BedjetPacket *pkt;
|
||||
|
||||
if (preset == climate::CLIMATE_PRESET_BOOST) {
|
||||
pkt = this->codec_->get_button_request(BTN_TURBO);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unsupported preset: %d", preset);
|
||||
return;
|
||||
}
|
||||
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
} else {
|
||||
// We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode.
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
this->preset = preset;
|
||||
this->custom_preset.reset();
|
||||
this->force_refresh_ = true;
|
||||
}
|
||||
} else if (call.get_custom_preset().has_value()) {
|
||||
std::string preset = *call.get_custom_preset();
|
||||
BedjetPacket *pkt;
|
||||
|
||||
if (preset == "M1") {
|
||||
pkt = this->codec_->get_button_request(BTN_M1);
|
||||
} else if (preset == "M2") {
|
||||
pkt = this->codec_->get_button_request(BTN_M2);
|
||||
} else if (preset == "M3") {
|
||||
pkt = this->codec_->get_button_request(BTN_M3);
|
||||
} else if (preset == "EXT HT") {
|
||||
pkt = this->codec_->get_button_request(BTN_EXTHT);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
} else {
|
||||
this->force_refresh_ = true;
|
||||
this->custom_preset = preset;
|
||||
this->preset.reset();
|
||||
}
|
||||
}
|
||||
|
||||
if (call.get_fan_mode().has_value()) {
|
||||
// Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments.
|
||||
// We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here.
|
||||
auto fan_mode = *call.get_fan_mode();
|
||||
BedjetPacket *pkt;
|
||||
if (fan_mode == climate::CLIMATE_FAN_LOW) {
|
||||
pkt = this->codec_->get_set_fan_speed_request(3 /* = 20% */);
|
||||
} else if (fan_mode == climate::CLIMATE_FAN_MEDIUM) {
|
||||
pkt = this->codec_->get_set_fan_speed_request(9 /* = 50% */);
|
||||
} else if (fan_mode == climate::CLIMATE_FAN_HIGH) {
|
||||
pkt = this->codec_->get_set_fan_speed_request(14 /* = 75% */);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(),
|
||||
LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
|
||||
return;
|
||||
}
|
||||
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
} else {
|
||||
this->force_refresh_ = true;
|
||||
}
|
||||
} else if (call.get_custom_fan_mode().has_value()) {
|
||||
auto fan_mode = *call.get_custom_fan_mode();
|
||||
auto fan_step = bedjet_fan_speed_to_step(fan_mode);
|
||||
if (fan_step >= 0 && fan_step <= 19) {
|
||||
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(),
|
||||
fan_step);
|
||||
// The index should represent the fan_step index.
|
||||
BedjetPacket *pkt = this->codec_->get_set_fan_speed_request(fan_step);
|
||||
auto status = this->write_bedjet_packet_(pkt);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
} else {
|
||||
this->force_refresh_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Bedjet::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
|
||||
switch (event) {
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
ESP_LOGV(TAG, "Disconnected: reason=%d", param->disconnect.reason);
|
||||
this->status_set_warning();
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
auto *chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str());
|
||||
break;
|
||||
}
|
||||
this->char_handle_cmd_ = chr->handle;
|
||||
|
||||
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
this->char_handle_status_ = chr->handle;
|
||||
// We also need to obtain the config descriptor for this handle.
|
||||
// Otherwise once we set node_state=Established, the parent will flush all handles/descriptors, and we won't be
|
||||
// able to look it up.
|
||||
auto *descr = this->parent_->get_config_descriptor(this->char_handle_status_);
|
||||
if (descr == nullptr) {
|
||||
ESP_LOGW(TAG, "No config descriptor for status handle 0x%x. Will not be able to receive status notifications",
|
||||
this->char_handle_status_);
|
||||
} else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 ||
|
||||
descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) {
|
||||
ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_,
|
||||
descr->uuid.to_string().c_str());
|
||||
} else {
|
||||
this->config_descr_status_ = descr->handle;
|
||||
}
|
||||
|
||||
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID);
|
||||
if (chr != nullptr) {
|
||||
this->char_handle_name_ = chr->handle;
|
||||
auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_,
|
||||
ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Services complete: obtained char handles.");
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
|
||||
this->set_notify_(true);
|
||||
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_.has_value()) {
|
||||
this->send_local_time_();
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_WRITE_DESCR_EVT: {
|
||||
if (param->write.status != ESP_GATT_OK) {
|
||||
// ESP_GATT_INVALID_ATTR_LEN
|
||||
ESP_LOGW(TAG, "Error writing descr at handle 0x%04d, status=%d", param->write.handle, param->write.status);
|
||||
break;
|
||||
}
|
||||
// [16:44:44][V][bedjet:279]: [JOENJET] Register for notify event success: h=0x002a s=0
|
||||
// This might be the enable-notify descriptor? (or disable-notify)
|
||||
ESP_LOGV(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle,
|
||||
param->write.status);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_WRITE_CHAR_EVT: {
|
||||
if (param->write.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "Error writing char at handle 0x%04d, status=%d", param->write.handle, param->write.status);
|
||||
break;
|
||||
}
|
||||
if (param->write.handle == this->char_handle_cmd_) {
|
||||
if (this->force_refresh_) {
|
||||
// Command write was successful. Publish the pending state, hoping that notify will kick in.
|
||||
this->publish_state();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_READ_CHAR_EVT: {
|
||||
if (param->read.conn_id != this->parent_->conn_id)
|
||||
break;
|
||||
if (param->read.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
|
||||
break;
|
||||
}
|
||||
if (param->read.handle == this->char_handle_status_) {
|
||||
// This is the additional packet that doesn't fit in the notify packet.
|
||||
this->codec_->decode_extra(param->read.value, param->read.value_len);
|
||||
} else if (param->read.handle == this->char_handle_name_) {
|
||||
// The data should represent the name.
|
||||
if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) {
|
||||
std::string bedjet_name(reinterpret_cast<char const *>(param->read.value), param->read.value_len);
|
||||
// this->set_name(bedjet_name);
|
||||
ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str());
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
// This event means that ESP received the request to enable notifications on the client side. But we also have to
|
||||
// tell the server that we want it to send notifications. Normally BLEClient parent would handle this
|
||||
// automatically, but as soon as we set our status to Established, the parent is going to purge all the
|
||||
// service/char/descriptor handles, and then get_config_descriptor() won't work anymore. There's no way to disable
|
||||
// the BLEClient parent behavior, so our only option is to write the handle anyway, and hope a double-write
|
||||
// doesn't break anything.
|
||||
|
||||
if (param->reg_for_notify.handle != this->char_handle_status_) {
|
||||
ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x",
|
||||
this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_);
|
||||
break;
|
||||
}
|
||||
|
||||
this->write_notify_config_descriptor_(true);
|
||||
this->last_notify_ = 0;
|
||||
this->force_refresh_ = true;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
|
||||
// This event is not handled by the parent BLEClient, so we need to do this either way.
|
||||
if (param->unreg_for_notify.handle != this->char_handle_status_) {
|
||||
ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x",
|
||||
this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_);
|
||||
break;
|
||||
}
|
||||
|
||||
this->write_notify_config_descriptor_(false);
|
||||
this->last_notify_ = 0;
|
||||
// Now we wait until the next update() poll to re-register notify...
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.handle != this->char_handle_status_) {
|
||||
ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(),
|
||||
this->char_handle_status_, param->notify.handle);
|
||||
break;
|
||||
}
|
||||
|
||||
// FIXME: notify events come in every ~200-300 ms, which is too fast to be helpful. So we
|
||||
// throttle the updates to once every MIN_NOTIFY_THROTTLE (5 seconds).
|
||||
// Another idea would be to keep notify off by default, and use update() as an opportunity to turn on
|
||||
// notify to get enough data to update status, then turn off notify again.
|
||||
|
||||
uint32_t now = millis();
|
||||
auto delta = now - this->last_notify_;
|
||||
|
||||
if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) {
|
||||
bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len);
|
||||
this->last_notify_ = now;
|
||||
|
||||
if (needs_extra) {
|
||||
// this means the packet was partial, so read the status characteristic to get the second part.
|
||||
auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id,
|
||||
this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (this->force_refresh_) {
|
||||
// If we requested an immediate update, do that now.
|
||||
this->update();
|
||||
this->force_refresh_ = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT.
|
||||
*
|
||||
* This is a copy of ble_client's automatic handling of `ESP_GATTC_REG_FOR_NOTIFY_EVT`, in order
|
||||
* to undo the same on unregister. It also allows us to maintain the config descriptor separately,
|
||||
* since the parent BLEClient is going to purge all descriptors once we set our connection status
|
||||
* to `Established`.
|
||||
*/
|
||||
uint8_t Bedjet::write_notify_config_descriptor_(bool enable) {
|
||||
auto handle = this->config_descr_status_;
|
||||
if (handle == 0) {
|
||||
ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", this->char_handle_status_);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits.
|
||||
uint8_t notify_en[] = {0, 0};
|
||||
notify_en[0] = enable;
|
||||
auto status =
|
||||
esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en),
|
||||
¬ify_en[0], 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);
|
||||
return status;
|
||||
}
|
||||
ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x", this->get_name().c_str(), enable ? "true" : "false",
|
||||
handle);
|
||||
return ESP_GATT_OK;
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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_();
|
||||
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);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/** 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) {
|
||||
if (!this->parent_->enabled) {
|
||||
ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str());
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_,
|
||||
pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP,
|
||||
ESP_GATT_AUTH_REQ_NONE);
|
||||
return status;
|
||||
}
|
||||
|
||||
/** Configures the local ESP BLE client to register (`true`) or unregister (`false`) for status notifications. */
|
||||
uint8_t Bedjet::set_notify_(const bool enable) {
|
||||
uint8_t status;
|
||||
if (enable) {
|
||||
status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda,
|
||||
this->char_handle_status_);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
|
||||
}
|
||||
} else {
|
||||
status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda,
|
||||
this->char_handle_status_);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status);
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status);
|
||||
return status;
|
||||
}
|
||||
|
||||
/** Attempts to update the climate device from the last received BedjetStatusPacket.
|
||||
*
|
||||
* @return `true` if the status has been applied; `false` if there is nothing to apply.
|
||||
*/
|
||||
bool Bedjet::update_status_() {
|
||||
if (!this->codec_->has_status())
|
||||
return false;
|
||||
|
||||
BedjetStatusPacket status = *this->codec_->get_status_packet();
|
||||
|
||||
auto converted_temp = bedjet_temp_to_c(status.target_temp_step);
|
||||
if (converted_temp > 0)
|
||||
this->target_temperature = converted_temp;
|
||||
converted_temp = bedjet_temp_to_c(status.ambient_temp_step);
|
||||
if (converted_temp > 0)
|
||||
this->current_temperature = converted_temp;
|
||||
|
||||
const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(status.fan_step);
|
||||
if (fan_mode_name != nullptr) {
|
||||
this->custom_fan_mode = *fan_mode_name;
|
||||
}
|
||||
|
||||
// TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
|
||||
switch (status.mode) {
|
||||
case MODE_WAIT: // Biorhythm "wait" step: device is idle
|
||||
case MODE_STANDBY:
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
this->action = climate::CLIMATE_ACTION_IDLE;
|
||||
this->fan_mode = climate::CLIMATE_FAN_OFF;
|
||||
this->custom_preset.reset();
|
||||
this->preset.reset();
|
||||
break;
|
||||
|
||||
case MODE_HEAT:
|
||||
case MODE_EXTHT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
this->action = climate::CLIMATE_ACTION_HEATING;
|
||||
this->custom_preset.reset();
|
||||
this->preset.reset();
|
||||
break;
|
||||
|
||||
case MODE_COOL:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
this->action = climate::CLIMATE_ACTION_COOLING;
|
||||
this->custom_preset.reset();
|
||||
this->preset.reset();
|
||||
break;
|
||||
|
||||
case MODE_DRY:
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
this->action = climate::CLIMATE_ACTION_DRYING;
|
||||
this->custom_preset.reset();
|
||||
this->preset.reset();
|
||||
break;
|
||||
|
||||
case MODE_TURBO:
|
||||
this->preset = climate::CLIMATE_PRESET_BOOST;
|
||||
this->custom_preset.reset();
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
this->action = climate::CLIMATE_ACTION_HEATING;
|
||||
break;
|
||||
|
||||
default:
|
||||
ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), status.mode);
|
||||
break;
|
||||
}
|
||||
|
||||
if (this->is_valid_()) {
|
||||
this->publish_state();
|
||||
this->codec_->clear_status();
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Bedjet::update() {
|
||||
ESP_LOGV(TAG, "[%s] update()", this->get_name().c_str());
|
||||
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
if (!this->parent()->enabled) {
|
||||
ESP_LOGD(TAG, "[%s] Not connected, because enabled=false", this->get_name().c_str());
|
||||
} else {
|
||||
// Possibly still trying to connect.
|
||||
ESP_LOGD(TAG, "[%s] Not connected, enabled=true", this->get_name().c_str());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = this->update_status_();
|
||||
if (!result) {
|
||||
uint32_t now = millis();
|
||||
uint32_t diff = now - this->last_notify_;
|
||||
|
||||
if (this->last_notify_ == 0) {
|
||||
// This means we're connected and haven't received a notification, so it likely means that the BedJet is off.
|
||||
// However, it could also mean that it's running, but failing to send notifications.
|
||||
// We can try to unregister for notifications now, and then re-register, hoping to clear it up...
|
||||
// But how do we know for sure which state we're in, and how do we actually clear out the buggy state?
|
||||
|
||||
ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str());
|
||||
this->set_notify_(false);
|
||||
} else if (diff > NOTIFY_WARN_THRESHOLD) {
|
||||
ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000);
|
||||
}
|
||||
|
||||
if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) {
|
||||
ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_);
|
||||
this->parent()->set_enabled(false);
|
||||
this->parent()->set_enabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace bedjet
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
123
esphome/components/bedjet/bedjet.h
Normal file
123
esphome/components/bedjet/bedjet.h
Normal file
@@ -0,0 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "bedjet_base.h"
|
||||
|
||||
#ifdef USE_TIME
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace bedjet {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
static const espbt::ESPBTUUID BEDJET_SERVICE_UUID = espbt::ESPBTUUID::from_raw("00001000-bed0-0080-aa55-4265644a6574");
|
||||
static const espbt::ESPBTUUID BEDJET_STATUS_UUID = espbt::ESPBTUUID::from_raw("00002000-bed0-0080-aa55-4265644a6574");
|
||||
static const espbt::ESPBTUUID BEDJET_COMMAND_UUID = espbt::ESPBTUUID::from_raw("00002004-bed0-0080-aa55-4265644a6574");
|
||||
static const espbt::ESPBTUUID BEDJET_NAME_UUID = espbt::ESPBTUUID::from_raw("00002001-bed0-0080-aa55-4265644a6574");
|
||||
|
||||
class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void update() override;
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
#ifdef USE_TIME
|
||||
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
|
||||
#endif
|
||||
void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; }
|
||||
|
||||
/** Attempts to check for and apply firmware updates. */
|
||||
void upgrade_firmware();
|
||||
|
||||
climate::ClimateTraits traits() override {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.set_supports_action(true);
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
// climate::CLIMATE_MODE_TURBO // Not supported by Climate: see presets instead
|
||||
climate::CLIMATE_MODE_FAN_ONLY,
|
||||
climate::CLIMATE_MODE_DRY,
|
||||
});
|
||||
|
||||
// It would be better if we had a slider for the fan modes.
|
||||
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET);
|
||||
traits.set_supported_presets({
|
||||
// If we support NONE, then have to decide what happens if the user switches to it (turn off?)
|
||||
// climate::CLIMATE_PRESET_NONE,
|
||||
// Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
|
||||
climate::CLIMATE_PRESET_BOOST,
|
||||
});
|
||||
traits.set_supported_custom_presets({
|
||||
// We could fetch biodata from bedjet and set these names that way.
|
||||
// But then we have to invert the lookup in order to send the right preset.
|
||||
// For now, we can leave them as M1-3 to match the remote buttons.
|
||||
// EXT HT added to match remote button.
|
||||
"EXT HT",
|
||||
"M1",
|
||||
"M2",
|
||||
"M3",
|
||||
});
|
||||
traits.set_visual_min_temperature(19.0);
|
||||
traits.set_visual_max_temperature(43.0);
|
||||
traits.set_visual_temperature_step(1.0);
|
||||
return traits;
|
||||
}
|
||||
|
||||
protected:
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
|
||||
#ifdef USE_TIME
|
||||
void setup_time_();
|
||||
void send_local_time_();
|
||||
optional<time::RealTimeClock *> time_id_{};
|
||||
#endif
|
||||
|
||||
uint32_t timeout_{DEFAULT_STATUS_TIMEOUT};
|
||||
|
||||
static const uint32_t MIN_NOTIFY_THROTTLE = 5000;
|
||||
static const uint32_t NOTIFY_WARN_THRESHOLD = 300000;
|
||||
static const uint32_t DEFAULT_STATUS_TIMEOUT = 900000;
|
||||
|
||||
uint8_t set_notify_(bool enable);
|
||||
uint8_t write_bedjet_packet_(BedjetPacket *pkt);
|
||||
void reset_state_();
|
||||
bool update_status_();
|
||||
|
||||
bool is_valid_() {
|
||||
// FIXME: find a better way to check this?
|
||||
return !std::isnan(this->current_temperature) && !std::isnan(this->target_temperature) &&
|
||||
this->current_temperature > 1 && this->target_temperature > 1;
|
||||
}
|
||||
|
||||
uint32_t last_notify_ = 0;
|
||||
bool force_refresh_ = false;
|
||||
|
||||
std::unique_ptr<BedjetCodec> codec_;
|
||||
uint16_t char_handle_cmd_;
|
||||
uint16_t char_handle_name_;
|
||||
uint16_t char_handle_status_;
|
||||
uint16_t config_descr_status_;
|
||||
|
||||
uint8_t write_notify_config_descriptor_(bool enable);
|
||||
};
|
||||
|
||||
} // namespace bedjet
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
123
esphome/components/bedjet/bedjet_base.cpp
Normal file
123
esphome/components/bedjet/bedjet_base.cpp
Normal file
@@ -0,0 +1,123 @@
|
||||
#include "bedjet_base.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome {
|
||||
namespace bedjet {
|
||||
|
||||
/// Converts a BedJet temp step into degrees Fahrenheit.
|
||||
float bedjet_temp_to_f(const uint8_t temp) {
|
||||
// BedJet temp is "C*2"; to get F, multiply by 0.9 (half 1.8) and add 32.
|
||||
return 0.9f * temp + 32.0f;
|
||||
}
|
||||
|
||||
/** Cleans up the packet before sending. */
|
||||
BedjetPacket *BedjetCodec::clean_packet_() {
|
||||
// So far no commands require more than 2 bytes of data.
|
||||
assert(this->packet_.data_length <= 2);
|
||||
for (int i = this->packet_.data_length; i < 2; i++) {
|
||||
this->packet_.data[i] = '\0';
|
||||
}
|
||||
ESP_LOGV(TAG, "Created packet: %02X, %02X %02X", this->packet_.command, this->packet_.data[0], this->packet_.data[1]);
|
||||
return &this->packet_;
|
||||
}
|
||||
|
||||
/** Returns a BedjetPacket that will initiate a BedjetButton press. */
|
||||
BedjetPacket *BedjetCodec::get_button_request(BedjetButton button) {
|
||||
this->packet_.command = CMD_BUTTON;
|
||||
this->packet_.data_length = 1;
|
||||
this->packet_.data[0] = button;
|
||||
return this->clean_packet_();
|
||||
}
|
||||
|
||||
/** Returns a BedjetPacket that will set the device's target `temperature`. */
|
||||
BedjetPacket *BedjetCodec::get_set_target_temp_request(float temperature) {
|
||||
this->packet_.command = CMD_SET_TEMP;
|
||||
this->packet_.data_length = 1;
|
||||
this->packet_.data[0] = temperature * 2;
|
||||
return this->clean_packet_();
|
||||
}
|
||||
|
||||
/** Returns a BedjetPacket that will set the device's target fan speed. */
|
||||
BedjetPacket *BedjetCodec::get_set_fan_speed_request(const uint8_t fan_step) {
|
||||
this->packet_.command = CMD_SET_FAN;
|
||||
this->packet_.data_length = 1;
|
||||
this->packet_.data[0] = fan_step;
|
||||
return this->clean_packet_();
|
||||
}
|
||||
|
||||
/** Returns a BedjetPacket that will set the device's current time. */
|
||||
BedjetPacket *BedjetCodec::get_set_time_request(const uint8_t hour, const uint8_t minute) {
|
||||
this->packet_.command = CMD_SET_TIME;
|
||||
this->packet_.data_length = 2;
|
||||
this->packet_.data[0] = hour;
|
||||
this->packet_.data[1] = minute;
|
||||
return this->clean_packet_();
|
||||
}
|
||||
|
||||
/** Decodes the extra bytes that were received after being notified with a partial packet. */
|
||||
void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) {
|
||||
ESP_LOGV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
|
||||
uint8_t offset = this->last_buffer_size_;
|
||||
if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) {
|
||||
memcpy(((uint8_t *) (&this->buf_)) + offset, data, length);
|
||||
ESP_LOGV(TAG,
|
||||
"Extra bytes: skip1=0x%08x, skip2=0x%04x, skip3=0x%02x; update phase=0x%02x, "
|
||||
"flags=BedjetFlags <conn=%c, leds=%c, units=%c, mute=%c, others=%02x>",
|
||||
this->buf_._skip_1_, this->buf_._skip_2_, this->buf_._skip_3_, this->buf_.update_phase,
|
||||
this->buf_.flags & 0x20 ? '1' : '0', this->buf_.flags & 0x10 ? '1' : '0',
|
||||
this->buf_.flags & 0x04 ? '1' : '0', this->buf_.flags & 0x01 ? '1' : '0',
|
||||
this->buf_.flags & ~(0x20 | 0x10 | 0x04 | 0x01));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Could not determine where to append to, last offset=%d, max size=%u, new size would be %d", offset,
|
||||
sizeof(BedjetStatusPacket), length + offset);
|
||||
}
|
||||
}
|
||||
|
||||
/** Decodes the incoming status packet received on the BEDJET_STATUS_UUID.
|
||||
*
|
||||
* @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise.
|
||||
*/
|
||||
bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) {
|
||||
ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
|
||||
|
||||
if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) {
|
||||
this->status_packet_.reset();
|
||||
|
||||
// Clear old buffer
|
||||
memset(&this->buf_, 0, sizeof(BedjetStatusPacket));
|
||||
// Copy new data into buffer
|
||||
memcpy(&this->buf_, data, length);
|
||||
this->last_buffer_size_ = length;
|
||||
|
||||
// TODO: validate the packet checksum?
|
||||
if (this->buf_.mode >= 0 && this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 &&
|
||||
this->buf_.target_temp_step <= 86 && this->buf_.actual_temp_step > 1 && this->buf_.actual_temp_step <= 100 &&
|
||||
this->buf_.ambient_temp_step > 1 && this->buf_.ambient_temp_step <= 100) {
|
||||
// and save it for the update() loop
|
||||
this->status_packet_ = this->buf_;
|
||||
return this->buf_.is_partial == 1;
|
||||
} else {
|
||||
// TODO: log a warning if we detect that we connected to a non-V3 device.
|
||||
ESP_LOGW(TAG, "Received potentially invalid packet (len %d):", length);
|
||||
}
|
||||
} else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) {
|
||||
// We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself.
|
||||
ESP_LOGV(TAG,
|
||||
"received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, "
|
||||
"[12]=%d, [-1]=%d",
|
||||
bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], data[9],
|
||||
data[10], data[11], data[12], data[length - 1]);
|
||||
|
||||
if (this->has_status()) {
|
||||
this->status_packet_->ambient_temp_step = data[6];
|
||||
}
|
||||
} else {
|
||||
// TODO: log a warning if we detect that we connected to a non-V3 device.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace bedjet
|
||||
} // namespace esphome
|
||||
159
esphome/components/bedjet/bedjet_base.h
Normal file
159
esphome/components/bedjet/bedjet_base.h
Normal file
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "bedjet_const.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bedjet {
|
||||
|
||||
struct BedjetPacket {
|
||||
uint8_t data_length;
|
||||
BedjetCommand command;
|
||||
uint8_t data[2];
|
||||
};
|
||||
|
||||
struct BedjetFlags {
|
||||
/* uint8_t */
|
||||
int a_ : 1; // 0x80
|
||||
int b_ : 1; // 0x40
|
||||
int conn_test_passed : 1; ///< (0x20) Bit is set `1` if the last connection test passed.
|
||||
int leds_enabled : 1; ///< (0x10) Bit is set `1` if the LEDs on the device are enabled.
|
||||
int c_ : 1; // 0x08
|
||||
int units_setup : 1; ///< (0x04) Bit is set `1` if the device's units have been configured.
|
||||
int d_ : 1; // 0x02
|
||||
int beeps_muted : 1; ///< (0x01) Bit is set `1` if the device's sound output is muted.
|
||||
} __attribute__((packed));
|
||||
|
||||
enum BedjetPacketFormat : uint8_t {
|
||||
PACKET_FORMAT_DEBUG = 0x05, // 5
|
||||
PACKET_FORMAT_V3_HOME = 0x56, // 86
|
||||
};
|
||||
|
||||
enum BedjetPacketType : uint8_t {
|
||||
PACKET_TYPE_STATUS = 0x1,
|
||||
PACKET_TYPE_DEBUG = 0x2,
|
||||
};
|
||||
|
||||
/** The format of a BedJet V3 status packet. */
|
||||
struct BedjetStatusPacket {
|
||||
// [0]
|
||||
uint8_t is_partial : 8; ///< `1` indicates that this is a partial packet, and more data can be read directly from the
|
||||
///< characteristic.
|
||||
BedjetPacketFormat packet_format : 8; ///< BedjetPacketFormat::PACKET_FORMAT_V3_HOME for BedJet V3 status packet
|
||||
///< format. BedjetPacketFormat::PACKET_FORMAT_DEBUG for debugging packets.
|
||||
uint8_t
|
||||
expecting_length : 8; ///< The expected total length of the status packet after merging the additional packet.
|
||||
BedjetPacketType packet_type : 8; ///< Typically BedjetPacketType::PACKET_TYPE_STATUS for BedJet V3 status packet.
|
||||
|
||||
// [4]
|
||||
uint8_t time_remaining_hrs : 8; ///< Hours remaining in program runtime
|
||||
uint8_t time_remaining_mins : 8; ///< Minutes remaining in program runtime
|
||||
uint8_t time_remaining_secs : 8; ///< Seconds remaining in program runtime
|
||||
|
||||
// [7]
|
||||
uint8_t actual_temp_step : 8; ///< Actual temp of the air blown by the BedJet fan; value represents `2 *
|
||||
///< degrees_celsius`. See #bedjet_temp_to_c and #bedjet_temp_to_f
|
||||
uint8_t target_temp_step : 8; ///< Target temp that the BedJet will try to heat to. See #actual_temp_step.
|
||||
|
||||
// [9]
|
||||
BedjetMode mode : 8; ///< BedJet operating mode.
|
||||
|
||||
// [10]
|
||||
uint8_t fan_step : 8; ///< BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): `5 + 5
|
||||
///< * fan_step`
|
||||
uint8_t max_hrs : 8; ///< Max hours of mode runtime
|
||||
uint8_t max_mins : 8; ///< Max minutes of mode runtime
|
||||
uint8_t min_temp_step : 8; ///< Min temp allowed in mode. See #actual_temp_step.
|
||||
uint8_t max_temp_step : 8; ///< Max temp allowed in mode. See #actual_temp_step.
|
||||
|
||||
// [15-16]
|
||||
uint16_t turbo_time : 16; ///< Time remaining in BedjetMode::MODE_TURBO.
|
||||
|
||||
// [17]
|
||||
uint8_t ambient_temp_step : 8; ///< Current ambient air temp. This is the coldest air the BedJet can blow. See
|
||||
///< #actual_temp_step.
|
||||
uint8_t shutdown_reason : 8; ///< The reason for the last device shutdown.
|
||||
|
||||
// [19-25]; the initial partial packet cuts off here after [19]
|
||||
// Skip 7 bytes?
|
||||
uint32_t _skip_1_ : 32; // Unknown 19-22 = 0x01810112
|
||||
|
||||
uint16_t _skip_2_ : 16; // Unknown 23-24 = 0x1310
|
||||
uint8_t _skip_3_ : 8; // Unknown 25 = 0x00
|
||||
|
||||
// [26]
|
||||
// 0x18(24) = "Connection test has completed OK"
|
||||
// 0x1a(26) = "Firmware update is not needed"
|
||||
uint8_t update_phase : 8; ///< The current status/phase of a firmware update.
|
||||
|
||||
// [27]
|
||||
// FIXME: cannot nest packed struct of matching length here?
|
||||
/* BedjetFlags */ uint8_t flags : 8; /// See BedjetFlags for the packed byte flags.
|
||||
// [28-31]; 20+11 bytes
|
||||
uint32_t _skip_4_ : 32; // Unknown
|
||||
|
||||
} __attribute__((packed));
|
||||
|
||||
/** This class is responsible for encoding command packets and decoding status packets.
|
||||
*
|
||||
* Status Packets
|
||||
* ==============
|
||||
* The BedJet protocol depends on registering for notifications on the esphome::BedJet::BEDJET_SERVICE_UUID
|
||||
* characteristic. If the BedJet is on, it will send rapid updates as notifications. If it is off,
|
||||
* it generally will not notify of any status.
|
||||
*
|
||||
* As the BedJet V3's BedjetStatusPacket exceeds the buffer size allowed for BLE notification packets,
|
||||
* the notification packet will contain `BedjetStatusPacket::is_partial == 1`. When that happens, an additional
|
||||
* read of the esphome::BedJet::BEDJET_SERVICE_UUID characteristic will contain the second portion of the
|
||||
* full status packet.
|
||||
*
|
||||
* Command Packets
|
||||
* ===============
|
||||
* This class supports encoding a number of BedjetPacket commands:
|
||||
* - Button press
|
||||
* This simulates a press of one of the BedjetButton values.
|
||||
* - BedjetPacket#command = BedjetCommand::CMD_BUTTON
|
||||
* - BedjetPacket#data [0] contains the BedjetButton value
|
||||
* - Set target temp
|
||||
* This sets the BedJet's target temp to a concrete temperature value.
|
||||
* - BedjetPacket#command = BedjetCommand::CMD_SET_TEMP
|
||||
* - BedjetPacket#data [0] contains the BedJet temp value; see BedjetStatusPacket#actual_temp_step
|
||||
* - Set fan speed
|
||||
* This sets the BedJet fan speed.
|
||||
* - BedjetPacket#command = BedjetCommand::CMD_SET_FAN
|
||||
* - BedjetPacket#data [0] contains the BedJet fan step in the range 0-19.
|
||||
* - Set current time
|
||||
* The BedJet needs to have its clock set properly in order to run the biorhythm programs, which might
|
||||
* contain time-of-day based step rules.
|
||||
* - BedjetPacket#command = BedjetCommand::CMD_SET_TIME
|
||||
* - BedjetPacket#data [0] is hours, [1] is minutes
|
||||
*/
|
||||
class BedjetCodec {
|
||||
public:
|
||||
BedjetPacket *get_button_request(BedjetButton button);
|
||||
BedjetPacket *get_set_target_temp_request(float temperature);
|
||||
BedjetPacket *get_set_fan_speed_request(uint8_t fan_step);
|
||||
BedjetPacket *get_set_time_request(uint8_t hour, uint8_t minute);
|
||||
|
||||
bool decode_notify(const uint8_t *data, uint16_t length);
|
||||
void decode_extra(const uint8_t *data, uint16_t length);
|
||||
|
||||
inline bool has_status() { return this->status_packet_.has_value(); }
|
||||
const optional<BedjetStatusPacket> &get_status_packet() const { return this->status_packet_; }
|
||||
void clear_status() { this->status_packet_.reset(); }
|
||||
|
||||
protected:
|
||||
BedjetPacket *clean_packet_();
|
||||
|
||||
uint8_t last_buffer_size_ = 0;
|
||||
|
||||
BedjetPacket packet_;
|
||||
|
||||
optional<BedjetStatusPacket> status_packet_;
|
||||
BedjetStatusPacket buf_;
|
||||
};
|
||||
|
||||
} // namespace bedjet
|
||||
} // namespace esphome
|
||||
78
esphome/components/bedjet/bedjet_const.h
Normal file
78
esphome/components/bedjet/bedjet_const.h
Normal file
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
|
||||
namespace esphome {
|
||||
namespace bedjet {
|
||||
|
||||
static const char *const TAG = "bedjet";
|
||||
|
||||
enum BedjetMode : uint8_t {
|
||||
/// BedJet is Off
|
||||
MODE_STANDBY = 0,
|
||||
/// BedJet is in Heat mode (limited to 4 hours)
|
||||
MODE_HEAT = 1,
|
||||
/// BedJet is in Turbo mode (high heat, limited time)
|
||||
MODE_TURBO = 2,
|
||||
/// BedJet is in Extended Heat mode (limited to 10 hours)
|
||||
MODE_EXTHT = 3,
|
||||
/// BedJet is in Cool mode (actually "Fan only" mode)
|
||||
MODE_COOL = 4,
|
||||
/// BedJet is in Dry mode (high speed, no heat)
|
||||
MODE_DRY = 5,
|
||||
/// BedJet is in "wait" mode, a step during a biorhythm program
|
||||
MODE_WAIT = 6,
|
||||
};
|
||||
|
||||
enum BedjetButton : uint8_t {
|
||||
/// Turn BedJet off
|
||||
BTN_OFF = 0x1,
|
||||
/// Enter Cool mode (fan only)
|
||||
BTN_COOL = 0x2,
|
||||
/// Enter Heat mode (limited to 4 hours)
|
||||
BTN_HEAT = 0x3,
|
||||
/// Enter Turbo mode (high heat, limited to 10 minutes)
|
||||
BTN_TURBO = 0x4,
|
||||
/// Enter Dry mode (high speed, no heat)
|
||||
BTN_DRY = 0x5,
|
||||
/// Enter Extended Heat mode (limited to 10 hours)
|
||||
BTN_EXTHT = 0x6,
|
||||
|
||||
/// Start the M1 biorhythm/preset program
|
||||
BTN_M1 = 0x20,
|
||||
/// Start the M2 biorhythm/preset program
|
||||
BTN_M2 = 0x21,
|
||||
/// Start the M3 biorhythm/preset program
|
||||
BTN_M3 = 0x22,
|
||||
|
||||
/* These are "MAGIC" buttons */
|
||||
|
||||
/// Turn debug mode on/off
|
||||
MAGIC_DEBUG_ON = 0x40,
|
||||
MAGIC_DEBUG_OFF = 0x41,
|
||||
/// Perform a connection test.
|
||||
MAGIC_CONNTEST = 0x42,
|
||||
/// Request a firmware update. This will also restart the Bedjet.
|
||||
MAGIC_UPDATE = 0x43,
|
||||
};
|
||||
|
||||
enum BedjetCommand : uint8_t {
|
||||
CMD_BUTTON = 0x1,
|
||||
CMD_SET_TEMP = 0x3,
|
||||
CMD_STATUS = 0x6,
|
||||
CMD_SET_FAN = 0x7,
|
||||
CMD_SET_TIME = 0x8,
|
||||
};
|
||||
|
||||
#define BEDJET_FAN_STEP_NAMES_ \
|
||||
{ \
|
||||
" 5%", " 10%", " 15%", " 20%", " 25%", " 30%", " 35%", " 40%", " 45%", " 50%", " 55%", " 60%", " 65%", " 70%", \
|
||||
" 75%", " 80%", " 85%", " 90%", " 95%", "100%" \
|
||||
}
|
||||
|
||||
static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_;
|
||||
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[20] = BEDJET_FAN_STEP_NAMES_;
|
||||
static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_;
|
||||
|
||||
} // namespace bedjet
|
||||
} // namespace esphome
|
||||
42
esphome/components/bedjet/climate.py
Normal file
42
esphome/components/bedjet/climate.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import climate, ble_client, time
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_RECEIVE_TIMEOUT,
|
||||
CONF_TIME_ID,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@jhansche"]
|
||||
DEPENDENCIES = ["ble_client"]
|
||||
|
||||
bedjet_ns = cg.esphome_ns.namespace("bedjet")
|
||||
Bedjet = bedjet_ns.class_(
|
||||
"Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
climate.CLIMATE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Bedjet),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
cv.Optional(
|
||||
CONF_RECEIVE_TIMEOUT, default="0s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
.extend(ble_client.BLE_CLIENT_SCHEMA)
|
||||
.extend(cv.polling_component_schema("30s"))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await climate.register_climate(var, config)
|
||||
await ble_client.register_ble_node(var, config)
|
||||
if CONF_TIME_ID in config:
|
||||
time_ = await cg.get_variable(config[CONF_TIME_ID])
|
||||
cg.add(var.set_time_id(time_))
|
||||
if CONF_RECEIVE_TIMEOUT in config:
|
||||
cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT]))
|
||||
1
esphome/components/bl0939/__init__.py
Normal file
1
esphome/components/bl0939/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@ziceva"]
|
||||
144
esphome/components/bl0939/bl0939.cpp
Normal file
144
esphome/components/bl0939/bl0939.cpp
Normal file
@@ -0,0 +1,144 @@
|
||||
#include "bl0939.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
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)
|
||||
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;
|
||||
|
||||
static const uint8_t BL0939_WRITE_COMMAND = 0xA5; // 0xA{A4,A3,A2,A1}
|
||||
static const uint8_t BL0939_REG_IA_FAST_RMS_CTRL = 0x10;
|
||||
static const uint8_t BL0939_REG_IB_FAST_RMS_CTRL = 0x1E;
|
||||
static const uint8_t BL0939_REG_MODE = 0x18;
|
||||
static const uint8_t BL0939_REG_SOFT_RESET = 0x19;
|
||||
static const uint8_t BL0939_REG_USR_WRPROT = 0x1A;
|
||||
static const uint8_t BL0939_REG_TPS_CTRL = 0x1B;
|
||||
|
||||
const uint8_t BL0939_INIT[6][6] = {
|
||||
// Reset to default
|
||||
{BL0939_WRITE_COMMAND, BL0939_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x33},
|
||||
// Enable User Operation Write
|
||||
{BL0939_WRITE_COMMAND, BL0939_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xEB},
|
||||
// 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS
|
||||
{BL0939_WRITE_COMMAND, BL0939_REG_MODE, 0x00, 0x10, 0x00, 0x32},
|
||||
// 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS
|
||||
{BL0939_WRITE_COMMAND, BL0939_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xF9},
|
||||
// 0x181C = Half cycle, Fast RMS threshold 6172
|
||||
{BL0939_WRITE_COMMAND, BL0939_REG_IA_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x16},
|
||||
// 0x181C = Half cycle, Fast RMS threshold 6172
|
||||
{BL0939_WRITE_COMMAND, BL0939_REG_IB_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x08}};
|
||||
|
||||
void BL0939::loop() {
|
||||
DataPacket buffer;
|
||||
if (!this->available()) {
|
||||
return;
|
||||
}
|
||||
if (read_array((uint8_t *) &buffer, sizeof(buffer))) {
|
||||
if (validate_checksum(&buffer)) {
|
||||
received_package_(&buffer);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message");
|
||||
while (read() >= 0)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
bool BL0939::validate_checksum(const DataPacket *data) {
|
||||
uint8_t checksum = BL0939_READ_COMMAND;
|
||||
// Whole package but checksum
|
||||
for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) {
|
||||
checksum += data->raw[i];
|
||||
}
|
||||
checksum ^= 0xFF;
|
||||
if (checksum != data->checksum) {
|
||||
ESP_LOGW(TAG, "BL0939 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum);
|
||||
}
|
||||
return checksum == data->checksum;
|
||||
}
|
||||
|
||||
void BL0939::update() {
|
||||
this->flush();
|
||||
this->write_byte(BL0939_READ_COMMAND);
|
||||
this->write_byte(BL0939_FULL_PACKET);
|
||||
}
|
||||
|
||||
void BL0939::setup() {
|
||||
for (auto *i : BL0939_INIT) {
|
||||
this->write_array(i, 6);
|
||||
delay(1);
|
||||
}
|
||||
this->flush();
|
||||
}
|
||||
|
||||
void BL0939::received_package_(const DataPacket *data) const {
|
||||
// Bad header
|
||||
if (data->frame_header != BL0939_PACKET_HEADER) {
|
||||
ESP_LOGI("bl0939", "Invalid data. Header mismatch: %d", data->frame_header);
|
||||
return;
|
||||
}
|
||||
|
||||
float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_;
|
||||
float ia_rms = (float) to_uint32_t(data->ia_rms) / current_reference_;
|
||||
float ib_rms = (float) to_uint32_t(data->ib_rms) / current_reference_;
|
||||
float a_watt = (float) to_int32_t(data->a_watt) / power_reference_;
|
||||
float b_watt = (float) to_int32_t(data->b_watt) / power_reference_;
|
||||
int32_t cfa_cnt = to_int32_t(data->cfa_cnt);
|
||||
int32_t cfb_cnt = to_int32_t(data->cfb_cnt);
|
||||
float a_energy_consumption = (float) cfa_cnt / energy_reference_;
|
||||
float b_energy_consumption = (float) cfb_cnt / energy_reference_;
|
||||
float total_energy_consumption = a_energy_consumption + b_energy_consumption;
|
||||
|
||||
if (voltage_sensor_ != nullptr) {
|
||||
voltage_sensor_->publish_state(v_rms);
|
||||
}
|
||||
if (current_sensor_1_ != nullptr) {
|
||||
current_sensor_1_->publish_state(ia_rms);
|
||||
}
|
||||
if (current_sensor_2_ != nullptr) {
|
||||
current_sensor_2_->publish_state(ib_rms);
|
||||
}
|
||||
if (power_sensor_1_ != nullptr) {
|
||||
power_sensor_1_->publish_state(a_watt);
|
||||
}
|
||||
if (power_sensor_2_ != nullptr) {
|
||||
power_sensor_2_->publish_state(b_watt);
|
||||
}
|
||||
if (energy_sensor_1_ != nullptr) {
|
||||
energy_sensor_1_->publish_state(a_energy_consumption);
|
||||
}
|
||||
if (energy_sensor_2_ != nullptr) {
|
||||
energy_sensor_2_->publish_state(b_energy_consumption);
|
||||
}
|
||||
if (energy_sensor_sum_ != nullptr) {
|
||||
energy_sensor_sum_->publish_state(total_energy_consumption);
|
||||
}
|
||||
|
||||
ESP_LOGV("bl0939", "BL0939: U %fV, I1 %fA, I2 %fA, P1 %fW, P2 %fW, CntA %d, CntB %d, ∫P1 %fkWh, ∫P2 %fkWh", v_rms,
|
||||
ia_rms, ib_rms, a_watt, b_watt, cfa_cnt, cfb_cnt, a_energy_consumption, b_energy_consumption);
|
||||
}
|
||||
|
||||
void BL0939::dump_config() { // NOLINT(readability-function-cognitive-complexity)
|
||||
ESP_LOGCONFIG(TAG, "BL0939:");
|
||||
LOG_SENSOR("", "Voltage", this->voltage_sensor_);
|
||||
LOG_SENSOR("", "Current 1", this->current_sensor_1_);
|
||||
LOG_SENSOR("", "Current 2", this->current_sensor_2_);
|
||||
LOG_SENSOR("", "Power 1", this->power_sensor_1_);
|
||||
LOG_SENSOR("", "Power 2", this->power_sensor_2_);
|
||||
LOG_SENSOR("", "Energy 1", this->energy_sensor_1_);
|
||||
LOG_SENSOR("", "Energy 2", this->energy_sensor_2_);
|
||||
LOG_SENSOR("", "Energy sum", this->energy_sensor_sum_);
|
||||
}
|
||||
|
||||
uint32_t BL0939::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; }
|
||||
|
||||
int32_t BL0939::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; }
|
||||
|
||||
} // namespace bl0939
|
||||
} // namespace esphome
|
||||
107
esphome/components/bl0939/bl0939.h
Normal file
107
esphome/components/bl0939/bl0939.h
Normal file
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
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)
|
||||
// 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)
|
||||
static const float BL0939_IREF = 324004 * 1 / 1.218;
|
||||
static const float BL0939_UREF = 79931 * 0.51 * 1000 / (1.218 * (5 * 390 + 0.51));
|
||||
static const float BL0939_PREF = 4046 * 1 * 0.51 * 1000 / (1.218 * 1.218 * (5 * 390 + 0.51));
|
||||
static const float BL0939_EREF = 3.6e6 * 4046 * 1 * 0.51 * 1000 / (1638.4 * 256 * 1.218 * 1.218 * (5 * 390 + 0.51));
|
||||
|
||||
struct ube24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align)
|
||||
uint8_t l;
|
||||
uint8_t m;
|
||||
uint8_t h;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ube16_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align)
|
||||
uint8_t l;
|
||||
uint8_t h;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align)
|
||||
uint8_t l;
|
||||
uint8_t m;
|
||||
int8_t h;
|
||||
} __attribute__((packed));
|
||||
|
||||
// Caveat: All these values are big endian (low - middle - high)
|
||||
|
||||
union DataPacket { // NOLINT(altera-struct-pack-align)
|
||||
uint8_t raw[35];
|
||||
struct {
|
||||
uint8_t frame_header; // 0x55 according to docs
|
||||
ube24_t ia_fast_rms;
|
||||
ube24_t ia_rms;
|
||||
ube24_t ib_rms;
|
||||
ube24_t v_rms;
|
||||
ube24_t ib_fast_rms;
|
||||
sbe24_t a_watt;
|
||||
sbe24_t b_watt;
|
||||
sbe24_t cfa_cnt;
|
||||
sbe24_t cfb_cnt;
|
||||
ube16_t tps1;
|
||||
uint8_t RESERVED1; // value of 0x00
|
||||
ube16_t tps2;
|
||||
uint8_t RESERVED2; // value of 0x00
|
||||
uint8_t checksum; // checksum
|
||||
};
|
||||
} __attribute__((packed));
|
||||
|
||||
class BL0939 : public PollingComponent, public uart::UARTDevice {
|
||||
public:
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
||||
void set_current_sensor_1(sensor::Sensor *current_sensor_1) { current_sensor_1_ = current_sensor_1; }
|
||||
void set_current_sensor_2(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; }
|
||||
void set_power_sensor_1(sensor::Sensor *power_sensor_1) { power_sensor_1_ = power_sensor_1; }
|
||||
void set_power_sensor_2(sensor::Sensor *power_sensor_2) { power_sensor_2_ = power_sensor_2; }
|
||||
void set_energy_sensor_1(sensor::Sensor *energy_sensor_1) { energy_sensor_1_ = energy_sensor_1; }
|
||||
void set_energy_sensor_2(sensor::Sensor *energy_sensor_2) { energy_sensor_2_ = energy_sensor_2; }
|
||||
void set_energy_sensor_sum(sensor::Sensor *energy_sensor_sum) { energy_sensor_sum_ = energy_sensor_sum; }
|
||||
|
||||
void loop() override;
|
||||
|
||||
void update() override;
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
sensor::Sensor *voltage_sensor_;
|
||||
sensor::Sensor *current_sensor_1_;
|
||||
sensor::Sensor *current_sensor_2_;
|
||||
// NB This may be negative as the circuits is seemingly able to measure
|
||||
// power in both directions
|
||||
sensor::Sensor *power_sensor_1_;
|
||||
sensor::Sensor *power_sensor_2_;
|
||||
sensor::Sensor *energy_sensor_1_;
|
||||
sensor::Sensor *energy_sensor_2_;
|
||||
sensor::Sensor *energy_sensor_sum_;
|
||||
|
||||
// Divide by this to turn into Watt
|
||||
float power_reference_ = BL0939_PREF;
|
||||
// Divide by this to turn into Volt
|
||||
float voltage_reference_ = BL0939_UREF;
|
||||
// Divide by this to turn into Ampere
|
||||
float current_reference_ = BL0939_IREF;
|
||||
// Divide by this to turn into kWh
|
||||
float energy_reference_ = BL0939_EREF;
|
||||
|
||||
static uint32_t to_uint32_t(ube24_t input);
|
||||
|
||||
static int32_t to_int32_t(sbe24_t input);
|
||||
|
||||
static bool validate_checksum(const DataPacket *data);
|
||||
|
||||
void received_package_(const DataPacket *data) const;
|
||||
};
|
||||
} // namespace bl0939
|
||||
} // namespace esphome
|
||||
123
esphome/components/bl0939/sensor.py
Normal file
123
esphome/components/bl0939/sensor.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, uart
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_VOLTAGE,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_AMPERE,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
CONF_CURRENT_1 = "current_1"
|
||||
CONF_CURRENT_2 = "current_2"
|
||||
CONF_ACTIVE_POWER_1 = "active_power_1"
|
||||
CONF_ACTIVE_POWER_2 = "active_power_2"
|
||||
CONF_ENERGY_1 = "energy_1"
|
||||
CONF_ENERGY_2 = "energy_2"
|
||||
CONF_ENERGY_TOTAL = "energy_total"
|
||||
|
||||
bl0939_ns = cg.esphome_ns.namespace("bl0939")
|
||||
BL0939 = bl0939_ns.class_("BL0939", cg.PollingComponent, uart.UARTDevice)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BL0939),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT_1): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT_2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_ACTIVE_POWER_1): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_ACTIVE_POWER_2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_ENERGY_1): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT_HOURS,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
),
|
||||
cv.Optional(CONF_ENERGY_2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT_HOURS,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
),
|
||||
cv.Optional(CONF_ENERGY_TOTAL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT_HOURS,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
if CONF_VOLTAGE in config:
|
||||
conf = config[CONF_VOLTAGE]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_voltage_sensor(sens))
|
||||
if CONF_CURRENT_1 in config:
|
||||
conf = config[CONF_CURRENT_1]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_current_sensor_1(sens))
|
||||
if CONF_CURRENT_2 in config:
|
||||
conf = config[CONF_CURRENT_2]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_current_sensor_2(sens))
|
||||
if CONF_ACTIVE_POWER_1 in config:
|
||||
conf = config[CONF_ACTIVE_POWER_1]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_power_sensor_1(sens))
|
||||
if CONF_ACTIVE_POWER_2 in config:
|
||||
conf = config[CONF_ACTIVE_POWER_2]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_power_sensor_2(sens))
|
||||
if CONF_ENERGY_1 in config:
|
||||
conf = config[CONF_ENERGY_1]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_energy_sensor_1(sens))
|
||||
if CONF_ENERGY_2 in config:
|
||||
conf = config[CONF_ENERGY_2]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_energy_sensor_2(sens))
|
||||
if CONF_ENERGY_TOTAL in config:
|
||||
conf = config[CONF_ENERGY_TOTAL]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_energy_sensor_sum(sens))
|
||||
@@ -118,16 +118,21 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
|
||||
this->set_states_(espbt::ClientState::IDLE);
|
||||
break;
|
||||
}
|
||||
this->conn_id = param->open.conn_id;
|
||||
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->open.conn_id);
|
||||
break;
|
||||
}
|
||||
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;
|
||||
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=%d", ret);
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%x", ret);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CFG_MTU_EVT: {
|
||||
if (param->cfg_mtu.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "cfg_mtu to %s failed, status %d", this->address_str().c_str(), param->cfg_mtu.status);
|
||||
ESP_LOGW(TAG, "cfg_mtu to %s failed, mtu %d, status %d", this->address_str().c_str(), param->cfg_mtu.mtu,
|
||||
param->cfg_mtu.status);
|
||||
this->set_states_(espbt::ClientState::IDLE);
|
||||
break;
|
||||
}
|
||||
@@ -139,7 +144,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
|
||||
if (memcmp(param->disconnect.remote_bda, this->remote_bda, 6) != 0) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT", this->address_str().c_str());
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->address_str().c_str(), param->disconnect.reason);
|
||||
for (auto &svc : this->services_)
|
||||
delete svc; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
this->services_.clear();
|
||||
@@ -201,6 +206,32 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
|
||||
}
|
||||
}
|
||||
|
||||
void BLEClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
switch (event) {
|
||||
// This event is sent by the server when it requests security
|
||||
case ESP_GAP_BLE_SEC_REQ_EVT:
|
||||
ESP_LOGV(TAG, "ESP_GAP_BLE_SEC_REQ_EVT %x", event);
|
||||
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
|
||||
break;
|
||||
// This event is sent once authentication has completed
|
||||
case ESP_GAP_BLE_AUTH_CMPL_EVT:
|
||||
esp_bd_addr_t bd_addr;
|
||||
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
|
||||
ESP_LOGI(TAG, "auth complete. remote BD_ADDR: %s", format_hex(bd_addr, 6).c_str());
|
||||
if (!param->ble_security.auth_cmpl.success) {
|
||||
ESP_LOGE(TAG, "auth fail reason = 0x%x", param->ble_security.auth_cmpl.fail_reason);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "auth success. address type = %d auth mode = %d", param->ble_security.auth_cmpl.addr_type,
|
||||
param->ble_security.auth_cmpl.auth_mode);
|
||||
}
|
||||
break;
|
||||
// There are other events we'll want to implement at some point to support things like pass key
|
||||
// https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse GATT values into a float for a sensor.
|
||||
// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/
|
||||
float BLEClient::parse_char_value(uint8_t *value, uint16_t length) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gattc_api.h>
|
||||
#include <esp_bt_defs.h>
|
||||
#include <esp_gatt_common_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
@@ -86,6 +87,7 @@ class BLEClient : public espbt::ESPBTClient, public Component {
|
||||
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||
bool parse_device(const espbt::ESPBTDevice &device) override;
|
||||
void on_scan_end() override {}
|
||||
void connect() override;
|
||||
|
||||
@@ -81,6 +81,11 @@ static const char *iir_filter_to_str(BME280IIRFilter filter) {
|
||||
void BME280Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up BME280...");
|
||||
uint8_t chip_id = 0;
|
||||
|
||||
// Mark as not failed before initializing. Some devices will turn off sensors to save on batteries
|
||||
// and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component.
|
||||
this->component_state_ &= ~COMPONENT_STATE_FAILED;
|
||||
|
||||
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
|
||||
@@ -169,6 +169,14 @@ void BME680BSECComponent::loop() {
|
||||
} else {
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
// Process a single action from the queue. These are primarily sensor state publishes
|
||||
// that in totality take too long to send in a single call.
|
||||
if (this->queue_.size()) {
|
||||
auto action = std::move(this->queue_.front());
|
||||
this->queue_.pop();
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
void BME680BSECComponent::run_() {
|
||||
@@ -306,37 +314,39 @@ void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme
|
||||
}
|
||||
|
||||
void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) {
|
||||
ESP_LOGV(TAG, "Publishing sensor states");
|
||||
ESP_LOGV(TAG, "Queuing sensor state publish actions");
|
||||
for (uint8_t i = 0; i < num_outputs; i++) {
|
||||
float signal = outputs[i].signal;
|
||||
switch (outputs[i].sensor_id) {
|
||||
case BSEC_OUTPUT_IAQ:
|
||||
case BSEC_OUTPUT_STATIC_IAQ:
|
||||
uint8_t accuracy;
|
||||
accuracy = outputs[i].accuracy;
|
||||
this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal);
|
||||
this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
|
||||
this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true);
|
||||
case BSEC_OUTPUT_STATIC_IAQ: {
|
||||
uint8_t accuracy = outputs[i].accuracy;
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
|
||||
this->queue_push_([this, accuracy]() {
|
||||
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
|
||||
});
|
||||
this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
|
||||
|
||||
// Queue up an opportunity to save state
|
||||
this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); });
|
||||
break;
|
||||
this->queue_push_([this, accuracy]() { this->save_state_(accuracy); });
|
||||
} break;
|
||||
case BSEC_OUTPUT_CO2_EQUIVALENT:
|
||||
this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal);
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); });
|
||||
break;
|
||||
case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
|
||||
this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal);
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); });
|
||||
break;
|
||||
case BSEC_OUTPUT_RAW_PRESSURE:
|
||||
this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f);
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); });
|
||||
break;
|
||||
case BSEC_OUTPUT_RAW_GAS:
|
||||
this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal);
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); });
|
||||
break;
|
||||
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE:
|
||||
this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal);
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); });
|
||||
break;
|
||||
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY:
|
||||
this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal);
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); });
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -352,14 +362,14 @@ int64_t BME680BSECComponent::get_time_ns_() {
|
||||
return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000);
|
||||
}
|
||||
|
||||
void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) {
|
||||
void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, bool change_only) {
|
||||
if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) {
|
||||
return;
|
||||
}
|
||||
sensor->publish_state(value);
|
||||
}
|
||||
|
||||
void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) {
|
||||
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) {
|
||||
if (!sensor || (sensor->has_state() && sensor->state == value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,12 +70,14 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
|
||||
void publish_(const bsec_output_t *outputs, uint8_t num_outputs);
|
||||
int64_t get_time_ns_();
|
||||
|
||||
void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false);
|
||||
void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value);
|
||||
void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false);
|
||||
void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value);
|
||||
|
||||
void load_state_();
|
||||
void save_state_(uint8_t accuracy);
|
||||
|
||||
void queue_push_(std::function<void()> &&f) { this->queue_.push(std::move(f)); }
|
||||
|
||||
struct bme680_dev bme680_;
|
||||
bsec_library_return_t bsec_status_{BSEC_OK};
|
||||
int8_t bme680_status_{BME680_OK};
|
||||
@@ -84,6 +86,8 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
|
||||
uint32_t millis_overflow_counter_{0};
|
||||
int64_t next_call_ns_{0};
|
||||
|
||||
std::queue<std::function<void()>> queue_;
|
||||
|
||||
ESPPreferenceObject bsec_state_;
|
||||
uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day
|
||||
uint32_t last_state_save_ms_ = 0;
|
||||
|
||||
@@ -10,6 +10,7 @@ IS_PLATFORM_COMPONENT = True
|
||||
CONF_CAN_ID = "can_id"
|
||||
CONF_CAN_ID_MASK = "can_id_mask"
|
||||
CONF_USE_EXTENDED_ID = "use_extended_id"
|
||||
CONF_REMOTE_TRANSMISSION_REQUEST = "remote_transmission_request"
|
||||
CONF_CANBUS_ID = "canbus_id"
|
||||
CONF_BIT_RATE = "bit_rate"
|
||||
CONF_ON_FRAME = "on_frame"
|
||||
@@ -77,6 +78,7 @@ CANBUS_SCHEMA = cv.Schema(
|
||||
min=0, max=0x1FFFFFFF
|
||||
),
|
||||
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
|
||||
cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST): cv.boolean,
|
||||
},
|
||||
validate_id,
|
||||
),
|
||||
@@ -99,10 +101,20 @@ async def setup_canbus_core_(var, config):
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id
|
||||
)
|
||||
if CONF_REMOTE_TRANSMISSION_REQUEST in conf:
|
||||
cg.add(
|
||||
trigger.set_remote_transmission_request(
|
||||
conf[CONF_REMOTE_TRANSMISSION_REQUEST]
|
||||
)
|
||||
)
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(
|
||||
trigger,
|
||||
[(cg.std_vector.template(cg.uint8), "x"), (cg.uint32, "can_id")],
|
||||
[
|
||||
(cg.std_vector.template(cg.uint8), "x"),
|
||||
(cg.uint32, "can_id"),
|
||||
(cg.bool_, "remote_transmission_request"),
|
||||
],
|
||||
conf,
|
||||
)
|
||||
|
||||
@@ -122,6 +134,7 @@ async def register_canbus(var, config):
|
||||
cv.GenerateID(CONF_CANBUS_ID): cv.use_id(CanbusComponent),
|
||||
cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
|
||||
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
|
||||
cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST, default=False): cv.boolean,
|
||||
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
|
||||
},
|
||||
validate_id,
|
||||
@@ -140,6 +153,11 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
cg.add(var.set_use_extended_id(use_extended_id))
|
||||
|
||||
remote_transmission_request = await cg.templatable(
|
||||
config[CONF_REMOTE_TRANSMISSION_REQUEST], args, bool
|
||||
)
|
||||
cg.add(var.set_remote_transmission_request(remote_transmission_request))
|
||||
|
||||
data = config[CONF_DATA]
|
||||
if isinstance(data, bytes):
|
||||
data = [int(x) for x in data]
|
||||
|
||||
@@ -22,20 +22,22 @@ void Canbus::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
void Canbus::send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data) {
|
||||
void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
|
||||
const std::vector<uint8_t> &data) {
|
||||
struct CanFrame can_message;
|
||||
|
||||
uint8_t size = static_cast<uint8_t>(data.size());
|
||||
if (use_extended_id) {
|
||||
ESP_LOGD(TAG, "send extended id=0x%08x size=%d", can_id, size);
|
||||
ESP_LOGD(TAG, "send extended id=0x%08x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "send extended id=0x%03x size=%d", can_id, size);
|
||||
ESP_LOGD(TAG, "send extended id=0x%03x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
|
||||
}
|
||||
if (size > CAN_MAX_DATA_LENGTH)
|
||||
size = CAN_MAX_DATA_LENGTH;
|
||||
can_message.can_data_length_code = size;
|
||||
can_message.can_id = can_id;
|
||||
can_message.use_extended_id = use_extended_id;
|
||||
can_message.remote_transmission_request = remote_transmission_request;
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
can_message.data[i] = data[i];
|
||||
@@ -79,8 +81,10 @@ void Canbus::loop() {
|
||||
// fire all triggers
|
||||
for (auto *trigger : this->triggers_) {
|
||||
if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) &&
|
||||
(trigger->use_extended_id_ == can_message.use_extended_id)) {
|
||||
trigger->trigger(data, can_message.can_id);
|
||||
(trigger->use_extended_id_ == can_message.use_extended_id) &&
|
||||
(!trigger->remote_transmission_request_.has_value() ||
|
||||
trigger->remote_transmission_request_.value() == can_message.remote_transmission_request)) {
|
||||
trigger->trigger(data, can_message.can_id, can_message.remote_transmission_request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,12 @@ class Canbus : public Component {
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
void loop() override;
|
||||
|
||||
void send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data);
|
||||
void send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
|
||||
const std::vector<uint8_t> &data);
|
||||
void send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data) {
|
||||
// for backwards compatibility only
|
||||
this->send_data(can_id, use_extended_id, false, data);
|
||||
}
|
||||
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
|
||||
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
|
||||
void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
|
||||
@@ -96,33 +101,43 @@ template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public P
|
||||
|
||||
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
|
||||
|
||||
void set_remote_transmission_request(bool remote_transmission_request) {
|
||||
this->remote_transmission_request_ = remote_transmission_request;
|
||||
}
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
|
||||
auto use_extended_id =
|
||||
this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;
|
||||
if (this->static_) {
|
||||
this->parent_->send_data(can_id, use_extended_id, this->data_static_);
|
||||
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, this->data_static_);
|
||||
} else {
|
||||
auto val = this->data_func_(x...);
|
||||
this->parent_->send_data(can_id, use_extended_id, val);
|
||||
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, val);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
optional<uint32_t> can_id_{};
|
||||
optional<bool> use_extended_id_{};
|
||||
bool remote_transmission_request_{false};
|
||||
bool static_{false};
|
||||
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
|
||||
std::vector<uint8_t> data_static_{};
|
||||
};
|
||||
|
||||
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t>, public Component {
|
||||
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {
|
||||
friend class Canbus;
|
||||
|
||||
public:
|
||||
explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask,
|
||||
const bool use_extended_id)
|
||||
: parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id){};
|
||||
|
||||
void set_remote_transmission_request(bool remote_transmission_request) {
|
||||
this->remote_transmission_request_ = remote_transmission_request;
|
||||
}
|
||||
|
||||
void setup() override { this->parent_->add_trigger(this); }
|
||||
|
||||
protected:
|
||||
@@ -130,6 +145,7 @@ class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t>, public Com
|
||||
uint32_t can_id_;
|
||||
uint32_t can_id_mask_;
|
||||
bool use_extended_id_;
|
||||
optional<bool> remote_transmission_request_{};
|
||||
};
|
||||
|
||||
} // namespace canbus
|
||||
|
||||
@@ -39,17 +39,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
|
||||
if (request->method() == HTTP_GET) {
|
||||
if (request->url() == "/")
|
||||
return true;
|
||||
if (request->url() == "/stylesheet.css")
|
||||
return true;
|
||||
if (request->url() == "/wifi-strength-1.svg")
|
||||
return true;
|
||||
if (request->url() == "/wifi-strength-2.svg")
|
||||
return true;
|
||||
if (request->url() == "/wifi-strength-3.svg")
|
||||
return true;
|
||||
if (request->url() == "/wifi-strength-4.svg")
|
||||
return true;
|
||||
if (request->url() == "/lock.svg")
|
||||
if (request->url() == "/config.json")
|
||||
return true;
|
||||
if (request->url() == "/wifisave")
|
||||
return true;
|
||||
|
||||
@@ -287,9 +287,11 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
|
||||
cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
|
||||
validate_climate_fan_mode
|
||||
),
|
||||
cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.string_strict,
|
||||
cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.templatable(
|
||||
cv.string_strict
|
||||
),
|
||||
cv.Exclusive(CONF_PRESET, "preset"): cv.templatable(validate_climate_preset),
|
||||
cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.string_strict,
|
||||
cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.templatable(cv.string_strict),
|
||||
cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode),
|
||||
}
|
||||
)
|
||||
@@ -324,13 +326,17 @@ async def climate_control_to_code(config, action_id, template_arg, args):
|
||||
template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
|
||||
cg.add(var.set_fan_mode(template_))
|
||||
if CONF_CUSTOM_FAN_MODE in config:
|
||||
template_ = await cg.templatable(config[CONF_CUSTOM_FAN_MODE], args, str)
|
||||
template_ = await cg.templatable(
|
||||
config[CONF_CUSTOM_FAN_MODE], args, cg.std_string
|
||||
)
|
||||
cg.add(var.set_custom_fan_mode(template_))
|
||||
if CONF_PRESET in config:
|
||||
template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset)
|
||||
cg.add(var.set_preset(template_))
|
||||
if CONF_CUSTOM_PRESET in config:
|
||||
template_ = await cg.templatable(config[CONF_CUSTOM_PRESET], args, str)
|
||||
template_ = await cg.templatable(
|
||||
config[CONF_CUSTOM_PRESET], args, cg.std_string
|
||||
)
|
||||
cg.add(var.set_custom_preset(template_))
|
||||
if CONF_SWING_MODE in config:
|
||||
template_ = await cg.templatable(
|
||||
|
||||
@@ -76,7 +76,7 @@ enum ClimateSwingMode : uint8_t {
|
||||
CLIMATE_SWING_HORIZONTAL = 3,
|
||||
};
|
||||
|
||||
/// Enum for all modes a climate swing can be in
|
||||
/// Enum for all preset modes
|
||||
enum ClimatePreset : uint8_t {
|
||||
/// No preset is active
|
||||
CLIMATE_PRESET_NONE = 0,
|
||||
@@ -108,7 +108,7 @@ const LogString *climate_fan_mode_to_string(ClimateFanMode mode);
|
||||
/// Convert the given ClimateSwingMode to a human-readable string.
|
||||
const LogString *climate_swing_mode_to_string(ClimateSwingMode mode);
|
||||
|
||||
/// Convert the given ClimateSwingMode to a human-readable string.
|
||||
/// Convert the given PresetMode to a human-readable string.
|
||||
const LogString *climate_preset_to_string(ClimatePreset preset);
|
||||
|
||||
} // namespace climate
|
||||
|
||||
@@ -141,7 +141,7 @@ class ClimateTraits {
|
||||
}
|
||||
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return supported_swing_modes_.count(swing_mode); }
|
||||
bool get_supports_swing_modes() const { return !supported_swing_modes_.empty(); }
|
||||
std::set<ClimateSwingMode> get_supported_swing_modes() { return supported_swing_modes_; }
|
||||
std::set<ClimateSwingMode> get_supported_swing_modes() const { return supported_swing_modes_; }
|
||||
|
||||
float get_visual_min_temperature() const { return visual_min_temperature_; }
|
||||
void set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; }
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace copy {
|
||||
static const char *const TAG = "copy.select";
|
||||
|
||||
void CopySelect::setup() {
|
||||
source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); });
|
||||
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
|
||||
|
||||
traits.set_options(source_->traits.get_options());
|
||||
|
||||
|
||||
@@ -142,7 +142,6 @@ void IRAM_ATTR ESPOneWire::select(uint64_t address) {
|
||||
void IRAM_ATTR ESPOneWire::reset_search() {
|
||||
this->last_discrepancy_ = 0;
|
||||
this->last_device_flag_ = false;
|
||||
this->last_family_discrepancy_ = 0;
|
||||
this->rom_number_ = 0;
|
||||
}
|
||||
uint64_t IRAM_ATTR ESPOneWire::search() {
|
||||
@@ -195,9 +194,6 @@ uint64_t IRAM_ATTR ESPOneWire::search() {
|
||||
|
||||
if (!branch) {
|
||||
last_zero = id_bit_number;
|
||||
if (last_zero < 9) {
|
||||
this->last_discrepancy_ = last_zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ class ESPOneWire {
|
||||
|
||||
ISRInternalGPIOPin pin_;
|
||||
uint8_t last_discrepancy_{0};
|
||||
uint8_t last_family_discrepancy_{0};
|
||||
bool last_device_flag_{false};
|
||||
uint64_t rom_number_{0};
|
||||
};
|
||||
|
||||
@@ -98,6 +98,8 @@ CELL_VOLTAGE_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon=ICON_FLASH,
|
||||
accuracy_decimals=3,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
|
||||
@@ -64,7 +64,10 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N
|
||||
config = {
|
||||
"substitutions": {"name": name},
|
||||
"packages": {project_name: import_url},
|
||||
"esphome": {"name_add_mac_suffix": False},
|
||||
"esphome": {
|
||||
"name": "${name}",
|
||||
"name_add_mac_suffix": False,
|
||||
},
|
||||
}
|
||||
p.write_text(
|
||||
dump(config) + WIFI_CONFIG,
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import time
|
||||
import esphome.config_validation as cv
|
||||
from esphome import pins, automation
|
||||
from esphome.const import (
|
||||
CONF_HOUR,
|
||||
CONF_ID,
|
||||
CONF_MINUTE,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
CONF_PINS,
|
||||
CONF_RUN_DURATION,
|
||||
CONF_SECOND,
|
||||
CONF_SLEEP_DURATION,
|
||||
CONF_TIME_ID,
|
||||
CONF_WAKEUP_PIN,
|
||||
)
|
||||
|
||||
@@ -15,6 +20,7 @@ from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32S2,
|
||||
)
|
||||
|
||||
WAKEUP_PINS = {
|
||||
@@ -39,6 +45,30 @@ WAKEUP_PINS = {
|
||||
39,
|
||||
],
|
||||
VARIANT_ESP32C3: [0, 1, 2, 3, 4, 5],
|
||||
VARIANT_ESP32S2: [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +93,14 @@ deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
|
||||
DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component)
|
||||
EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action)
|
||||
PreventDeepSleepAction = deep_sleep_ns.class_(
|
||||
"PreventDeepSleepAction", automation.Action
|
||||
"PreventDeepSleepAction",
|
||||
automation.Action,
|
||||
cg.Parented.template(DeepSleepComponent),
|
||||
)
|
||||
AllowDeepSleepAction = deep_sleep_ns.class_(
|
||||
"AllowDeepSleepAction",
|
||||
automation.Action,
|
||||
cg.Parented.template(DeepSleepComponent),
|
||||
)
|
||||
|
||||
WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode")
|
||||
@@ -87,6 +124,7 @@ CONF_TOUCH_WAKEUP = "touch_wakeup"
|
||||
CONF_DEFAULT = "default"
|
||||
CONF_GPIO_WAKEUP_REASON = "gpio_wakeup_reason"
|
||||
CONF_TOUCH_WAKEUP_REASON = "touch_wakeup_reason"
|
||||
CONF_UNTIL = "until"
|
||||
|
||||
WAKEUP_CAUSES_SCHEMA = cv.Schema(
|
||||
{
|
||||
@@ -177,20 +215,30 @@ async def to_code(config):
|
||||
cg.add_define("USE_DEEP_SLEEP")
|
||||
|
||||
|
||||
DEEP_SLEEP_ENTER_SCHEMA = automation.maybe_simple_id(
|
||||
DEEP_SLEEP_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DeepSleepComponent),
|
||||
cv.Optional(CONF_SLEEP_DURATION): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
DEEP_SLEEP_PREVENT_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DeepSleepComponent),
|
||||
}
|
||||
DEEP_SLEEP_ENTER_SCHEMA = cv.All(
|
||||
automation.maybe_simple_id(
|
||||
DEEP_SLEEP_ACTION_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
),
|
||||
# Only on ESP32 due to how long the RTC on ESP8266 can stay asleep
|
||||
cv.Exclusive(CONF_UNTIL, "time"): cv.All(
|
||||
cv.only_on_esp32, cv.time_of_day
|
||||
),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID),
|
||||
)
|
||||
|
||||
|
||||
@@ -203,12 +251,28 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args):
|
||||
if CONF_SLEEP_DURATION in config:
|
||||
template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32)
|
||||
cg.add(var.set_sleep_duration(template_))
|
||||
|
||||
if CONF_UNTIL in config:
|
||||
until = config[CONF_UNTIL]
|
||||
cg.add(var.set_until(until[CONF_HOUR], until[CONF_MINUTE], until[CONF_SECOND]))
|
||||
|
||||
time_ = await cg.get_variable(config[CONF_TIME_ID])
|
||||
cg.add(var.set_time(time_))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"deep_sleep.prevent", PreventDeepSleepAction, DEEP_SLEEP_PREVENT_SCHEMA
|
||||
"deep_sleep.prevent",
|
||||
PreventDeepSleepAction,
|
||||
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
|
||||
)
|
||||
async def deep_sleep_prevent_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
@automation.register_action(
|
||||
"deep_sleep.allow",
|
||||
AllowDeepSleepAction,
|
||||
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
|
||||
)
|
||||
async def deep_sleep_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "deep_sleep_component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include <Esp.h>
|
||||
@@ -20,6 +21,7 @@ optional<uint32_t> DeepSleepComponent::get_run_duration_() const {
|
||||
switch (wakeup_cause) {
|
||||
case ESP_SLEEP_WAKEUP_EXT0:
|
||||
case ESP_SLEEP_WAKEUP_EXT1:
|
||||
case ESP_SLEEP_WAKEUP_GPIO:
|
||||
return this->wakeup_cause_to_run_duration_->gpio_cause;
|
||||
case ESP_SLEEP_WAKEUP_TOUCHPAD:
|
||||
return this->wakeup_cause_to_run_duration_->touch_cause;
|
||||
@@ -71,16 +73,27 @@ float DeepSleepComponent::get_loop_priority() const {
|
||||
return -100.0f; // run after everything else is ready
|
||||
}
|
||||
void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; }
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32)
|
||||
void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) {
|
||||
this->wakeup_pin_mode_ = wakeup_pin_mode;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32)
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
|
||||
void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; }
|
||||
|
||||
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
|
||||
|
||||
#endif
|
||||
|
||||
void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) {
|
||||
wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; }
|
||||
void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
if (this->prevent_ && !manual) {
|
||||
@@ -101,10 +114,13 @@ void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
#endif
|
||||
|
||||
ESP_LOGI(TAG, "Beginning Deep Sleep");
|
||||
if (this->sleep_duration_.has_value())
|
||||
ESP_LOGI(TAG, "Sleeping for %" PRId64 "us", *this->sleep_duration_);
|
||||
|
||||
App.run_safe_shutdown_hooks();
|
||||
|
||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
#if defined(USE_ESP32)
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
if (this->sleep_duration_.has_value())
|
||||
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
|
||||
if (this->wakeup_pin_ != nullptr) {
|
||||
@@ -122,10 +138,7 @@ void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
esp_sleep_enable_touchpad_wakeup();
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
}
|
||||
|
||||
esp_deep_sleep_start();
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32C3
|
||||
if (this->sleep_duration_.has_value())
|
||||
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
|
||||
@@ -134,9 +147,12 @@ void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
|
||||
level = !level;
|
||||
}
|
||||
esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level);
|
||||
esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()),
|
||||
static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level));
|
||||
}
|
||||
#endif
|
||||
esp_deep_sleep_start();
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
ESP.deepSleep(*this->sleep_duration_); // NOLINT(readability-static-accessed-through-instance)
|
||||
@@ -144,6 +160,7 @@ void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
}
|
||||
float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; }
|
||||
void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; }
|
||||
void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; }
|
||||
|
||||
} // namespace deep_sleep
|
||||
} // namespace esphome
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#include <esp_sleep.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_TIME
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace deep_sleep {
|
||||
|
||||
@@ -66,17 +70,19 @@ class DeepSleepComponent : public Component {
|
||||
void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode);
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
#if defined(USE_ESP32)
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
|
||||
void set_ext1_wakeup(Ext1Wakeup ext1_wakeup);
|
||||
|
||||
void set_touch_wakeup(bool touch_wakeup);
|
||||
|
||||
#endif
|
||||
// Set the duration in ms for how long the code should run before entering
|
||||
// deep sleep mode, according to the cause the ESP32 has woken.
|
||||
void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration);
|
||||
|
||||
#endif
|
||||
|
||||
/// Set a duration in ms for how long the code should run before entering deep sleep mode.
|
||||
void set_run_duration(uint32_t time_ms);
|
||||
|
||||
@@ -90,6 +96,7 @@ class DeepSleepComponent : public Component {
|
||||
void begin_sleep(bool manual = false);
|
||||
|
||||
void prevent_deep_sleep();
|
||||
void allow_deep_sleep();
|
||||
|
||||
protected:
|
||||
// Returns nullopt if no run duration is set. Otherwise, returns the run
|
||||
@@ -116,25 +123,81 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> {
|
||||
EnterDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {}
|
||||
TEMPLATABLE_VALUE(uint32_t, sleep_duration);
|
||||
|
||||
#ifdef USE_TIME
|
||||
void set_until(uint8_t hour, uint8_t minute, uint8_t second) {
|
||||
this->hour_ = hour;
|
||||
this->minute_ = minute;
|
||||
this->second_ = second;
|
||||
}
|
||||
|
||||
void set_time(time::RealTimeClock *time) { this->time_ = time; }
|
||||
#endif
|
||||
|
||||
void play(Ts... x) override {
|
||||
if (this->sleep_duration_.has_value()) {
|
||||
this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...));
|
||||
}
|
||||
#ifdef USE_TIME
|
||||
|
||||
if (this->hour_.has_value()) {
|
||||
auto time = this->time_->now();
|
||||
const uint32_t timestamp_now = time.timestamp;
|
||||
|
||||
bool after_time = false;
|
||||
if (time.hour > this->hour_) {
|
||||
after_time = true;
|
||||
} else {
|
||||
if (time.hour == this->hour_) {
|
||||
if (time.minute > this->minute_) {
|
||||
after_time = true;
|
||||
} else {
|
||||
if (time.minute == this->minute_) {
|
||||
if (time.second > this->second_) {
|
||||
after_time = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
time.hour = *this->hour_;
|
||||
time.minute = *this->minute_;
|
||||
time.second = *this->second_;
|
||||
time.recalc_timestamp_utc();
|
||||
|
||||
time_t timestamp = time.timestamp; // timestamp in local time zone
|
||||
|
||||
if (after_time)
|
||||
timestamp += 60 * 60 * 24;
|
||||
|
||||
int32_t offset = time::ESPTime::timezone_offset();
|
||||
timestamp -= offset; // Change timestamp to utc
|
||||
const uint32_t ms_left = (timestamp - timestamp_now) * 1000;
|
||||
this->deep_sleep_->set_sleep_duration(ms_left);
|
||||
}
|
||||
#endif
|
||||
this->deep_sleep_->begin_sleep(true);
|
||||
}
|
||||
|
||||
protected:
|
||||
DeepSleepComponent *deep_sleep_;
|
||||
#ifdef USE_TIME
|
||||
optional<uint8_t> hour_;
|
||||
optional<uint8_t> minute_;
|
||||
optional<uint8_t> second_;
|
||||
|
||||
time::RealTimeClock *time_;
|
||||
#endif
|
||||
};
|
||||
|
||||
template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...> {
|
||||
template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
|
||||
public:
|
||||
PreventDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {}
|
||||
void play(Ts... x) override { this->parent_->prevent_deep_sleep(); }
|
||||
};
|
||||
|
||||
void play(Ts... x) override { this->deep_sleep_->prevent_deep_sleep(); }
|
||||
|
||||
protected:
|
||||
DeepSleepComponent *deep_sleep_;
|
||||
template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->allow_deep_sleep(); }
|
||||
};
|
||||
|
||||
} // namespace deep_sleep
|
||||
|
||||
1
esphome/components/delonghi/__init__.py
Normal file
1
esphome/components/delonghi/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@grob6000"]
|
||||
20
esphome/components/delonghi/climate.py
Normal file
20
esphome/components/delonghi/climate.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import climate_ir
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
|
||||
delonghi_ns = cg.esphome_ns.namespace("delonghi")
|
||||
DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR)
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DelonghiClimate),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await climate_ir.register_climate_ir(var, config)
|
||||
186
esphome/components/delonghi/delonghi.cpp
Normal file
186
esphome/components/delonghi/delonghi.cpp
Normal file
@@ -0,0 +1,186 @@
|
||||
#include "delonghi.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace delonghi {
|
||||
|
||||
static const char *const TAG = "delonghi.climate";
|
||||
|
||||
void DelonghiClimate::transmit_state() {
|
||||
uint8_t remote_state[DELONGHI_STATE_FRAME_SIZE] = {0};
|
||||
remote_state[0] = DELONGHI_ADDRESS;
|
||||
remote_state[1] = this->temperature_();
|
||||
remote_state[1] |= (this->fan_speed_()) << 5;
|
||||
remote_state[2] = this->operation_mode_();
|
||||
// Calculate checksum
|
||||
for (int i = 0; i < DELONGHI_STATE_FRAME_SIZE - 1; i++) {
|
||||
remote_state[DELONGHI_STATE_FRAME_SIZE - 1] += remote_state[i];
|
||||
}
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
data->set_carrier_frequency(DELONGHI_IR_FREQUENCY);
|
||||
|
||||
data->mark(DELONGHI_HEADER_MARK);
|
||||
data->space(DELONGHI_HEADER_SPACE);
|
||||
for (unsigned char b : remote_state) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(DELONGHI_BIT_MARK);
|
||||
bool bit = b & mask;
|
||||
data->space(bit ? DELONGHI_ONE_SPACE : DELONGHI_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
data->mark(DELONGHI_BIT_MARK);
|
||||
data->space(0);
|
||||
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
uint8_t DelonghiClimate::operation_mode_() {
|
||||
uint8_t operating_mode = DELONGHI_MODE_ON;
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
operating_mode |= DELONGHI_MODE_COOL;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
operating_mode |= DELONGHI_MODE_DRY;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
operating_mode |= DELONGHI_MODE_HEAT;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
operating_mode |= DELONGHI_MODE_AUTO;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
operating_mode |= DELONGHI_MODE_FAN;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
default:
|
||||
operating_mode = DELONGHI_MODE_OFF;
|
||||
break;
|
||||
}
|
||||
return operating_mode;
|
||||
}
|
||||
|
||||
uint16_t DelonghiClimate::fan_speed_() {
|
||||
uint16_t fan_speed;
|
||||
switch (this->fan_mode.value()) {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
fan_speed = DELONGHI_FAN_LOW;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
fan_speed = DELONGHI_FAN_MEDIUM;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
fan_speed = DELONGHI_FAN_HIGH;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
fan_speed = DELONGHI_FAN_AUTO;
|
||||
}
|
||||
return fan_speed;
|
||||
}
|
||||
|
||||
uint8_t DelonghiClimate::temperature_() {
|
||||
// Force special temperatures depending on the mode
|
||||
uint8_t temperature = 0b0001;
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_HEAT;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
default:
|
||||
temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_COOL;
|
||||
}
|
||||
if (temperature > 0x0F) {
|
||||
temperature = 0x0F; // clamp maximum
|
||||
}
|
||||
return temperature;
|
||||
}
|
||||
|
||||
bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) {
|
||||
uint8_t checksum = 0;
|
||||
for (int i = 0; i < (DELONGHI_STATE_FRAME_SIZE - 1); i++) {
|
||||
checksum += frame[i];
|
||||
}
|
||||
if (frame[DELONGHI_STATE_FRAME_SIZE - 1] != checksum) {
|
||||
return false;
|
||||
}
|
||||
uint8_t mode = frame[2] & 0x0F;
|
||||
if (mode & DELONGHI_MODE_ON) {
|
||||
switch (mode & 0x0E) {
|
||||
case DELONGHI_MODE_COOL:
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
break;
|
||||
case DELONGHI_MODE_DRY:
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
break;
|
||||
case DELONGHI_MODE_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
case DELONGHI_MODE_AUTO:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
case DELONGHI_MODE_FAN:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
}
|
||||
uint8_t temperature = frame[1] & 0x0F;
|
||||
if (this->mode == climate::CLIMATE_MODE_HEAT) {
|
||||
this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_HEAT;
|
||||
} else {
|
||||
this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_COOL;
|
||||
}
|
||||
uint8_t fan_mode = frame[1] >> 5;
|
||||
switch (fan_mode) {
|
||||
case DELONGHI_FAN_LOW:
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
break;
|
||||
case DELONGHI_FAN_MEDIUM:
|
||||
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
||||
break;
|
||||
case DELONGHI_FAN_HIGH:
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
break;
|
||||
case DELONGHI_FAN_AUTO:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
}
|
||||
this->publish_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DelonghiClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
uint8_t state_frame[DELONGHI_STATE_FRAME_SIZE] = {};
|
||||
if (!data.expect_item(DELONGHI_HEADER_MARK, DELONGHI_HEADER_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
for (uint8_t pos = 0; pos < DELONGHI_STATE_FRAME_SIZE; pos++) {
|
||||
uint8_t byte = 0;
|
||||
for (int8_t bit = 0; bit < 8; bit++) {
|
||||
if (data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ONE_SPACE)) {
|
||||
byte |= 1 << bit;
|
||||
} else if (!data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state_frame[pos] = byte;
|
||||
if (pos == 0) {
|
||||
// frame header
|
||||
if (byte != DELONGHI_ADDRESS) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this->parse_state_frame_(state_frame);
|
||||
}
|
||||
|
||||
} // namespace delonghi
|
||||
} // namespace esphome
|
||||
64
esphome/components/delonghi/delonghi.h
Normal file
64
esphome/components/delonghi/delonghi.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace delonghi {
|
||||
|
||||
// Values for DELONGHI ARC43XXX IR Controllers
|
||||
const uint8_t DELONGHI_ADDRESS = 83;
|
||||
|
||||
// Temperature
|
||||
const uint8_t DELONGHI_TEMP_MIN = 13; // Celsius
|
||||
const uint8_t DELONGHI_TEMP_MAX = 32; // Celsius
|
||||
const uint8_t DELONGHI_TEMP_OFFSET_COOL = 17; // Celsius
|
||||
const uint8_t DELONGHI_TEMP_OFFSET_HEAT = 12; // Celsius
|
||||
|
||||
// Modes
|
||||
const uint8_t DELONGHI_MODE_AUTO = 0b1000;
|
||||
const uint8_t DELONGHI_MODE_COOL = 0b0000;
|
||||
const uint8_t DELONGHI_MODE_HEAT = 0b0110;
|
||||
const uint8_t DELONGHI_MODE_DRY = 0b0010;
|
||||
const uint8_t DELONGHI_MODE_FAN = 0b0100;
|
||||
const uint8_t DELONGHI_MODE_OFF = 0b0000;
|
||||
const uint8_t DELONGHI_MODE_ON = 0b0001;
|
||||
|
||||
// Fan Speed
|
||||
const uint8_t DELONGHI_FAN_AUTO = 0b00;
|
||||
const uint8_t DELONGHI_FAN_HIGH = 0b01;
|
||||
const uint8_t DELONGHI_FAN_MEDIUM = 0b10;
|
||||
const uint8_t DELONGHI_FAN_LOW = 0b11;
|
||||
|
||||
// IR Transmission - similar to NEC1
|
||||
const uint32_t DELONGHI_IR_FREQUENCY = 38000;
|
||||
const uint32_t DELONGHI_HEADER_MARK = 9000;
|
||||
const uint32_t DELONGHI_HEADER_SPACE = 4500;
|
||||
const uint32_t DELONGHI_BIT_MARK = 465;
|
||||
const uint32_t DELONGHI_ONE_SPACE = 1750;
|
||||
const uint32_t DELONGHI_ZERO_SPACE = 670;
|
||||
|
||||
// State Frame size
|
||||
const uint8_t DELONGHI_STATE_FRAME_SIZE = 8;
|
||||
|
||||
class DelonghiClimate : public climate_ir::ClimateIR {
|
||||
public:
|
||||
DelonghiClimate()
|
||||
: climate_ir::ClimateIR(DELONGHI_TEMP_MIN, DELONGHI_TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_HIGH},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
|
||||
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
|
||||
|
||||
protected:
|
||||
// Transmit via IR the state of this climate controller.
|
||||
void transmit_state() override;
|
||||
uint8_t operation_mode_();
|
||||
uint16_t fan_speed_();
|
||||
uint8_t temperature_();
|
||||
// Handle received IR Buffer
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
bool parse_state_frame_(const uint8_t frame[]);
|
||||
};
|
||||
|
||||
} // namespace delonghi
|
||||
} // namespace esphome
|
||||
@@ -242,6 +242,13 @@ void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color colo
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IMAGE_TYPE_RGB565:
|
||||
for (int img_x = 0; img_x < image->get_width(); img_x++) {
|
||||
for (int img_y = 0; img_y < image->get_height(); img_y++) {
|
||||
this->draw_pixel_at(x + img_x, y + img_y, image->get_rgb565_pixel(img_x, img_y));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,6 +504,17 @@ Color Image::get_color_pixel(int x, int y) const {
|
||||
(progmem_read_byte(this->data_start_ + pos + 0) << 16);
|
||||
return Color(color32);
|
||||
}
|
||||
Color Image::get_rgb565_pixel(int x, int y) const {
|
||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return Color::BLACK;
|
||||
const uint32_t pos = (x + y * this->width_) * 2;
|
||||
uint16_t rgb565 =
|
||||
progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
|
||||
auto r = (rgb565 & 0xF800) >> 11;
|
||||
auto g = (rgb565 & 0x07E0) >> 5;
|
||||
auto b = rgb565 & 0x001F;
|
||||
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
|
||||
}
|
||||
Color Image::get_grayscale_pixel(int x, int y) const {
|
||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return Color::BLACK;
|
||||
@@ -532,6 +550,20 @@ Color Animation::get_color_pixel(int x, int y) const {
|
||||
(progmem_read_byte(this->data_start_ + pos + 0) << 16);
|
||||
return Color(color32);
|
||||
}
|
||||
Color Animation::get_rgb565_pixel(int x, int y) const {
|
||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return Color::BLACK;
|
||||
const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_;
|
||||
if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_))
|
||||
return Color::BLACK;
|
||||
const uint32_t pos = (x + y * this->width_ + frame_index) * 2;
|
||||
uint16_t rgb565 =
|
||||
progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
|
||||
auto r = (rgb565 & 0xF800) >> 11;
|
||||
auto g = (rgb565 & 0x07E0) >> 5;
|
||||
auto b = rgb565 & 0x001F;
|
||||
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
|
||||
}
|
||||
Color Animation::get_grayscale_pixel(int x, int y) const {
|
||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return Color::BLACK;
|
||||
@@ -552,6 +584,12 @@ void Animation::next_frame() {
|
||||
this->current_frame_ = 0;
|
||||
}
|
||||
}
|
||||
void Animation::prev_frame() {
|
||||
this->current_frame_--;
|
||||
if (this->current_frame_ < 0) {
|
||||
this->current_frame_ = this->animation_frame_count_ - 1;
|
||||
}
|
||||
}
|
||||
|
||||
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
|
||||
void DisplayPage::show() { this->parent_->show_page(this); }
|
||||
|
||||
@@ -82,6 +82,7 @@ enum ImageType {
|
||||
IMAGE_TYPE_GRAYSCALE = 1,
|
||||
IMAGE_TYPE_RGB24 = 2,
|
||||
IMAGE_TYPE_TRANSPARENT_BINARY = 3,
|
||||
IMAGE_TYPE_RGB565 = 4,
|
||||
};
|
||||
|
||||
enum DisplayRotation {
|
||||
@@ -453,6 +454,7 @@ class Image {
|
||||
Image(const uint8_t *data_start, int width, int height, ImageType type);
|
||||
virtual bool get_pixel(int x, int y) const;
|
||||
virtual Color get_color_pixel(int x, int y) const;
|
||||
virtual Color get_rgb565_pixel(int x, int y) const;
|
||||
virtual Color get_grayscale_pixel(int x, int y) const;
|
||||
int get_width() const;
|
||||
int get_height() const;
|
||||
@@ -470,11 +472,13 @@ class Animation : public Image {
|
||||
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type);
|
||||
bool get_pixel(int x, int y) const override;
|
||||
Color get_color_pixel(int x, int y) const override;
|
||||
Color get_rgb565_pixel(int x, int y) const override;
|
||||
Color get_grayscale_pixel(int x, int y) const override;
|
||||
|
||||
int get_animation_frame_count() const;
|
||||
int get_current_frame() const;
|
||||
void next_frame();
|
||||
void prev_frame();
|
||||
|
||||
protected:
|
||||
int current_frame_;
|
||||
|
||||
@@ -143,37 +143,37 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.Optional("power_delivered_l1"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("power_delivered_l2"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("power_delivered_l3"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("power_returned_l1"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("power_returned_l2"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("power_returned_l3"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema(
|
||||
|
||||
@@ -12,6 +12,7 @@ using namespace esphome::cover;
|
||||
CoverTraits EndstopCover::get_traits() {
|
||||
auto traits = CoverTraits();
|
||||
traits.set_supports_position(true);
|
||||
traits.set_supports_toggle(true);
|
||||
traits.set_is_assumed_state(false);
|
||||
return traits;
|
||||
}
|
||||
@@ -20,6 +21,20 @@ void EndstopCover::control(const CoverCall &call) {
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
this->publish_state();
|
||||
}
|
||||
if (call.get_toggle().has_value()) {
|
||||
if (this->current_operation != COVER_OPERATION_IDLE) {
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
this->publish_state();
|
||||
} else {
|
||||
if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) {
|
||||
this->target_position_ = COVER_OPEN;
|
||||
this->start_direction_(COVER_OPERATION_OPENING);
|
||||
} else {
|
||||
this->target_position_ = COVER_CLOSED;
|
||||
this->start_direction_(COVER_OPERATION_CLOSING);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (call.get_position().has_value()) {
|
||||
auto pos = *call.get_position();
|
||||
if (pos == this->position) {
|
||||
@@ -125,9 +140,11 @@ void EndstopCover::start_direction_(CoverOperation dir) {
|
||||
trig = this->stop_trigger_;
|
||||
break;
|
||||
case COVER_OPERATION_OPENING:
|
||||
this->last_operation_ = dir;
|
||||
trig = this->open_trigger_;
|
||||
break;
|
||||
case COVER_OPERATION_CLOSING:
|
||||
this->last_operation_ = dir;
|
||||
trig = this->close_trigger_;
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -51,6 +51,7 @@ class EndstopCover : public cover::Cover, public Component {
|
||||
uint32_t start_dir_time_{0};
|
||||
uint32_t last_publish_time_{0};
|
||||
float target_position_{0};
|
||||
cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING};
|
||||
};
|
||||
|
||||
} // namespace endstop
|
||||
|
||||
0
esphome/components/ens210/__init__.py
Normal file
0
esphome/components/ens210/__init__.py
Normal file
230
esphome/components/ens210/ens210.cpp
Normal file
230
esphome/components/ens210/ens210.cpp
Normal file
@@ -0,0 +1,230 @@
|
||||
// ENS210 relative humidity and temperature sensor with I2C interface from ScioSense
|
||||
//
|
||||
// Datasheet: https://www.sciosense.com/wp-content/uploads/2021/01/ENS210.pdf
|
||||
//
|
||||
// Implementation based on:
|
||||
// https://github.com/maarten-pennings/ENS210
|
||||
// https://github.com/sciosense/ENS210_driver
|
||||
|
||||
#include "ens210.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ens210 {
|
||||
|
||||
static const char *const TAG = "ens210";
|
||||
|
||||
// ENS210 chip constants
|
||||
static const uint8_t ENS210_BOOTING_MS = 2; // Booting time in ms (also after reset, or going to high power)
|
||||
static const uint8_t ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS =
|
||||
130; // Conversion time in ms for single shot T/H measurement
|
||||
static const uint16_t ENS210_PART_ID = 0x0210; // The expected part id of the ENS210
|
||||
|
||||
// Addresses of the ENS210 registers
|
||||
static const uint8_t ENS210_REGISTER_PART_ID = 0x00;
|
||||
static const uint8_t ENS210_REGISTER_UID = 0x04;
|
||||
static const uint8_t ENS210_REGISTER_SYS_CTRL = 0x10;
|
||||
static const uint8_t ENS210_REGISTER_SYS_STAT = 0x11;
|
||||
static const uint8_t ENS210_REGISTER_SENS_RUN = 0x21;
|
||||
static const uint8_t ENS210_REGISTER_SENS_START = 0x22;
|
||||
static const uint8_t ENS210_REGISTER_SENS_STOP = 0x23;
|
||||
static const uint8_t ENS210_REGISTER_SENS_STAT = 0x24;
|
||||
static const uint8_t ENS210_REGISTER_T_VAL = 0x30;
|
||||
static const uint8_t ENS210_REGISTER_H_VAL = 0x33;
|
||||
|
||||
// CRC-7 constants
|
||||
static const uint8_t CRC7_WIDTH = 7; // A 7 bits CRC has polynomial of 7th order, which has 8 terms
|
||||
static const uint8_t CRC7_POLY = 0x89; // The 8 coefficients of the polynomial
|
||||
static const uint8_t CRC7_IVEC = 0x7F; // Initial vector has all 7 bits high
|
||||
|
||||
// Payload data constants
|
||||
static const uint8_t DATA7_WIDTH = 17;
|
||||
static const uint32_t DATA7_MASK = ((1UL << DATA7_WIDTH) - 1); // 0b 0 1111 1111 1111 1111
|
||||
static const uint32_t DATA7_MSB = (1UL << (DATA7_WIDTH - 1)); // 0b 1 0000 0000 0000 0000
|
||||
|
||||
// Converts a status to a human readable string
|
||||
static const LogString *ens210_status_to_human(int status) {
|
||||
switch (status) {
|
||||
case ENS210Component::ENS210_STATUS_I2C_ERROR:
|
||||
return LOG_STR("I2C error - communication with ENS210 failed!");
|
||||
case ENS210Component::ENS210_STATUS_CRC_ERROR:
|
||||
return LOG_STR("CRC error");
|
||||
case ENS210Component::ENS210_STATUS_INVALID:
|
||||
return LOG_STR("Invalid data");
|
||||
case ENS210Component::ENS210_STATUS_OK:
|
||||
return LOG_STR("Status OK");
|
||||
case ENS210Component::ENS210_WRONG_CHIP_ID:
|
||||
return LOG_STR("ENS210 has wrong chip ID! Is it a ENS210?");
|
||||
default:
|
||||
return LOG_STR("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the CRC-7 of 'value' (should only have 17 bits)
|
||||
// https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Computation
|
||||
static uint32_t crc7(uint32_t value) {
|
||||
// Setup polynomial
|
||||
uint32_t polynomial = CRC7_POLY;
|
||||
// Align polynomial with data
|
||||
polynomial = polynomial << (DATA7_WIDTH - CRC7_WIDTH - 1);
|
||||
// Loop variable (indicates which bit to test, start with highest)
|
||||
uint32_t bit = DATA7_MSB;
|
||||
// Make room for CRC value
|
||||
value = value << CRC7_WIDTH;
|
||||
bit = bit << CRC7_WIDTH;
|
||||
polynomial = polynomial << CRC7_WIDTH;
|
||||
// Insert initial vector
|
||||
value |= CRC7_IVEC;
|
||||
// Apply division until all bits done
|
||||
while (bit & (DATA7_MASK << CRC7_WIDTH)) {
|
||||
if (bit & value)
|
||||
value ^= polynomial;
|
||||
bit >>= 1;
|
||||
polynomial >>= 1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void ENS210Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up ENS210...");
|
||||
uint8_t data[2];
|
||||
uint16_t part_id = 0;
|
||||
// Reset
|
||||
if (!this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80)) {
|
||||
this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80);
|
||||
this->error_code_ = ENS210_STATUS_I2C_ERROR;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Wait to boot after reset
|
||||
delay(ENS210_BOOTING_MS);
|
||||
// Must disable low power to read PART_ID
|
||||
if (!set_low_power_(false)) {
|
||||
// Try to go back to default mode (low power enabled)
|
||||
set_low_power_(true);
|
||||
this->error_code_ = ENS210_STATUS_I2C_ERROR;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Read the PART_ID
|
||||
if (!this->read_bytes(ENS210_REGISTER_PART_ID, data, 2)) {
|
||||
// Try to go back to default mode (low power enabled)
|
||||
set_low_power_(true);
|
||||
this->error_code_ = ENS210_STATUS_I2C_ERROR;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Pack bytes into partid
|
||||
part_id = data[1] * 256U + data[0] * 1U;
|
||||
// Check expected part id of the ENS210
|
||||
if (part_id != ENS210_PART_ID) {
|
||||
this->error_code_ = ENS210_WRONG_CHIP_ID;
|
||||
this->mark_failed();
|
||||
}
|
||||
// Set default power mode (low power enabled)
|
||||
set_low_power_(true);
|
||||
}
|
||||
|
||||
void ENS210Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ENS210:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "%s", LOG_STR_ARG(ens210_status_to_human(this->error_code_)));
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
float ENS210Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void ENS210Component::update() {
|
||||
// Execute a single measurement
|
||||
if (!this->write_byte(ENS210_REGISTER_SENS_RUN, 0x00)) {
|
||||
ESP_LOGE(TAG, "Starting single measurement failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
// Trigger measurement
|
||||
if (!this->write_byte(ENS210_REGISTER_SENS_START, 0x03)) {
|
||||
ESP_LOGE(TAG, "Trigger of measurement failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
// Wait for measurement to complete
|
||||
this->set_timeout("data", uint32_t(ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS), [this]() {
|
||||
int temperature_data, temperature_status, humidity_data, humidity_status;
|
||||
uint8_t data[6];
|
||||
uint32_t h_val_data, t_val_data;
|
||||
// Set default status for early bail out
|
||||
temperature_status = ENS210_STATUS_I2C_ERROR;
|
||||
humidity_status = ENS210_STATUS_I2C_ERROR;
|
||||
|
||||
// Read T_VAL and H_VAL
|
||||
if (!this->read_bytes(ENS210_REGISTER_T_VAL, data, 6)) {
|
||||
ESP_LOGE(TAG, "Communication with ENS210 failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
// Pack bytes for humidity
|
||||
h_val_data = (uint32_t)((uint32_t) data[5] << 16 | (uint32_t) data[4] << 8 | (uint32_t) data[3]);
|
||||
// Extract humidity data and update the status
|
||||
extract_measurement_(h_val_data, &humidity_data, &humidity_status);
|
||||
|
||||
if (humidity_status == ENS210_STATUS_OK) {
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
float humidity = (humidity_data & 0xFFFF) / 512.0;
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Humidity status failure: %s", LOG_STR_ARG(ens210_status_to_human(humidity_status)));
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
// Pack bytes for temperature
|
||||
t_val_data = (uint32_t)((uint32_t) data[2] << 16 | (uint32_t) data[1] << 8 | (uint32_t) data[0]);
|
||||
// Extract temperature data and update the status
|
||||
extract_measurement_(t_val_data, &temperature_data, &temperature_status);
|
||||
|
||||
if (temperature_status == ENS210_STATUS_OK) {
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
// Temperature in Celsius
|
||||
float temperature = (temperature_data & 0xFFFF) / 64.0 - 27315L / 100.0;
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Temperature status failure: %s", LOG_STR_ARG(ens210_status_to_human(temperature_status)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Extracts measurement 'data' and 'status' from a 'val' obtained from measurment.
|
||||
void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status) {
|
||||
*data = (val >> 0) & 0xffff;
|
||||
int valid = (val >> 16) & 0x1;
|
||||
uint32_t crc = (val >> 17) & 0x7f;
|
||||
uint32_t payload = (val >> 0) & 0x1ffff;
|
||||
// Check CRC
|
||||
uint8_t crc_ok = crc7(payload) == crc;
|
||||
|
||||
if (!crc_ok) {
|
||||
*status = ENS210_STATUS_CRC_ERROR;
|
||||
} else if (!valid) {
|
||||
*status = ENS210_STATUS_INVALID;
|
||||
} else {
|
||||
*status = ENS210_STATUS_OK;
|
||||
}
|
||||
}
|
||||
|
||||
// Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems.
|
||||
bool ENS210Component::set_low_power_(bool enable) {
|
||||
uint8_t low_power_cmd = enable ? 0x01 : 0x00;
|
||||
ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false");
|
||||
bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd);
|
||||
delay(ENS210_BOOTING_MS);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace ens210
|
||||
} // namespace esphome
|
||||
39
esphome/components/ens210/ens210.h
Normal file
39
esphome/components/ens210/ens210.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ens210 {
|
||||
|
||||
/// This class implements support for the ENS210 relative humidity and temperature i2c sensor.
|
||||
class ENS210Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
float get_setup_priority() const override;
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
void update() override;
|
||||
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
|
||||
enum ErrorCode {
|
||||
ENS210_STATUS_OK = 0, // The value was read, the CRC matches, and data is valid
|
||||
ENS210_STATUS_INVALID, // The value was read, the CRC matches, but the data is invalid (e.g. the measurement was
|
||||
// not yet finished)
|
||||
ENS210_STATUS_CRC_ERROR, // The value was read, but the CRC over the payload (valid and data) does not match
|
||||
ENS210_STATUS_I2C_ERROR, // There was an I2C communication error
|
||||
ENS210_WRONG_CHIP_ID // The read PART_ID is not the expected part id of the ENS210
|
||||
} error_code_{ENS210_STATUS_OK};
|
||||
|
||||
protected:
|
||||
bool set_low_power_(bool enable);
|
||||
void extract_measurement_(uint32_t val, int *data, int *status);
|
||||
|
||||
sensor::Sensor *temperature_sensor_;
|
||||
sensor::Sensor *humidity_sensor_;
|
||||
};
|
||||
|
||||
} // namespace ens210
|
||||
} // namespace esphome
|
||||
58
esphome/components/ens210/sensor.py
Normal file
58
esphome/components/ens210/sensor.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@itn3rd77"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
ens210_ns = cg.esphome_ns.namespace("ens210")
|
||||
|
||||
ENS210Component = ens210_ns.class_(
|
||||
"ENS210Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ENS210Component),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x43))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if CONF_TEMPERATURE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
|
||||
if CONF_HUMIDITY in config:
|
||||
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
|
||||
cg.add(var.set_humidity_sensor(sens))
|
||||
@@ -107,7 +107,7 @@ def validate_gpio_pin(value):
|
||||
value = _translate_pin(value)
|
||||
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
|
||||
if variant not in _esp32_validations:
|
||||
raise cv.Invalid("Unsupported ESP32 variant {variant}")
|
||||
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
|
||||
|
||||
return _esp32_validations[variant].pin_validation(value)
|
||||
|
||||
@@ -121,7 +121,7 @@ def validate_supports(value):
|
||||
is_pulldown = mode[CONF_PULLDOWN]
|
||||
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
|
||||
if variant not in _esp32_validations:
|
||||
raise cv.Invalid("Unsupported ESP32 variant {variant}")
|
||||
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
|
||||
|
||||
if is_open_drain and not is_output:
|
||||
raise cv.Invalid(
|
||||
|
||||
@@ -262,6 +262,9 @@ void ESP32BLETracker::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_
|
||||
default:
|
||||
break;
|
||||
}
|
||||
for (auto *client : global_esp32_ble_tracker->clients_) {
|
||||
client->gap_event_handler(event, param);
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) {
|
||||
|
||||
@@ -155,6 +155,7 @@ class ESPBTClient : public ESPBTDeviceListener {
|
||||
public:
|
||||
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) = 0;
|
||||
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
|
||||
virtual void connect() = 0;
|
||||
void set_state(ClientState st) { this->state_ = st; }
|
||||
ClientState state() const { return state_; }
|
||||
|
||||
@@ -19,6 +19,7 @@ from esphome.helpers import copy_file_if_changed
|
||||
|
||||
from .const import (
|
||||
CONF_RESTORE_FROM_FLASH,
|
||||
CONF_EARLY_PIN_INIT,
|
||||
KEY_BOARD,
|
||||
KEY_ESP8266,
|
||||
KEY_PIN_INITIAL_STATES,
|
||||
@@ -148,6 +149,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_BOARD): cv.string_strict,
|
||||
cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA,
|
||||
cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean,
|
||||
cv.Optional(CONF_EARLY_PIN_INIT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_BOARD_FLASH_MODE, default="dout"): cv.one_of(
|
||||
*BUILD_FLASH_MODES, lower=True
|
||||
),
|
||||
@@ -197,6 +199,9 @@ async def to_code(config):
|
||||
if config[CONF_RESTORE_FROM_FLASH]:
|
||||
cg.add_define("USE_ESP8266_PREFERENCES_FLASH")
|
||||
|
||||
if config[CONF_EARLY_PIN_INIT]:
|
||||
cg.add_define("USE_ESP8266_EARLY_PIN_INIT")
|
||||
|
||||
# Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when
|
||||
# out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make
|
||||
# new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of
|
||||
|
||||
@@ -4,6 +4,7 @@ KEY_ESP8266 = "esp8266"
|
||||
KEY_BOARD = "board"
|
||||
KEY_PIN_INITIAL_STATES = "pin_initial_states"
|
||||
CONF_RESTORE_FROM_FLASH = "restore_from_flash"
|
||||
CONF_EARLY_PIN_INIT = "early_pin_init"
|
||||
|
||||
# esp8266 namespace is already defined by arduino, manually prefix esphome
|
||||
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include "core.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
@@ -55,6 +56,7 @@ extern "C" void resetPins() { // NOLINT
|
||||
// ourselves and this causes pins to toggle during reboot.
|
||||
force_link_symbols();
|
||||
|
||||
#ifdef USE_ESP8266_EARLY_PIN_INIT
|
||||
for (int i = 0; i < 16; i++) {
|
||||
uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i];
|
||||
uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i];
|
||||
@@ -63,6 +65,7 @@ extern "C" void resetPins() { // NOLINT
|
||||
if (level != 255)
|
||||
digitalWrite(i, level); // NOLINT
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import functools
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import re
|
||||
|
||||
import requests
|
||||
|
||||
from esphome import core
|
||||
from esphome.components import display
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
from esphome.const import CONF_FILE, CONF_GLYPHS, CONF_ID, CONF_RAW_DATA_ID, CONF_SIZE
|
||||
from esphome.const import (
|
||||
CONF_FAMILY,
|
||||
CONF_FILE,
|
||||
CONF_GLYPHS,
|
||||
CONF_ID,
|
||||
CONF_RAW_DATA_ID,
|
||||
CONF_TYPE,
|
||||
CONF_SIZE,
|
||||
CONF_PATH,
|
||||
CONF_WEIGHT,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
|
||||
|
||||
DOMAIN = "font"
|
||||
DEPENDENCIES = ["display"]
|
||||
MULTI_CONF = True
|
||||
|
||||
@@ -71,6 +88,128 @@ def validate_truetype_file(value):
|
||||
return cv.file_(value)
|
||||
|
||||
|
||||
def _compute_gfonts_local_path(value) -> Path:
|
||||
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
|
||||
base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN
|
||||
h = hashlib.new("sha256")
|
||||
h.update(name.encode())
|
||||
return base_dir / h.hexdigest()[:8] / "font.ttf"
|
||||
|
||||
|
||||
TYPE_LOCAL = "local"
|
||||
TYPE_GFONTS = "gfonts"
|
||||
LOCAL_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PATH): validate_truetype_file,
|
||||
}
|
||||
)
|
||||
CONF_ITALIC = "italic"
|
||||
FONT_WEIGHTS = {
|
||||
"thin": 100,
|
||||
"extra-light": 200,
|
||||
"light": 300,
|
||||
"regular": 400,
|
||||
"medium": 500,
|
||||
"semi-bold": 600,
|
||||
"bold": 700,
|
||||
"extra-bold": 800,
|
||||
"black": 900,
|
||||
}
|
||||
|
||||
|
||||
def validate_weight_name(value):
|
||||
return FONT_WEIGHTS[cv.one_of(*FONT_WEIGHTS, lower=True, space="-")(value)]
|
||||
|
||||
|
||||
def download_gfonts(value):
|
||||
wght = value[CONF_WEIGHT]
|
||||
if value[CONF_ITALIC]:
|
||||
wght = f"1,{wght}"
|
||||
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}"
|
||||
url = f"https://fonts.googleapis.com/css2?family={value[CONF_FAMILY]}:wght@{wght}"
|
||||
|
||||
path = _compute_gfonts_local_path(value)
|
||||
if path.is_file():
|
||||
return value
|
||||
try:
|
||||
req = requests.get(url)
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise cv.Invalid(
|
||||
f"Could not download font for {name}, please check the fonts exists "
|
||||
f"at google fonts ({e})"
|
||||
)
|
||||
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
|
||||
if match is None:
|
||||
raise cv.Invalid(
|
||||
f"Could not extract ttf file from gfonts response for {name}, "
|
||||
f"please report this."
|
||||
)
|
||||
|
||||
ttf_url = match.group(1)
|
||||
try:
|
||||
req = requests.get(ttf_url)
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise cv.Invalid(f"Could not download ttf file for {name} ({ttf_url}): {e}")
|
||||
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
path.write_bytes(req.content)
|
||||
return value
|
||||
|
||||
|
||||
GFONTS_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_FAMILY): cv.string_strict,
|
||||
cv.Optional(CONF_WEIGHT, default="regular"): cv.Any(
|
||||
cv.int_, validate_weight_name
|
||||
),
|
||||
cv.Optional(CONF_ITALIC, default=False): cv.boolean,
|
||||
},
|
||||
download_gfonts,
|
||||
)
|
||||
|
||||
|
||||
def validate_file_shorthand(value):
|
||||
value = cv.string_strict(value)
|
||||
if value.startswith("gfonts://"):
|
||||
match = re.match(r"^gfonts://([^@]+)(@.+)?$", value)
|
||||
if match is None:
|
||||
raise cv.Invalid("Could not parse gfonts shorthand syntax, please check it")
|
||||
family = match.group(1)
|
||||
weight = match.group(2)
|
||||
data = {
|
||||
CONF_TYPE: TYPE_GFONTS,
|
||||
CONF_FAMILY: family,
|
||||
}
|
||||
if weight is not None:
|
||||
data[CONF_WEIGHT] = weight[1:]
|
||||
return FILE_SCHEMA(data)
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_TYPE: TYPE_LOCAL,
|
||||
CONF_PATH: value,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
TYPED_FILE_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
TYPE_LOCAL: LOCAL_SCHEMA,
|
||||
TYPE_GFONTS: GFONTS_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _file_schema(value):
|
||||
if isinstance(value, str):
|
||||
return validate_file_shorthand(value)
|
||||
return TYPED_FILE_SCHEMA(value)
|
||||
|
||||
|
||||
FILE_SCHEMA = cv.Schema(_file_schema)
|
||||
|
||||
|
||||
DEFAULT_GLYPHS = (
|
||||
' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
||||
)
|
||||
@@ -79,7 +218,7 @@ CONF_RAW_GLYPH_ID = "raw_glyph_id"
|
||||
FONT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Font),
|
||||
cv.Required(CONF_FILE): validate_truetype_file,
|
||||
cv.Required(CONF_FILE): FILE_SCHEMA,
|
||||
cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs,
|
||||
cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1),
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
@@ -93,9 +232,13 @@ CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA)
|
||||
async def to_code(config):
|
||||
from PIL import ImageFont
|
||||
|
||||
path = CORE.relative_config_path(config[CONF_FILE])
|
||||
conf = config[CONF_FILE]
|
||||
if conf[CONF_TYPE] == TYPE_LOCAL:
|
||||
path = CORE.relative_config_path(conf[CONF_PATH])
|
||||
elif conf[CONF_TYPE] == TYPE_GFONTS:
|
||||
path = _compute_gfonts_local_path(conf)
|
||||
try:
|
||||
font = ImageFont.truetype(path, config[CONF_SIZE])
|
||||
font = ImageFont.truetype(str(path), config[CONF_SIZE])
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(f"Could not load truetype file {path}: {e}")
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@ namespace growatt_solar {
|
||||
static const char *const TAG = "growatt_solar";
|
||||
|
||||
static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04;
|
||||
static const uint8_t MODBUS_REGISTER_COUNT = 33;
|
||||
static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion
|
||||
|
||||
void GrowattSolar::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); }
|
||||
void GrowattSolar::update() {
|
||||
this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT[this->protocol_version_]);
|
||||
}
|
||||
|
||||
void GrowattSolar::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void {
|
||||
@@ -27,37 +29,76 @@ void GrowattSolar::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
sensor->publish_state(value);
|
||||
};
|
||||
|
||||
publish_1_reg_sensor_state(this->inverter_status_, 0, 1);
|
||||
switch (this->protocol_version_) {
|
||||
case RTU: {
|
||||
publish_1_reg_sensor_state(this->inverter_status_, 0, 1);
|
||||
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, 11, 12, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->grid_frequency_sensor_, 13, TWO_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, 11, 12, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->grid_frequency_sensor_, 13, TWO_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 14, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 15, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 16, 17, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 14, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 15, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 16, 17, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 18, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 19, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 20, 21, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 18, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 19, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 20, 21, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 22, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 23, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 24, 25, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 22, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 23, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 24, 25, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->today_production_, 26, 27, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, 28, 29, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->today_production_, 26, 27, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, 28, 29, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->inverter_module_temp_, 32, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->inverter_module_temp_, 32, ONE_DEC_UNIT);
|
||||
break;
|
||||
}
|
||||
case RTU2: {
|
||||
publish_1_reg_sensor_state(this->inverter_status_, 0, 1);
|
||||
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, 35, 36, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->grid_frequency_sensor_, 37, TWO_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 38, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 39, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 40, 41, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 42, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 43, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 44, 45, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 46, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 47, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 48, 49, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->today_production_, 53, 54, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, 55, 56, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->inverter_module_temp_, 93, ONE_DEC_UNIT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GrowattSolar::dump_config() {
|
||||
|
||||
@@ -10,12 +10,19 @@ namespace growatt_solar {
|
||||
static const float TWO_DEC_UNIT = 0.01;
|
||||
static const float ONE_DEC_UNIT = 0.1;
|
||||
|
||||
enum GrowattProtocolVersion {
|
||||
RTU = 0,
|
||||
RTU2,
|
||||
};
|
||||
|
||||
class GrowattSolar : public PollingComponent, public modbus::ModbusDevice {
|
||||
public:
|
||||
void update() override;
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_protocol_version(GrowattProtocolVersion protocol_version) { this->protocol_version_ = protocol_version; }
|
||||
|
||||
void set_inverter_status_sensor(sensor::Sensor *sensor) { this->inverter_status_ = sensor; }
|
||||
|
||||
void set_grid_frequency_sensor(sensor::Sensor *sensor) { this->grid_frequency_sensor_ = sensor; }
|
||||
@@ -67,6 +74,7 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice {
|
||||
sensor::Sensor *today_production_{nullptr};
|
||||
sensor::Sensor *total_energy_production_{nullptr};
|
||||
sensor::Sensor *inverter_module_temp_{nullptr};
|
||||
GrowattProtocolVersion protocol_version_;
|
||||
};
|
||||
|
||||
} // namespace growatt_solar
|
||||
|
||||
@@ -39,7 +39,7 @@ UNIT_MILLIAMPERE = "mA"
|
||||
CONF_INVERTER_STATUS = "inverter_status"
|
||||
CONF_PV_ACTIVE_POWER = "pv_active_power"
|
||||
CONF_INVERTER_MODULE_TEMP = "inverter_module_temp"
|
||||
|
||||
CONF_PROTOCOL_VERSION = "protocol_version"
|
||||
|
||||
AUTO_LOAD = ["modbus"]
|
||||
CODEOWNERS = ["@leeuwte"]
|
||||
@@ -95,10 +95,20 @@ PV_SCHEMA = cv.Schema(
|
||||
{cv.Optional(sensor): schema for sensor, schema in PV_SENSORS.items()}
|
||||
)
|
||||
|
||||
GrowattProtocolVersion = growatt_solar_ns.enum("GrowattProtocolVersion")
|
||||
PROTOCOL_VERSIONS = {
|
||||
"RTU": GrowattProtocolVersion.RTU,
|
||||
"RTU2": GrowattProtocolVersion.RTU2,
|
||||
}
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(GrowattSolar),
|
||||
cv.Optional(CONF_PROTOCOL_VERSION, default="RTU"): cv.enum(
|
||||
PROTOCOL_VERSIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_PHASE_A): PHASE_SCHEMA,
|
||||
cv.Optional(CONF_PHASE_B): PHASE_SCHEMA,
|
||||
cv.Optional(CONF_PHASE_C): PHASE_SCHEMA,
|
||||
@@ -152,6 +162,8 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
await modbus.register_modbus_device(var, config)
|
||||
|
||||
cg.add(var.set_protocol_version(config[CONF_PROTOCOL_VERSION]))
|
||||
|
||||
if CONF_INVERTER_STATUS in config:
|
||||
sens = await sensor.new_sensor(config[CONF_INVERTER_STATUS])
|
||||
cg.add(var.set_inverter_status_sensor(sens))
|
||||
|
||||
@@ -25,6 +25,7 @@ PROTOCOLS = {
|
||||
"daikin_arc417": Protocol.PROTOCOL_DAIKIN_ARC417,
|
||||
"daikin_arc480": Protocol.PROTOCOL_DAIKIN_ARC480,
|
||||
"daikin": Protocol.PROTOCOL_DAIKIN,
|
||||
"electroluxyal": Protocol.PROTOCOL_ELECTROLUXYAL,
|
||||
"fuego": Protocol.PROTOCOL_FUEGO,
|
||||
"fujitsu_awyz": Protocol.PROTOCOL_FUJITSU_AWYZ,
|
||||
"gree": Protocol.PROTOCOL_GREE,
|
||||
@@ -112,6 +113,4 @@ def to_code(config):
|
||||
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
|
||||
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
|
||||
|
||||
# PIO isn't updating releases, so referencing the release tag directly. See:
|
||||
# https://github.com/ToniA/arduino-heatpumpir/commit/0948c619d86407a4e50e8db2f3c193e0576c86fd
|
||||
cg.add_library("", "", "https://github.com/ToniA/arduino-heatpumpir.git#1.0.18")
|
||||
cg.add_library("tonia/HeatpumpIR", "1.0.20")
|
||||
|
||||
@@ -20,6 +20,7 @@ const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP
|
||||
{PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_ELECTROLUXYAL, []() { return new ElectroluxYALHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT
|
||||
|
||||
@@ -20,6 +20,7 @@ enum Protocol {
|
||||
PROTOCOL_DAIKIN_ARC417,
|
||||
PROTOCOL_DAIKIN_ARC480,
|
||||
PROTOCOL_DAIKIN,
|
||||
PROTOCOL_ELECTROLUXYAL,
|
||||
PROTOCOL_FUEGO,
|
||||
PROTOCOL_FUJITSU_AWYZ,
|
||||
PROTOCOL_GREE,
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace hm3301 {
|
||||
|
||||
class AbstractAQICalculator {
|
||||
public:
|
||||
virtual uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0;
|
||||
virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0;
|
||||
};
|
||||
|
||||
} // namespace hm3301
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace hm3301 {
|
||||
|
||||
class AQICalculator : public AbstractAQICalculator {
|
||||
public:
|
||||
uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
|
||||
uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
|
||||
int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_);
|
||||
int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace hm3301 {
|
||||
|
||||
class CAQICalculator : public AbstractAQICalculator {
|
||||
public:
|
||||
uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
|
||||
uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
|
||||
int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_);
|
||||
int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_);
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ void HM3301Component::update() {
|
||||
pm_10_0_value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX);
|
||||
}
|
||||
|
||||
int8_t aqi_value = -1;
|
||||
int16_t aqi_value = -1;
|
||||
if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) {
|
||||
AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_);
|
||||
aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value);
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL
|
||||
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
homeassistant_ns = cg.esphome_ns.namespace("homeassistant")
|
||||
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
cv.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
cv.Optional(CONF_INTERNAL, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_home_assistant_entity(var, config):
|
||||
cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
|
||||
if CONF_ATTRIBUTE in config:
|
||||
cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import binary_sensor
|
||||
from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID
|
||||
from .. import homeassistant_ns
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA,
|
||||
homeassistant_ns,
|
||||
setup_home_assistant_entity,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["api"]
|
||||
|
||||
HomeassistantBinarySensor = homeassistant_ns.class_(
|
||||
"HomeassistantBinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
binary_sensor.binary_sensor_schema(HomeassistantBinarySensor)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
cv.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(HomeassistantBinarySensor).extend(
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
|
||||
if CONF_ATTRIBUTE in config:
|
||||
cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))
|
||||
setup_home_assistant_entity(var, config)
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor
|
||||
from esphome.const import (
|
||||
CONF_ATTRIBUTE,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_ID,
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA,
|
||||
homeassistant_ns,
|
||||
setup_home_assistant_entity,
|
||||
)
|
||||
from .. import homeassistant_ns
|
||||
|
||||
DEPENDENCIES = ["api"]
|
||||
|
||||
@@ -14,19 +13,12 @@ HomeassistantSensor = homeassistant_ns.class_(
|
||||
"HomeassistantSensor", sensor.Sensor, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1,).extend(
|
||||
{
|
||||
cv.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
cv.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
}
|
||||
CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1).extend(
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
|
||||
cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
|
||||
if CONF_ATTRIBUTE in config:
|
||||
cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))
|
||||
setup_home_assistant_entity(var, config)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import text_sensor
|
||||
from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID
|
||||
|
||||
from .. import homeassistant_ns
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA,
|
||||
homeassistant_ns,
|
||||
setup_home_assistant_entity,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["api"]
|
||||
|
||||
@@ -11,19 +13,12 @@ HomeassistantTextSensor = homeassistant_ns.class_(
|
||||
"HomeassistantTextSensor", text_sensor.TextSensor, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = text_sensor.text_sensor_schema().extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HomeassistantTextSensor),
|
||||
cv.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
cv.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
}
|
||||
CONFIG_SCHEMA = text_sensor.text_sensor_schema(HomeassistantTextSensor).extend(
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
|
||||
if CONF_ATTRIBUTE in config:
|
||||
cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))
|
||||
setup_home_assistant_entity(var, config)
|
||||
|
||||
11
esphome/components/hydreon_rgxx/__init__.py
Normal file
11
esphome/components/hydreon_rgxx/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
|
||||
CODEOWNERS = ["@functionpointer"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
hydreon_rgxx_ns = cg.esphome_ns.namespace("hydreon_rgxx")
|
||||
RGModel = hydreon_rgxx_ns.enum("RGModel")
|
||||
HydreonRGxxComponent = hydreon_rgxx_ns.class_(
|
||||
"HydreonRGxxComponent", cg.PollingComponent, uart.UARTDevice
|
||||
)
|
||||
36
esphome/components/hydreon_rgxx/binary_sensor.py
Normal file
36
esphome/components/hydreon_rgxx/binary_sensor.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import binary_sensor
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_COLD,
|
||||
)
|
||||
|
||||
from . import hydreon_rgxx_ns, HydreonRGxxComponent
|
||||
|
||||
CONF_HYDREON_RGXX_ID = "hydreon_rgxx_id"
|
||||
CONF_TOO_COLD = "too_cold"
|
||||
|
||||
HydreonRGxxBinarySensor = hydreon_rgxx_ns.class_(
|
||||
"HydreonRGxxBinaryComponent", cg.Component
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HydreonRGxxBinarySensor),
|
||||
cv.GenerateID(CONF_HYDREON_RGXX_ID): cv.use_id(HydreonRGxxComponent),
|
||||
cv.Optional(CONF_TOO_COLD): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_COLD
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
main_sensor = await cg.get_variable(config[CONF_HYDREON_RGXX_ID])
|
||||
bin_component = cg.new_Pvariable(config[CONF_ID], main_sensor)
|
||||
await cg.register_component(bin_component, config)
|
||||
if CONF_TOO_COLD in config:
|
||||
tc = await binary_sensor.new_binary_sensor(config[CONF_TOO_COLD])
|
||||
cg.add(main_sensor.set_too_cold_sensor(tc))
|
||||
211
esphome/components/hydreon_rgxx/hydreon_rgxx.cpp
Normal file
211
esphome/components/hydreon_rgxx/hydreon_rgxx.cpp
Normal file
@@ -0,0 +1,211 @@
|
||||
#include "hydreon_rgxx.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace hydreon_rgxx {
|
||||
|
||||
static const char *const TAG = "hydreon_rgxx.sensor";
|
||||
static const int MAX_DATA_LENGTH_BYTES = 80;
|
||||
static const uint8_t ASCII_LF = 0x0A;
|
||||
#define HYDREON_RGXX_COMMA ,
|
||||
static const char *const PROTOCOL_NAMES[] = {HYDREON_RGXX_PROTOCOL_LIST(, HYDREON_RGXX_COMMA)};
|
||||
|
||||
void HydreonRGxxComponent::dump_config() {
|
||||
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
ESP_LOGCONFIG(TAG, "hydreon_rgxx:");
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!");
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
int i = 0;
|
||||
#define HYDREON_RGXX_LOG_SENSOR(s) \
|
||||
if (this->sensors_[i++] != nullptr) { \
|
||||
LOG_SENSOR(" ", #s, this->sensors_[i - 1]); \
|
||||
}
|
||||
HYDREON_RGXX_PROTOCOL_LIST(HYDREON_RGXX_LOG_SENSOR, );
|
||||
}
|
||||
|
||||
void HydreonRGxxComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up hydreon_rgxx...");
|
||||
while (this->available() != 0) {
|
||||
this->read();
|
||||
}
|
||||
this->schedule_reboot_();
|
||||
}
|
||||
|
||||
bool HydreonRGxxComponent::sensor_missing_() {
|
||||
if (this->sensors_received_ == -1) {
|
||||
// no request sent yet, don't check
|
||||
return false;
|
||||
} else {
|
||||
if (this->sensors_received_ == 0) {
|
||||
ESP_LOGW(TAG, "No data at all");
|
||||
return true;
|
||||
}
|
||||
for (int i = 0; i < NUM_SENSORS; i++) {
|
||||
if (this->sensors_[i] == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if ((this->sensors_received_ >> i & 1) == 0) {
|
||||
ESP_LOGW(TAG, "Missing %s", PROTOCOL_NAMES[i]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void HydreonRGxxComponent::update() {
|
||||
if (this->boot_count_ > 0) {
|
||||
if (this->sensor_missing_()) {
|
||||
this->no_response_count_++;
|
||||
ESP_LOGE(TAG, "data missing %d times", this->no_response_count_);
|
||||
if (this->no_response_count_ > 15) {
|
||||
ESP_LOGE(TAG, "asking sensor to reboot");
|
||||
for (auto &sensor : this->sensors_) {
|
||||
if (sensor != nullptr) {
|
||||
sensor->publish_state(NAN);
|
||||
}
|
||||
}
|
||||
this->schedule_reboot_();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this->no_response_count_ = 0;
|
||||
}
|
||||
this->write_str("R\n");
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->too_cold_sensor_ != nullptr) {
|
||||
this->too_cold_sensor_->publish_state(this->too_cold_);
|
||||
}
|
||||
#endif
|
||||
this->too_cold_ = false;
|
||||
this->sensors_received_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void HydreonRGxxComponent::loop() {
|
||||
uint8_t data;
|
||||
while (this->available() > 0) {
|
||||
if (this->read_byte(&data)) {
|
||||
buffer_ += (char) data;
|
||||
if (this->buffer_.back() == static_cast<char>(ASCII_LF) || this->buffer_.length() >= MAX_DATA_LENGTH_BYTES) {
|
||||
// complete line received
|
||||
this->process_line_();
|
||||
this->buffer_.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Communication with the sensor is asynchronous.
|
||||
* We send requests and let esphome continue doing its thing.
|
||||
* Once we have received a complete line, we process it.
|
||||
*
|
||||
* Catching communication failures is done in two layers:
|
||||
*
|
||||
* 1. We check if all requested data has been received
|
||||
* before we send out the next request. If data keeps
|
||||
* missing, we escalate.
|
||||
* 2. Request the sensor to reboot. We retry based on
|
||||
* a timeout. If the sensor does not respond after
|
||||
* several boot attempts, we give up.
|
||||
*/
|
||||
void HydreonRGxxComponent::schedule_reboot_() {
|
||||
this->boot_count_ = 0;
|
||||
this->set_interval("reboot", 5000, [this]() {
|
||||
if (this->boot_count_ < 0) {
|
||||
ESP_LOGW(TAG, "hydreon_rgxx failed to boot %d times", -this->boot_count_);
|
||||
}
|
||||
this->boot_count_--;
|
||||
this->write_str("K\n");
|
||||
if (this->boot_count_ < -5) {
|
||||
ESP_LOGE(TAG, "hydreon_rgxx can't boot, giving up");
|
||||
for (auto &sensor : this->sensors_) {
|
||||
if (sensor != nullptr) {
|
||||
sensor->publish_state(NAN);
|
||||
}
|
||||
}
|
||||
this->mark_failed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool HydreonRGxxComponent::buffer_starts_with_(const std::string &prefix) {
|
||||
return this->buffer_starts_with_(prefix.c_str());
|
||||
}
|
||||
|
||||
bool HydreonRGxxComponent::buffer_starts_with_(const char *prefix) { return buffer_.rfind(prefix, 0) == 0; }
|
||||
|
||||
void HydreonRGxxComponent::process_line_() {
|
||||
ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
|
||||
|
||||
if (buffer_[0] == ';') {
|
||||
ESP_LOGI(TAG, "Comment: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
|
||||
return;
|
||||
}
|
||||
if (this->buffer_starts_with_("PwrDays")) {
|
||||
if (this->boot_count_ <= 0) {
|
||||
this->boot_count_ = 1;
|
||||
} else {
|
||||
this->boot_count_++;
|
||||
}
|
||||
this->cancel_interval("reboot");
|
||||
this->no_response_count_ = 0;
|
||||
ESP_LOGI(TAG, "Boot detected: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
|
||||
this->write_str("P\nH\nM\n"); // set sensor to polling mode, high res mode, metric mode
|
||||
return;
|
||||
}
|
||||
if (this->buffer_starts_with_("SW")) {
|
||||
std::string::size_type majend = this->buffer_.find('.');
|
||||
std::string::size_type endversion = this->buffer_.find(' ', 3);
|
||||
if (majend == std::string::npos || endversion == std::string::npos || majend > endversion) {
|
||||
ESP_LOGW(TAG, "invalid version string: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
|
||||
}
|
||||
int major = strtol(this->buffer_.substr(3, majend - 3).c_str(), nullptr, 10);
|
||||
int minor = strtol(this->buffer_.substr(majend + 1, endversion - (majend + 1)).c_str(), nullptr, 10);
|
||||
|
||||
if (major > 10 || minor >= 1000 || minor < 0 || major < 0) {
|
||||
ESP_LOGW(TAG, "invalid version: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str());
|
||||
}
|
||||
this->sw_version_ = major * 1000 + minor;
|
||||
ESP_LOGI(TAG, "detected sw version %i", this->sw_version_);
|
||||
return;
|
||||
}
|
||||
bool is_data_line = false;
|
||||
for (int i = 0; i < NUM_SENSORS; i++) {
|
||||
if (this->sensors_[i] != nullptr && this->buffer_starts_with_(PROTOCOL_NAMES[i])) {
|
||||
is_data_line = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_data_line) {
|
||||
std::string::size_type tc = this->buffer_.find("TooCold");
|
||||
this->too_cold_ |= tc != std::string::npos;
|
||||
if (this->too_cold_) {
|
||||
ESP_LOGD(TAG, "Received TooCold");
|
||||
}
|
||||
for (int i = 0; i < NUM_SENSORS; i++) {
|
||||
if (this->sensors_[i] == nullptr) {
|
||||
continue;
|
||||
}
|
||||
std::string::size_type n = this->buffer_.find(PROTOCOL_NAMES[i]);
|
||||
if (n == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
int data = strtol(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr, 10);
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Got unknown line: %s", this->buffer_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
float HydreonRGxxComponent::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
} // namespace hydreon_rgxx
|
||||
} // namespace esphome
|
||||
76
esphome/components/hydreon_rgxx/hydreon_rgxx.h
Normal file
76
esphome/components/hydreon_rgxx/hydreon_rgxx.h
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace hydreon_rgxx {
|
||||
|
||||
enum RGModel {
|
||||
RG9 = 1,
|
||||
RG15 = 2,
|
||||
};
|
||||
|
||||
#ifdef HYDREON_RGXX_NUM_SENSORS
|
||||
static const uint8_t NUM_SENSORS = HYDREON_RGXX_NUM_SENSORS;
|
||||
#else
|
||||
static const uint8_t NUM_SENSORS = 1;
|
||||
#endif
|
||||
|
||||
#ifndef HYDREON_RGXX_PROTOCOL_LIST
|
||||
#define HYDREON_RGXX_PROTOCOL_LIST(F, SEP) F("")
|
||||
#endif
|
||||
|
||||
class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice {
|
||||
public:
|
||||
void set_sensor(sensor::Sensor *sensor, int index) { this->sensors_[index] = sensor; }
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void set_too_cold_sensor(binary_sensor::BinarySensor *sensor) { this->too_cold_sensor_ = sensor; }
|
||||
#endif
|
||||
void set_model(RGModel model) { model_ = model; }
|
||||
|
||||
/// Schedule data readings.
|
||||
void update() override;
|
||||
/// Read data once available
|
||||
void loop() override;
|
||||
/// Setup the sensor and test for a connection.
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
void process_line_();
|
||||
void schedule_reboot_();
|
||||
bool buffer_starts_with_(const std::string &prefix);
|
||||
bool buffer_starts_with_(const char *prefix);
|
||||
bool sensor_missing_();
|
||||
|
||||
sensor::Sensor *sensors_[NUM_SENSORS] = {nullptr};
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
binary_sensor::BinarySensor *too_cold_sensor_ = nullptr;
|
||||
#endif
|
||||
|
||||
int16_t boot_count_ = 0;
|
||||
int16_t no_response_count_ = 0;
|
||||
std::string buffer_;
|
||||
RGModel model_ = RG9;
|
||||
int sw_version_ = 0;
|
||||
bool too_cold_ = false;
|
||||
|
||||
// bit field showing which sensors we have received data for
|
||||
int sensors_received_ = -1;
|
||||
};
|
||||
|
||||
class HydreonRGxxBinaryComponent : public Component {
|
||||
public:
|
||||
HydreonRGxxBinaryComponent(HydreonRGxxComponent *parent) {}
|
||||
};
|
||||
|
||||
} // namespace hydreon_rgxx
|
||||
} // namespace esphome
|
||||
119
esphome/components/hydreon_rgxx/sensor.py
Normal file
119
esphome/components/hydreon_rgxx/sensor.py
Normal file
@@ -0,0 +1,119 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import uart, sensor
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MODEL,
|
||||
CONF_MOISTURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
from . import RGModel, HydreonRGxxComponent
|
||||
|
||||
UNIT_INTENSITY = "intensity"
|
||||
UNIT_MILLIMETERS = "mm"
|
||||
UNIT_MILLIMETERS_PER_HOUR = "mm/h"
|
||||
|
||||
CONF_ACC = "acc"
|
||||
CONF_EVENT_ACC = "event_acc"
|
||||
CONF_TOTAL_ACC = "total_acc"
|
||||
CONF_R_INT = "r_int"
|
||||
|
||||
RG_MODELS = {
|
||||
"RG_9": RGModel.RG9,
|
||||
"RG_15": RGModel.RG15,
|
||||
# https://rainsensors.com/wp-content/uploads/sites/3/2020/07/rg-15_instructions_sw_1.000.pdf
|
||||
# https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2020.08.25-rg-9_instructions.pdf
|
||||
# https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2021.03.11-rg-9_instructions.pdf
|
||||
}
|
||||
SUPPORTED_SENSORS = {
|
||||
CONF_ACC: ["RG_15"],
|
||||
CONF_EVENT_ACC: ["RG_15"],
|
||||
CONF_TOTAL_ACC: ["RG_15"],
|
||||
CONF_R_INT: ["RG_15"],
|
||||
CONF_MOISTURE: ["RG_9"],
|
||||
}
|
||||
PROTOCOL_NAMES = {
|
||||
CONF_MOISTURE: "R",
|
||||
CONF_ACC: "Acc",
|
||||
CONF_R_INT: "Rint",
|
||||
CONF_EVENT_ACC: "EventAcc",
|
||||
CONF_TOTAL_ACC: "TotalAcc",
|
||||
}
|
||||
|
||||
|
||||
def _validate(config):
|
||||
for conf, models in SUPPORTED_SENSORS.items():
|
||||
if conf in config:
|
||||
if config[CONF_MODEL] not in models:
|
||||
raise cv.Invalid(
|
||||
f"{conf} is only available on {' and '.join(models)}, not {config[CONF_MODEL]}"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HydreonRGxxComponent),
|
||||
cv.Required(CONF_MODEL): cv.enum(
|
||||
RG_MODELS,
|
||||
upper=True,
|
||||
space="_",
|
||||
),
|
||||
cv.Optional(CONF_ACC): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MILLIMETERS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_EVENT_ACC): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MILLIMETERS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TOTAL_ACC): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MILLIMETERS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_R_INT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MILLIMETERS_PER_HOUR,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_INTENSITY,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(uart.UART_DEVICE_SCHEMA),
|
||||
_validate,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
cg.add_define(
|
||||
"HYDREON_RGXX_PROTOCOL_LIST(F, sep)",
|
||||
cg.RawExpression(
|
||||
" sep ".join([f'F("{name}")' for name in PROTOCOL_NAMES.values()])
|
||||
),
|
||||
)
|
||||
cg.add_define("HYDREON_RGXX_NUM_SENSORS", len(PROTOCOL_NAMES))
|
||||
|
||||
for i, conf in enumerate(PROTOCOL_NAMES):
|
||||
if conf in config:
|
||||
sens = await sensor.new_sensor(config[conf])
|
||||
cg.add(var.set_sensor(sens, i))
|
||||
@@ -46,21 +46,21 @@ class I2CDevice {
|
||||
I2CRegister reg(uint8_t a_register) { return {this, a_register}; }
|
||||
|
||||
ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); }
|
||||
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len) {
|
||||
ErrorCode err = this->write(&a_register, 1);
|
||||
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true) {
|
||||
ErrorCode err = this->write(&a_register, 1, stop);
|
||||
if (err != ERROR_OK)
|
||||
return err;
|
||||
return this->read(data, len);
|
||||
}
|
||||
|
||||
ErrorCode write(const uint8_t *data, uint8_t len) { return bus_->write(address_, data, len); }
|
||||
ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len) {
|
||||
ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); }
|
||||
ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true) {
|
||||
WriteBuffer buffers[2];
|
||||
buffers[0].data = &a_register;
|
||||
buffers[0].len = 1;
|
||||
buffers[1].data = data;
|
||||
buffers[1].len = len;
|
||||
return bus_->writev(address_, buffers, 2);
|
||||
return bus_->writev(address_, buffers, 2, stop);
|
||||
}
|
||||
|
||||
// Compat APIs
|
||||
@@ -93,7 +93,9 @@ class I2CDevice {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool read_byte(uint8_t a_register, uint8_t *data) { return read_register(a_register, data, 1) == ERROR_OK; }
|
||||
bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) {
|
||||
return read_register(a_register, data, 1, stop) == ERROR_OK;
|
||||
}
|
||||
|
||||
optional<uint8_t> read_byte(uint8_t a_register) {
|
||||
uint8_t data;
|
||||
@@ -104,8 +106,8 @@ class I2CDevice {
|
||||
|
||||
bool read_byte_16(uint8_t a_register, uint16_t *data) { return read_bytes_16(a_register, data, 1); }
|
||||
|
||||
bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) {
|
||||
return write_register(a_register, data, len) == ERROR_OK;
|
||||
bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len, bool stop = true) {
|
||||
return write_register(a_register, data, len, stop) == ERROR_OK;
|
||||
}
|
||||
|
||||
bool write_bytes(uint8_t a_register, const std::vector<uint8_t> &data) {
|
||||
@@ -118,7 +120,9 @@ class I2CDevice {
|
||||
|
||||
bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len);
|
||||
|
||||
bool write_byte(uint8_t a_register, uint8_t data) { return write_bytes(a_register, &data, 1); }
|
||||
bool write_byte(uint8_t a_register, uint8_t data, bool stop = true) {
|
||||
return write_bytes(a_register, &data, 1, stop);
|
||||
}
|
||||
|
||||
bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); }
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ enum ErrorCode {
|
||||
ERROR_NOT_INITIALIZED = 4,
|
||||
ERROR_TOO_LARGE = 5,
|
||||
ERROR_UNKNOWN = 6,
|
||||
ERROR_CRC = 7,
|
||||
};
|
||||
|
||||
struct ReadBuffer {
|
||||
@@ -36,12 +37,18 @@ class I2CBus {
|
||||
}
|
||||
virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) = 0;
|
||||
virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) {
|
||||
return write(address, buffer, len, true);
|
||||
}
|
||||
virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop) {
|
||||
WriteBuffer buf;
|
||||
buf.data = buffer;
|
||||
buf.len = len;
|
||||
return writev(address, &buf, 1);
|
||||
return writev(address, &buf, 1, stop);
|
||||
}
|
||||
virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) = 0;
|
||||
virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) {
|
||||
return writev(address, buffers, cnt, true);
|
||||
}
|
||||
virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) = 0;
|
||||
|
||||
protected:
|
||||
void i2c_scan_() {
|
||||
|
||||
@@ -104,7 +104,7 @@ ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt)
|
||||
|
||||
return ERROR_OK;
|
||||
}
|
||||
ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt) {
|
||||
ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) {
|
||||
// logging is only enabled with vv level, if warnings are shown the caller
|
||||
// should log them
|
||||
if (!initialized_) {
|
||||
@@ -139,7 +139,7 @@ ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cn
|
||||
return ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
uint8_t status = wire_->endTransmission(true);
|
||||
uint8_t status = wire_->endTransmission(stop);
|
||||
if (status == 0) {
|
||||
return ERROR_OK;
|
||||
} else if (status == 1) {
|
||||
|
||||
@@ -20,7 +20,7 @@ class ArduinoI2CBus : public I2CBus, public Component {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override;
|
||||
ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) override;
|
||||
ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||
|
||||
void set_scan(bool scan) { scan_ = scan; }
|
||||
|
||||
@@ -142,7 +142,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) {
|
||||
|
||||
return ERROR_OK;
|
||||
}
|
||||
ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt) {
|
||||
ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) {
|
||||
// logging is only enabled with vv level, if warnings are shown the caller
|
||||
// should log them
|
||||
if (!initialized_) {
|
||||
|
||||
@@ -20,7 +20,7 @@ class IDFI2CBus : public I2CBus, public Component {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override;
|
||||
ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) override;
|
||||
ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||
|
||||
void set_scan(bool scan) { scan_ = scan; }
|
||||
|
||||
@@ -25,6 +25,7 @@ IMAGE_TYPE = {
|
||||
"GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
|
||||
"RGB24": ImageType.IMAGE_TYPE_RGB24,
|
||||
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY,
|
||||
"RGB565": ImageType.IMAGE_TYPE_RGB565,
|
||||
}
|
||||
|
||||
Image_ = display.display_ns.class_("Image")
|
||||
@@ -89,6 +90,21 @@ async def to_code(config):
|
||||
data[pos] = pix[2]
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "RGB565":
|
||||
image = image.convert("RGB")
|
||||
pixels = list(image.getdata())
|
||||
data = [0 for _ in range(height * width * 3)]
|
||||
pos = 0
|
||||
for pix in pixels:
|
||||
R = pix[0] >> 3
|
||||
G = pix[1] >> 2
|
||||
B = pix[2] >> 3
|
||||
rgb = (R << 11) | (G << 5) | B
|
||||
data[pos] = rgb >> 8
|
||||
pos += 1
|
||||
data[pos] = rgb & 255
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "BINARY":
|
||||
image = image.convert("1", dither=dither)
|
||||
width8 = ((width + 7) // 8) * 8
|
||||
|
||||
@@ -51,7 +51,7 @@ class ImprovSerialComponent : public Component {
|
||||
void write_data_(std::vector<uint8_t> &data);
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
HardwareSerial *hw_serial_{nullptr};
|
||||
Stream *hw_serial_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_ESP_IDF
|
||||
uart_port_t uart_num_;
|
||||
|
||||
@@ -16,47 +16,73 @@ static const char *const TAG = "json";
|
||||
static std::vector<char> global_json_build_buffer; // NOLINT
|
||||
|
||||
std::string build_json(const json_build_t &f) {
|
||||
// Here we are allocating as much heap memory as available minus 2kb to be safe
|
||||
// Here we are allocating up to 5kb of memory,
|
||||
// with the heap size minus 2kb to be safe if less than 5kb
|
||||
// as we can not have a true dynamic sized document.
|
||||
// The excess memory is freed below with `shrinkToFit()`
|
||||
#ifdef USE_ESP8266
|
||||
const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance)
|
||||
const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance)
|
||||
#elif defined(USE_ESP32)
|
||||
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) - 2048;
|
||||
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
|
||||
#endif
|
||||
|
||||
DynamicJsonDocument json_document(free_heap);
|
||||
const size_t request_size = std::min(free_heap, (size_t) 512);
|
||||
|
||||
DynamicJsonDocument json_document(request_size);
|
||||
if (json_document.capacity() == 0) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
|
||||
request_size, free_heap);
|
||||
return "{}";
|
||||
}
|
||||
JsonObject root = json_document.to<JsonObject>();
|
||||
f(root);
|
||||
json_document.shrinkToFit();
|
||||
|
||||
ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity());
|
||||
std::string output;
|
||||
serializeJson(json_document, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
void parse_json(const std::string &data, const json_parse_t &f) {
|
||||
// Here we are allocating as much heap memory as available minus 2kb to be safe
|
||||
// Here we are allocating 1.5 times the data size,
|
||||
// with the heap size minus 2kb to be safe if less than that
|
||||
// as we can not have a true dynamic sized document.
|
||||
// The excess memory is freed below with `shrinkToFit()`
|
||||
#ifdef USE_ESP8266
|
||||
const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance)
|
||||
const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance)
|
||||
#elif defined(USE_ESP32)
|
||||
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) - 2048;
|
||||
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
|
||||
#endif
|
||||
bool pass = false;
|
||||
size_t request_size = std::min(free_heap, (size_t)(data.size() * 1.5));
|
||||
do {
|
||||
DynamicJsonDocument json_document(request_size);
|
||||
if (json_document.capacity() == 0) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size,
|
||||
free_heap);
|
||||
return;
|
||||
}
|
||||
DeserializationError err = deserializeJson(json_document, data);
|
||||
json_document.shrinkToFit();
|
||||
|
||||
DynamicJsonDocument json_document(free_heap);
|
||||
DeserializationError err = deserializeJson(json_document, data);
|
||||
json_document.shrinkToFit();
|
||||
JsonObject root = json_document.as<JsonObject>();
|
||||
|
||||
JsonObject root = json_document.as<JsonObject>();
|
||||
|
||||
if (err) {
|
||||
ESP_LOGW(TAG, "Parsing JSON failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
f(root);
|
||||
if (err == DeserializationError::Ok) {
|
||||
pass = true;
|
||||
f(root);
|
||||
} else if (err == DeserializationError::NoMemory) {
|
||||
if (request_size * 2 >= free_heap) {
|
||||
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "Increasing memory allocation.");
|
||||
request_size *= 2;
|
||||
continue;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "JSON parse error: %s", err.c_str());
|
||||
return;
|
||||
}
|
||||
} while (!pass);
|
||||
}
|
||||
|
||||
} // namespace json
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import display
|
||||
from esphome.const import CONF_DIMENSIONS
|
||||
from esphome.const import CONF_DIMENSIONS, CONF_POSITION, CONF_DATA
|
||||
|
||||
CONF_USER_CHARACTERS = "user_characters"
|
||||
|
||||
lcd_base_ns = cg.esphome_ns.namespace("lcd_base")
|
||||
LCDDisplay = lcd_base_ns.class_("LCDDisplay", cg.PollingComponent)
|
||||
@@ -16,9 +18,35 @@ def validate_lcd_dimensions(value):
|
||||
return value
|
||||
|
||||
|
||||
def validate_user_characters(value):
|
||||
positions = set()
|
||||
for conf in value:
|
||||
if conf[CONF_POSITION] in positions:
|
||||
raise cv.Invalid(
|
||||
f"Duplicate user defined character at position {conf[CONF_POSITION]}"
|
||||
)
|
||||
positions.add(conf[CONF_POSITION])
|
||||
return value
|
||||
|
||||
|
||||
LCD_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_DIMENSIONS): validate_lcd_dimensions,
|
||||
cv.Optional(CONF_USER_CHARACTERS): cv.All(
|
||||
cv.ensure_list(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_POSITION): cv.int_range(min=0, max=7),
|
||||
cv.Required(CONF_DATA): cv.All(
|
||||
cv.ensure_list(cv.int_range(min=0, max=31)),
|
||||
cv.Length(min=8, max=8),
|
||||
),
|
||||
}
|
||||
),
|
||||
),
|
||||
cv.Length(max=8),
|
||||
validate_user_characters,
|
||||
),
|
||||
}
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
@@ -27,3 +55,6 @@ async def setup_lcd_display(var, config):
|
||||
await cg.register_component(var, config)
|
||||
await display.register_display(var, config)
|
||||
cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1]))
|
||||
if CONF_USER_CHARACTERS in config:
|
||||
for usr in config[CONF_USER_CHARACTERS]:
|
||||
cg.add(var.set_user_defined_char(usr[CONF_POSITION], usr[CONF_DATA]))
|
||||
|
||||
@@ -65,6 +65,13 @@ void LCDDisplay::setup() {
|
||||
this->command_(LCD_DISPLAY_COMMAND_FUNCTION_SET | display_function);
|
||||
}
|
||||
|
||||
// store user defined characters
|
||||
for (auto &user_defined_char : this->user_defined_chars_) {
|
||||
this->command_(LCD_DISPLAY_COMMAND_SET_CGRAM_ADDR | (user_defined_char.first << 3));
|
||||
for (auto data : user_defined_char.second)
|
||||
this->send(data, true);
|
||||
}
|
||||
|
||||
this->command_(LCD_DISPLAY_COMMAND_FUNCTION_SET | display_function);
|
||||
uint8_t display_control = LCD_DISPLAY_DISPLAY_ON;
|
||||
this->command_(LCD_DISPLAY_COMMAND_DISPLAY_CONTROL | display_control);
|
||||
@@ -160,6 +167,13 @@ void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, time:
|
||||
}
|
||||
void LCDDisplay::strftime(const char *format, time::ESPTime time) { this->strftime(0, 0, format, time); }
|
||||
#endif
|
||||
void LCDDisplay::loadchar(uint8_t location, uint8_t charmap[]) {
|
||||
location &= 0x7; // we only have 8 locations 0-7
|
||||
this->command_(LCD_DISPLAY_COMMAND_SET_CGRAM_ADDR | (location << 3));
|
||||
for (int i = 0; i < 8; i++) {
|
||||
this->send(charmap[i], true);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace lcd_base
|
||||
} // namespace esphome
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
#endif
|
||||
|
||||
#include <map>
|
||||
|
||||
namespace esphome {
|
||||
namespace lcd_base {
|
||||
|
||||
@@ -19,6 +21,8 @@ class LCDDisplay : public PollingComponent {
|
||||
this->rows_ = rows;
|
||||
}
|
||||
|
||||
void set_user_defined_char(uint8_t pos, const std::vector<uint8_t> &data) { this->user_defined_chars_[pos] = data; }
|
||||
|
||||
void setup() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
@@ -47,6 +51,9 @@ class LCDDisplay : public PollingComponent {
|
||||
void strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0)));
|
||||
#endif
|
||||
|
||||
/// Load custom char to given location
|
||||
void loadchar(uint8_t location, uint8_t charmap[]);
|
||||
|
||||
protected:
|
||||
virtual bool is_four_bit_mode() = 0;
|
||||
virtual void write_n_bits(uint8_t value, uint8_t n) = 0;
|
||||
@@ -58,6 +65,7 @@ class LCDDisplay : public PollingComponent {
|
||||
uint8_t columns_;
|
||||
uint8_t rows_;
|
||||
uint8_t *buffer_{nullptr};
|
||||
std::map<uint8_t, std::vector<uint8_t> > user_defined_chars_;
|
||||
};
|
||||
|
||||
} // namespace lcd_base
|
||||
|
||||
@@ -19,8 +19,13 @@ from esphome.const import (
|
||||
CONF_TX_BUFFER_SIZE,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.esp32.const import VARIANT_ESP32S2, VARIANT_ESP32C3
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
logger_ns = cg.esphome_ns.namespace("logger")
|
||||
@@ -54,36 +59,51 @@ LOG_LEVEL_SEVERITY = [
|
||||
"VERY_VERBOSE",
|
||||
]
|
||||
|
||||
ESP32_REDUCED_VARIANTS = [VARIANT_ESP32C3, VARIANT_ESP32S2]
|
||||
UART0 = "UART0"
|
||||
UART1 = "UART1"
|
||||
UART2 = "UART2"
|
||||
UART0_SWAP = "UART0_SWAP"
|
||||
USB_SERIAL_JTAG = "USB_SERIAL_JTAG"
|
||||
USB_CDC = "USB_CDC"
|
||||
|
||||
UART_SELECTION_ESP32_REDUCED = ["UART0", "UART1"]
|
||||
UART_SELECTION_ESP32 = {
|
||||
VARIANT_ESP32: [UART0, UART1, UART2],
|
||||
VARIANT_ESP32S2: [UART0, UART1, USB_CDC],
|
||||
VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
|
||||
VARIANT_ESP32C3: [UART0, UART1, USB_SERIAL_JTAG],
|
||||
}
|
||||
|
||||
UART_SELECTION_ESP32 = ["UART0", "UART1", "UART2"]
|
||||
UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1]
|
||||
|
||||
UART_SELECTION_ESP8266 = ["UART0", "UART0_SWAP", "UART1"]
|
||||
ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG]
|
||||
|
||||
HARDWARE_UART_TO_UART_SELECTION = {
|
||||
"UART0": logger_ns.UART_SELECTION_UART0,
|
||||
"UART0_SWAP": logger_ns.UART_SELECTION_UART0_SWAP,
|
||||
"UART1": logger_ns.UART_SELECTION_UART1,
|
||||
"UART2": logger_ns.UART_SELECTION_UART2,
|
||||
UART0: logger_ns.UART_SELECTION_UART0,
|
||||
UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP,
|
||||
UART1: logger_ns.UART_SELECTION_UART1,
|
||||
UART2: logger_ns.UART_SELECTION_UART2,
|
||||
USB_CDC: logger_ns.UART_SELECTION_USB_CDC,
|
||||
USB_SERIAL_JTAG: logger_ns.UART_SELECTION_USB_SERIAL_JTAG,
|
||||
}
|
||||
|
||||
HARDWARE_UART_TO_SERIAL = {
|
||||
"UART0": cg.global_ns.Serial,
|
||||
"UART0_SWAP": cg.global_ns.Serial,
|
||||
"UART1": cg.global_ns.Serial1,
|
||||
"UART2": cg.global_ns.Serial2,
|
||||
UART0: cg.global_ns.Serial,
|
||||
UART0_SWAP: cg.global_ns.Serial,
|
||||
UART1: cg.global_ns.Serial1,
|
||||
UART2: cg.global_ns.Serial2,
|
||||
}
|
||||
|
||||
is_log_level = cv.one_of(*LOG_LEVELS, upper=True)
|
||||
|
||||
|
||||
def uart_selection(value):
|
||||
if value.upper() in ESP_IDF_UARTS:
|
||||
if not CORE.using_esp_idf:
|
||||
raise cv.Invalid(f"Only esp-idf framework supports {value}.")
|
||||
if CORE.is_esp32:
|
||||
if get_esp32_variant() in ESP32_REDUCED_VARIANTS:
|
||||
return cv.one_of(*UART_SELECTION_ESP32_REDUCED, upper=True)(value)
|
||||
return cv.one_of(*UART_SELECTION_ESP32, upper=True)(value)
|
||||
variant = get_esp32_variant()
|
||||
if variant in UART_SELECTION_ESP32:
|
||||
return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value)
|
||||
if CORE.is_esp8266:
|
||||
return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value)
|
||||
raise NotImplementedError
|
||||
@@ -113,7 +133,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int,
|
||||
cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes,
|
||||
cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HARDWARE_UART, default="UART0"): uart_selection,
|
||||
cv.Optional(CONF_HARDWARE_UART, default=UART0): uart_selection,
|
||||
cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level,
|
||||
cv.Optional(CONF_LOGS, default={}): cv.Schema(
|
||||
{
|
||||
@@ -185,6 +205,12 @@ async def to_code(config):
|
||||
if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH):
|
||||
cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH")
|
||||
|
||||
if CORE.using_esp_idf:
|
||||
if config[CONF_HARDWARE_UART] == USB_CDC:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
|
||||
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
|
||||
|
||||
# Register at end for safe mode
|
||||
await cg.register_component(log, config)
|
||||
|
||||
@@ -203,15 +229,6 @@ async def to_code(config):
|
||||
)
|
||||
|
||||
|
||||
def maybe_simple_message(schema):
|
||||
def validator(value):
|
||||
if isinstance(value, dict):
|
||||
return cv.Schema(schema)(value)
|
||||
return cv.Schema(schema)({CONF_FORMAT: value})
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def validate_printf(value):
|
||||
# https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python
|
||||
cfmt = r"""
|
||||
@@ -234,7 +251,7 @@ def validate_printf(value):
|
||||
|
||||
CONF_LOGGER_LOG = "logger.log"
|
||||
LOGGER_LOG_ACTION_SCHEMA = cv.All(
|
||||
maybe_simple_message(
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_FORMAT): cv.string,
|
||||
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
|
||||
@@ -242,9 +259,10 @@ LOGGER_LOG_ACTION_SCHEMA = cv.All(
|
||||
*LOG_LEVEL_TO_ESP_LOG, upper=True
|
||||
),
|
||||
cv.Optional(CONF_TAG, default="main"): cv.string,
|
||||
}
|
||||
),
|
||||
validate_printf,
|
||||
},
|
||||
validate_printf,
|
||||
key=CONF_FORMAT,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -116,8 +116,22 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) {
|
||||
this->hw_serial_->println(msg);
|
||||
#endif // USE_ARDUINO
|
||||
#ifdef USE_ESP_IDF
|
||||
uart_write_bytes(uart_num_, msg, strlen(msg));
|
||||
uart_write_bytes(uart_num_, "\n", 1);
|
||||
if (
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
uart_ == UART_SELECTION_USB_CDC
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
uart_ == UART_SELECTION_USB_SERIAL_JTAG
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
uart_ == UART_SELECTION_USB_CDC || uart_ == UART_SELECTION_USB_SERIAL_JTAG
|
||||
#else
|
||||
/* DISABLES CODE */ (false)
|
||||
#endif
|
||||
) {
|
||||
puts(msg);
|
||||
} else {
|
||||
uart_write_bytes(uart_num_, msg, strlen(msg));
|
||||
uart_write_bytes(uart_num_, "\n", 1);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -149,13 +163,25 @@ void Logger::pre_setup() {
|
||||
case UART_SELECTION_UART0_SWAP:
|
||||
#endif
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
#ifdef USE_ESP8266
|
||||
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
|
||||
Serial.swap();
|
||||
}
|
||||
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
#endif
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
#ifdef USE_ESP8266
|
||||
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)
|
||||
case UART_SELECTION_UART2:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
@@ -169,39 +195,41 @@ void Logger::pre_setup() {
|
||||
case UART_SELECTION_UART1:
|
||||
uart_num_ = UART_NUM_1;
|
||||
break;
|
||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
case UART_SELECTION_UART2:
|
||||
uart_num_ = UART_NUM_2;
|
||||
break;
|
||||
#endif
|
||||
#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
case UART_SELECTION_USB_CDC:
|
||||
uart_num_ = -1;
|
||||
break;
|
||||
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
case UART_SELECTION_USB_SERIAL_JTAG:
|
||||
uart_num_ = -1;
|
||||
break;
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3
|
||||
}
|
||||
uart_config_t uart_config{};
|
||||
uart_config.baud_rate = (int) baud_rate_;
|
||||
uart_config.data_bits = UART_DATA_8_BITS;
|
||||
uart_config.parity = UART_PARITY_DISABLE;
|
||||
uart_config.stop_bits = UART_STOP_BITS_1;
|
||||
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
|
||||
uart_param_config(uart_num_, &uart_config);
|
||||
const int uart_buffer_size = tx_buffer_size_;
|
||||
// Install UART driver using an event queue here
|
||||
uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
this->hw_serial_->begin(this->baud_rate_);
|
||||
#ifdef USE_ESP8266
|
||||
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
|
||||
this->hw_serial_->swap();
|
||||
if (uart_num_ >= 0) {
|
||||
uart_config_t uart_config{};
|
||||
uart_config.baud_rate = (int) baud_rate_;
|
||||
uart_config.data_bits = UART_DATA_8_BITS;
|
||||
uart_config.parity = UART_PARITY_DISABLE;
|
||||
uart_config.stop_bits = UART_STOP_BITS_1;
|
||||
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
|
||||
uart_param_config(uart_num_, &uart_config);
|
||||
const int uart_buffer_size = tx_buffer_size_;
|
||||
// Install UART driver using an event queue here
|
||||
uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0);
|
||||
}
|
||||
this->hw_serial_->setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
#endif
|
||||
#endif // USE_ARDUINO
|
||||
#endif // USE_ESP_IDF
|
||||
}
|
||||
#ifdef USE_ESP8266
|
||||
else {
|
||||
uart_set_debug(UART_NO);
|
||||
}
|
||||
#endif
|
||||
#endif // USE_ESP8266
|
||||
|
||||
global_logger = this;
|
||||
#if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO)
|
||||
@@ -209,7 +237,7 @@ void Logger::pre_setup() {
|
||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
|
||||
esp_log_level_set("*", ESP_LOG_VERBOSE);
|
||||
}
|
||||
#endif
|
||||
#endif // USE_ESP_IDF || USE_ESP32_FRAMEWORK_ARDUINO
|
||||
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
@@ -224,11 +252,24 @@ void Logger::add_on_log_callback(std::function<void(int, const char *, const cha
|
||||
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
|
||||
const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
|
||||
#ifdef USE_ESP32
|
||||
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART2"};
|
||||
#endif
|
||||
const char *const UART_SELECTIONS[] = {
|
||||
"UART0", "UART1",
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
"UART2",
|
||||
#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
|
||||
#if defined(USE_ESP_IDF)
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
"USB_CDC",
|
||||
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
"USB_SERIAL_JTAG",
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3
|
||||
#endif // USE_ESP_IDF
|
||||
};
|
||||
#endif // USE_ESP32
|
||||
#ifdef USE_ESP8266
|
||||
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"};
|
||||
#endif
|
||||
#endif // USE_ESP8266
|
||||
void Logger::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Logger:");
|
||||
ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]);
|
||||
|
||||
@@ -24,9 +24,19 @@ namespace logger {
|
||||
enum UARTSelection {
|
||||
UART_SELECTION_UART0 = 0,
|
||||
UART_SELECTION_UART1,
|
||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
#if defined(USE_ESP32)
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
UART_SELECTION_UART2,
|
||||
#endif
|
||||
#ifdef USE_ESP_IDF
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
UART_SELECTION_USB_CDC,
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
UART_SELECTION_USB_SERIAL_JTAG,
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_ESP8266
|
||||
UART_SELECTION_UART0_SWAP,
|
||||
#endif
|
||||
@@ -40,7 +50,7 @@ class Logger : public Component {
|
||||
void set_baud_rate(uint32_t baud_rate);
|
||||
uint32_t get_baud_rate() const { return baud_rate_; }
|
||||
#ifdef USE_ARDUINO
|
||||
HardwareSerial *get_hw_serial() const { return hw_serial_; }
|
||||
Stream *get_hw_serial() const { return hw_serial_; }
|
||||
#endif
|
||||
#ifdef USE_ESP_IDF
|
||||
uart_port_t get_uart_num() const { return uart_num_; }
|
||||
@@ -119,7 +129,7 @@ class Logger : public Component {
|
||||
int tx_buffer_size_{0};
|
||||
UARTSelection uart_{UART_SELECTION_UART0};
|
||||
#ifdef USE_ARDUINO
|
||||
HardwareSerial *hw_serial_{nullptr};
|
||||
Stream *hw_serial_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_ESP_IDF
|
||||
uart_port_t uart_num_;
|
||||
|
||||
@@ -20,7 +20,7 @@ void MCP3204::dump_config() {
|
||||
}
|
||||
|
||||
float MCP3204::read_data(uint8_t pin) {
|
||||
uint8_t adc_primary_config = 0b00000110 & 0b00000111;
|
||||
uint8_t adc_primary_config = 0b00000110 | (pin >> 2);
|
||||
uint8_t adc_secondary_config = pin << 6;
|
||||
this->enable();
|
||||
this->transfer_byte(adc_primary_config);
|
||||
|
||||
@@ -17,7 +17,7 @@ CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(MCP3204Sensor),
|
||||
cv.GenerateID(CONF_MCP3204_ID): cv.use_id(MCP3204),
|
||||
cv.Required(CONF_NUMBER): cv.int_range(min=0, max=3),
|
||||
cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7),
|
||||
}
|
||||
).extend(cv.polling_component_schema("60s"))
|
||||
|
||||
|
||||
@@ -71,9 +71,9 @@ SENSOR_VALUE_TYPE = {
|
||||
"S_DWORD": SensorValueType.S_DWORD,
|
||||
"S_DWORD_R": SensorValueType.S_DWORD_R,
|
||||
"U_QWORD": SensorValueType.U_QWORD,
|
||||
"U_QWORDU_R": SensorValueType.U_QWORD_R,
|
||||
"U_QWORD_R": SensorValueType.U_QWORD_R,
|
||||
"S_QWORD": SensorValueType.S_QWORD,
|
||||
"U_QWORD_R": SensorValueType.S_QWORD_R,
|
||||
"S_QWORD_R": SensorValueType.S_QWORD_R,
|
||||
"FP32": SensorValueType.FP32,
|
||||
"FP32_R": SensorValueType.FP32_R,
|
||||
}
|
||||
@@ -87,9 +87,9 @@ TYPE_REGISTER_MAP = {
|
||||
"S_DWORD": 2,
|
||||
"S_DWORD_R": 2,
|
||||
"U_QWORD": 4,
|
||||
"U_QWORDU_R": 4,
|
||||
"S_QWORD": 4,
|
||||
"U_QWORD_R": 4,
|
||||
"S_QWORD": 4,
|
||||
"S_QWORD_R": 4,
|
||||
"FP32": 2,
|
||||
"FP32_R": 2,
|
||||
}
|
||||
|
||||
@@ -455,6 +455,28 @@ ModbusCommandItem ModbusCommandItem::create_custom_command(
|
||||
return cmd;
|
||||
}
|
||||
|
||||
ModbusCommandItem ModbusCommandItem::create_custom_command(
|
||||
ModbusController *modbusdevice, const std::vector<uint16_t> &values,
|
||||
std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
|
||||
&&handler) {
|
||||
ModbusCommandItem cmd = {};
|
||||
cmd.modbusdevice = modbusdevice;
|
||||
cmd.function_code = ModbusFunctionCode::CUSTOM;
|
||||
if (handler == nullptr) {
|
||||
cmd.on_data_func = [](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
|
||||
ESP_LOGI(TAG, "Custom Command sent");
|
||||
};
|
||||
} else {
|
||||
cmd.on_data_func = handler;
|
||||
}
|
||||
for (auto v : values) {
|
||||
cmd.payload.push_back((v >> 8) & 0xFF);
|
||||
cmd.payload.push_back(v & 0xFF);
|
||||
}
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
bool ModbusCommandItem::send() {
|
||||
if (this->function_code != ModbusFunctionCode::CUSTOM) {
|
||||
modbusdevice->send(uint8_t(this->function_code), this->register_address, this->register_count, this->payload.size(),
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
#include <list>
|
||||
#include <set>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
@@ -374,8 +374,8 @@ class ModbusCommandItem {
|
||||
const std::vector<bool> &values);
|
||||
/** Create custom modbus command
|
||||
* @param modbusdevice pointer to the device to execute the command
|
||||
* @param values byte vector of data to be sent to the device. The compplete payload must be provided with the
|
||||
* exception of the crc codess
|
||||
* @param values byte vector of data to be sent to the device. The complete payload must be provided with the
|
||||
* exception of the crc codes
|
||||
* @param handler function called when the response is received. Default is just logging a response
|
||||
* @return ModbusCommandItem with the prepared command
|
||||
*/
|
||||
@@ -383,6 +383,18 @@ class ModbusCommandItem {
|
||||
ModbusController *modbusdevice, const std::vector<uint8_t> &values,
|
||||
std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
|
||||
&&handler = nullptr);
|
||||
|
||||
/** Create custom modbus command
|
||||
* @param modbusdevice pointer to the device to execute the command
|
||||
* @param values word vector of data to be sent to the device. The complete payload must be provided with the
|
||||
* exception of the crc codes
|
||||
* @param handler function called when the response is received. Default is just logging a response
|
||||
* @return ModbusCommandItem with the prepared command
|
||||
*/
|
||||
static ModbusCommandItem create_custom_command(
|
||||
ModbusController *modbusdevice, const std::vector<uint16_t> &values,
|
||||
std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
|
||||
&&handler = nullptr);
|
||||
};
|
||||
|
||||
/** Modbus controller class.
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
)
|
||||
|
||||
from .. import (
|
||||
MODBUS_WRITE_REGISTER_TYPE,
|
||||
add_modbus_base_properties,
|
||||
modbus_controller_ns,
|
||||
modbus_calc_properties,
|
||||
@@ -24,6 +25,7 @@ from ..const import (
|
||||
CONF_CUSTOM_COMMAND,
|
||||
CONF_FORCE_NEW_RANGE,
|
||||
CONF_MODBUS_CONTROLLER_ID,
|
||||
CONF_REGISTER_TYPE,
|
||||
CONF_SKIP_UPDATES,
|
||||
CONF_USE_WRITE_MULTIPLE,
|
||||
CONF_VALUE_TYPE,
|
||||
@@ -61,6 +63,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
number.NUMBER_SCHEMA.extend(ModbusItemBaseSchema).extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ModbusNumber),
|
||||
cv.Optional(CONF_REGISTER_TYPE, default="holding"): cv.enum(
|
||||
MODBUS_WRITE_REGISTER_TYPE
|
||||
),
|
||||
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
|
||||
@@ -81,6 +86,7 @@ async def to_code(config):
|
||||
byte_offset, reg_count = modbus_calc_properties(config)
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
config[CONF_REGISTER_TYPE],
|
||||
config[CONF_ADDRESS],
|
||||
byte_offset,
|
||||
config[CONF_BITMASK],
|
||||
|
||||
@@ -26,6 +26,7 @@ void ModbusNumber::parse_and_publish(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void ModbusNumber::control(float value) {
|
||||
ModbusCommandItem write_cmd;
|
||||
std::vector<uint16_t> data;
|
||||
float write_value = value;
|
||||
// Is there are lambda configured?
|
||||
@@ -45,33 +46,39 @@ void ModbusNumber::control(float value) {
|
||||
write_value = multiply_by_ * write_value;
|
||||
}
|
||||
|
||||
// lambda didn't set payload
|
||||
if (data.empty()) {
|
||||
data = float_to_payload(write_value, this->sensor_value_type);
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG,
|
||||
"Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)",
|
||||
this->get_name().c_str(), this->start_address, this->register_count, value, write_value);
|
||||
|
||||
// Create and send the write command
|
||||
ModbusCommandItem write_cmd;
|
||||
if (this->register_count == 1 && !this->use_write_multiple_) {
|
||||
// since offset is in bytes and a register is 16 bits we get the start by adding offset/2
|
||||
write_cmd =
|
||||
ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, data[0]);
|
||||
if (!data.empty()) {
|
||||
ESP_LOGV(TAG, "Modbus Number write raw: %s", format_hex_pretty(data).c_str());
|
||||
write_cmd = ModbusCommandItem::create_custom_command(
|
||||
this->parent_, data,
|
||||
[this, write_cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
|
||||
this->parent_->on_write_register_response(write_cmd.register_type, this->start_address, data);
|
||||
});
|
||||
} else {
|
||||
write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset / 2,
|
||||
this->register_count, data);
|
||||
data = float_to_payload(write_value, this->sensor_value_type);
|
||||
|
||||
ESP_LOGD(TAG,
|
||||
"Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)",
|
||||
this->get_name().c_str(), this->start_address, this->register_count, value, write_value);
|
||||
|
||||
// Create and send the write command
|
||||
if (this->register_count == 1 && !this->use_write_multiple_) {
|
||||
// since offset is in bytes and a register is 16 bits we get the start by adding offset/2
|
||||
write_cmd =
|
||||
ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, data[0]);
|
||||
} else {
|
||||
write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset / 2,
|
||||
this->register_count, data);
|
||||
}
|
||||
// publish new value
|
||||
write_cmd.on_data_func = [this, write_cmd, value](ModbusRegisterType register_type, uint16_t start_address,
|
||||
const std::vector<uint8_t> &data) {
|
||||
// gets called when the write command is ack'd from the device
|
||||
parent_->on_write_register_response(write_cmd.register_type, start_address, data);
|
||||
this->publish_state(value);
|
||||
};
|
||||
}
|
||||
// publish new value
|
||||
write_cmd.on_data_func = [this, write_cmd, value](ModbusRegisterType register_type, uint16_t start_address,
|
||||
const std::vector<uint8_t> &data) {
|
||||
// gets called when the write command is ack'd from the device
|
||||
parent_->on_write_register_response(write_cmd.register_type, start_address, data);
|
||||
this->publish_state(value);
|
||||
};
|
||||
parent_->queue_command(write_cmd);
|
||||
this->publish_state(value);
|
||||
}
|
||||
void ModbusNumber::dump_config() { LOG_NUMBER(TAG, "Modbus Number", this); }
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ using value_to_data_t = std::function<float>(float);
|
||||
|
||||
class ModbusNumber : public number::Number, public Component, public SensorItem {
|
||||
public:
|
||||
ModbusNumber(uint16_t start_address, uint8_t offset, uint32_t bitmask, SensorValueType value_type, int register_count,
|
||||
uint8_t skip_updates, bool force_new_range) {
|
||||
this->register_type = ModbusRegisterType::HOLDING;
|
||||
ModbusNumber(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask,
|
||||
SensorValueType value_type, int register_count, uint8_t skip_updates, bool force_new_range) {
|
||||
this->register_type = register_type;
|
||||
this->start_address = start_address;
|
||||
this->offset = offset;
|
||||
this->bitmask = bitmask;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import select
|
||||
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA
|
||||
from esphome.jsonschema import jschema_composite
|
||||
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC
|
||||
|
||||
from .. import (
|
||||
SENSOR_VALUE_TYPE,
|
||||
@@ -30,7 +29,6 @@ ModbusSelect = modbus_controller_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
@jschema_composite
|
||||
def ensure_option_map():
|
||||
def validator(value):
|
||||
cv.check_not_templatable(value)
|
||||
@@ -79,6 +77,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean,
|
||||
cv.Required(CONF_OPTIONSMAP): ensure_option_map(),
|
||||
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
|
||||
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
|
||||
},
|
||||
@@ -112,6 +111,7 @@ async def to_code(config):
|
||||
cg.add(parent.add_sensor_item(var))
|
||||
cg.add(var.set_parent(parent))
|
||||
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
|
||||
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
|
||||
|
||||
if CONF_LAMBDA in config:
|
||||
template_ = await cg.process_lambda(
|
||||
|
||||
@@ -80,6 +80,9 @@ void ModbusSelect::control(const std::string &value) {
|
||||
}
|
||||
|
||||
parent_->queue_command(write_cmd);
|
||||
|
||||
if (this->optimistic_)
|
||||
this->publish_state(value);
|
||||
}
|
||||
|
||||
} // namespace modbus_controller
|
||||
|
||||
@@ -32,6 +32,7 @@ class ModbusSelect : public Component, public select::Select, public SensorItem
|
||||
|
||||
void set_parent(ModbusController *const parent) { this->parent_ = parent; }
|
||||
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
|
||||
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
void set_template(transform_func_t &&f) { this->transform_func_ = f; }
|
||||
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; }
|
||||
|
||||
@@ -43,6 +44,7 @@ class ModbusSelect : public Component, public select::Select, public SensorItem
|
||||
std::vector<int64_t> mapping_;
|
||||
ModbusController *parent_;
|
||||
bool use_write_multiple_{false};
|
||||
bool optimistic_{false};
|
||||
optional<transform_func_t> transform_func_;
|
||||
optional<write_transform_func_t> write_transform_func_;
|
||||
};
|
||||
|
||||
@@ -52,7 +52,8 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
|
||||
|
||||
// Now parse the data - See Datasheet for definition
|
||||
|
||||
if (static_cast<SensorType>(manu_data.data[0]) != STANDARD_BOTTOM_UP) {
|
||||
if (static_cast<SensorType>(manu_data.data[0]) != STANDARD_BOTTOM_UP &&
|
||||
static_cast<SensorType>(manu_data.data[0]) != PLUS_BOTTOM_UP) {
|
||||
ESP_LOGE(TAG, "Unsupported Sensor Type (0x%X)", manu_data.data[0]);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace mopeka_pro_check {
|
||||
enum SensorType {
|
||||
STANDARD_BOTTOM_UP = 0x03,
|
||||
TOP_DOWN_AIR_ABOVE = 0x04,
|
||||
BOTTOM_UP_WATER = 0x05
|
||||
BOTTOM_UP_WATER = 0x05,
|
||||
PLUS_BOTTOM_UP = 0x08
|
||||
// all other values are reserved
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from esphome.const import (
|
||||
CONF_AVAILABILITY,
|
||||
CONF_BIRTH_MESSAGE,
|
||||
CONF_BROKER,
|
||||
CONF_CERTIFICATE_AUTHORITY,
|
||||
CONF_CLIENT_ID,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_COMMAND_RETAIN,
|
||||
@@ -42,9 +43,14 @@ from esphome.const import (
|
||||
CONF_WILL_MESSAGE,
|
||||
)
|
||||
from esphome.core import coroutine_with_priority, CORE
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
|
||||
DEPENDENCIES = ["network"]
|
||||
AUTO_LOAD = ["json", "async_tcp"]
|
||||
|
||||
AUTO_LOAD = ["json"]
|
||||
|
||||
CONF_IDF_SEND_ASYNC = "idf_send_async"
|
||||
CONF_SKIP_CERT_CN_CHECK = "skip_cert_cn_check"
|
||||
|
||||
|
||||
def validate_message_just_topic(value):
|
||||
@@ -163,6 +169,15 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_USERNAME, default=""): cv.string,
|
||||
cv.Optional(CONF_PASSWORD, default=""): cv.string,
|
||||
cv.Optional(CONF_CLIENT_ID): cv.string,
|
||||
cv.SplitDefault(CONF_IDF_SEND_ASYNC, esp32_idf=False): cv.All(
|
||||
cv.boolean, cv.only_with_esp_idf
|
||||
),
|
||||
cv.Optional(CONF_CERTIFICATE_AUTHORITY): cv.All(
|
||||
cv.string, cv.only_with_esp_idf
|
||||
),
|
||||
cv.SplitDefault(CONF_SKIP_CERT_CN_CHECK, esp32_idf=False): cv.All(
|
||||
cv.boolean, cv.only_with_esp_idf
|
||||
),
|
||||
cv.Optional(CONF_DISCOVERY, default=True): cv.Any(
|
||||
cv.boolean, cv.one_of("CLEAN", upper=True)
|
||||
),
|
||||
@@ -217,7 +232,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
),
|
||||
validate_config,
|
||||
cv.only_with_arduino,
|
||||
)
|
||||
|
||||
|
||||
@@ -238,9 +252,11 @@ def exp_mqtt_message(config):
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
# Add required libraries for arduino
|
||||
if CORE.using_arduino:
|
||||
# https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json
|
||||
cg.add_library("ottowinter/AsyncMqttClient-esphome", "0.8.6")
|
||||
|
||||
# https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json
|
||||
cg.add_library("ottowinter/AsyncMqttClient-esphome", "0.8.6")
|
||||
cg.add_define("USE_MQTT")
|
||||
cg.add_global(mqtt_ns.using)
|
||||
|
||||
@@ -321,6 +337,19 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||
|
||||
# esp-idf only
|
||||
if CONF_CERTIFICATE_AUTHORITY in config:
|
||||
cg.add(var.set_ca_certificate(config[CONF_CERTIFICATE_AUTHORITY]))
|
||||
cg.add(var.set_skip_cert_cn_check(config[CONF_SKIP_CERT_CN_CHECK]))
|
||||
|
||||
# prevent error -0x428e
|
||||
# See https://github.com/espressif/esp-idf/issues/139
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_HARDWARE_MPI", False)
|
||||
|
||||
if CONF_IDF_SEND_ASYNC in config and config[CONF_IDF_SEND_ASYNC]:
|
||||
cg.add_define("USE_MQTT_IDF_ENQUEUE")
|
||||
# end esp-idf
|
||||
|
||||
for conf in config.get(CONF_ON_MESSAGE, []):
|
||||
trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC])
|
||||
cg.add(trig.set_qos(conf[CONF_QOS]))
|
||||
|
||||
69
esphome/components/mqtt/mqtt_backend.h
Normal file
69
esphome/components/mqtt/mqtt_backend.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace mqtt {
|
||||
|
||||
enum class MQTTClientDisconnectReason : int8_t {
|
||||
TCP_DISCONNECTED = 0,
|
||||
MQTT_UNACCEPTABLE_PROTOCOL_VERSION = 1,
|
||||
MQTT_IDENTIFIER_REJECTED = 2,
|
||||
MQTT_SERVER_UNAVAILABLE = 3,
|
||||
MQTT_MALFORMED_CREDENTIALS = 4,
|
||||
MQTT_NOT_AUTHORIZED = 5,
|
||||
ESP8266_NOT_ENOUGH_SPACE = 6,
|
||||
TLS_BAD_FINGERPRINT = 7
|
||||
};
|
||||
|
||||
/// internal struct for MQTT messages.
|
||||
struct MQTTMessage {
|
||||
std::string topic;
|
||||
std::string payload;
|
||||
uint8_t qos; ///< QoS. Only for last will testaments.
|
||||
bool retain;
|
||||
};
|
||||
|
||||
class MQTTBackend {
|
||||
public:
|
||||
using on_connect_callback_t = void(bool session_present);
|
||||
using on_disconnect_callback_t = void(MQTTClientDisconnectReason reason);
|
||||
using on_subscribe_callback_t = void(uint16_t packet_id, uint8_t qos);
|
||||
using on_unsubscribe_callback_t = void(uint16_t packet_id);
|
||||
using on_message_callback_t = void(const char *topic, const char *payload, size_t len, size_t index, size_t total);
|
||||
using on_publish_user_callback_t = void(uint16_t packet_id);
|
||||
|
||||
virtual void set_keep_alive(uint16_t keep_alive) = 0;
|
||||
virtual void set_client_id(const char *client_id) = 0;
|
||||
virtual void set_clean_session(bool clean_session) = 0;
|
||||
virtual void set_credentials(const char *username, const char *password) = 0;
|
||||
virtual void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) = 0;
|
||||
virtual void set_server(network::IPAddress ip, uint16_t port) = 0;
|
||||
virtual void set_server(const char *host, uint16_t port) = 0;
|
||||
virtual void set_on_connect(std::function<on_connect_callback_t> &&callback) = 0;
|
||||
virtual void set_on_disconnect(std::function<on_disconnect_callback_t> &&callback) = 0;
|
||||
virtual void set_on_subscribe(std::function<on_subscribe_callback_t> &&callback) = 0;
|
||||
virtual void set_on_unsubscribe(std::function<on_unsubscribe_callback_t> &&callback) = 0;
|
||||
virtual void set_on_message(std::function<on_message_callback_t> &&callback) = 0;
|
||||
virtual void set_on_publish(std::function<on_publish_user_callback_t> &&callback) = 0;
|
||||
virtual bool connected() const = 0;
|
||||
virtual void connect() = 0;
|
||||
virtual void disconnect() = 0;
|
||||
virtual bool subscribe(const char *topic, uint8_t qos) = 0;
|
||||
virtual bool unsubscribe(const char *topic) = 0;
|
||||
virtual bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) = 0;
|
||||
|
||||
virtual bool publish(const MQTTMessage &message) {
|
||||
return publish(message.topic.c_str(), message.payload.c_str(), message.payload.length(), message.qos,
|
||||
message.retain);
|
||||
}
|
||||
|
||||
// called from MQTTClient::loop()
|
||||
virtual void loop() {}
|
||||
};
|
||||
|
||||
} // namespace mqtt
|
||||
} // namespace esphome
|
||||
74
esphome/components/mqtt/mqtt_backend_arduino.h
Normal file
74
esphome/components/mqtt/mqtt_backend_arduino.h
Normal file
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "mqtt_backend.h"
|
||||
#include <AsyncMqttClient.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace mqtt {
|
||||
|
||||
class MQTTBackendArduino final : public MQTTBackend {
|
||||
public:
|
||||
void set_keep_alive(uint16_t keep_alive) final { mqtt_client_.setKeepAlive(keep_alive); }
|
||||
void set_client_id(const char *client_id) final { mqtt_client_.setClientId(client_id); }
|
||||
void set_clean_session(bool clean_session) final { mqtt_client_.setCleanSession(clean_session); }
|
||||
void set_credentials(const char *username, const char *password) final {
|
||||
mqtt_client_.setCredentials(username, password);
|
||||
}
|
||||
void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final {
|
||||
mqtt_client_.setWill(topic, qos, retain, payload);
|
||||
}
|
||||
void set_server(network::IPAddress ip, uint16_t port) final {
|
||||
mqtt_client_.setServer(IPAddress(static_cast<uint32_t>(ip)), port);
|
||||
}
|
||||
void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); }
|
||||
#if ASYNC_TCP_SSL_ENABLED
|
||||
void set_secure(bool secure) { mqtt_client.setSecure(secure); }
|
||||
void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); }
|
||||
#endif
|
||||
|
||||
void set_on_connect(std::function<on_connect_callback_t> &&callback) final {
|
||||
this->mqtt_client_.onConnect(std::move(callback));
|
||||
}
|
||||
void set_on_disconnect(std::function<on_disconnect_callback_t> &&callback) final {
|
||||
auto async_callback = [callback](AsyncMqttClientDisconnectReason reason) {
|
||||
// int based enum so casting isn't a problem
|
||||
callback(static_cast<MQTTClientDisconnectReason>(reason));
|
||||
};
|
||||
this->mqtt_client_.onDisconnect(std::move(async_callback));
|
||||
}
|
||||
void set_on_subscribe(std::function<on_subscribe_callback_t> &&callback) final {
|
||||
this->mqtt_client_.onSubscribe(std::move(callback));
|
||||
}
|
||||
void set_on_unsubscribe(std::function<on_unsubscribe_callback_t> &&callback) final {
|
||||
this->mqtt_client_.onUnsubscribe(std::move(callback));
|
||||
}
|
||||
void set_on_message(std::function<on_message_callback_t> &&callback) final {
|
||||
auto async_callback = [callback](const char *topic, const char *payload,
|
||||
AsyncMqttClientMessageProperties async_properties, size_t len, size_t index,
|
||||
size_t total) { callback(topic, payload, len, index, total); };
|
||||
mqtt_client_.onMessage(std::move(async_callback));
|
||||
}
|
||||
void set_on_publish(std::function<on_publish_user_callback_t> &&callback) final {
|
||||
this->mqtt_client_.onPublish(std::move(callback));
|
||||
}
|
||||
|
||||
bool connected() const final { return mqtt_client_.connected(); }
|
||||
void connect() final { mqtt_client_.connect(); }
|
||||
void disconnect() final { mqtt_client_.disconnect(true); }
|
||||
bool subscribe(const char *topic, uint8_t qos) final { return mqtt_client_.subscribe(topic, qos) != 0; }
|
||||
bool unsubscribe(const char *topic) final { return mqtt_client_.unsubscribe(topic) != 0; }
|
||||
bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final {
|
||||
return mqtt_client_.publish(topic, qos, retain, payload, length, false, 0) != 0;
|
||||
}
|
||||
using MQTTBackend::publish;
|
||||
|
||||
protected:
|
||||
AsyncMqttClient mqtt_client_;
|
||||
};
|
||||
|
||||
} // namespace mqtt
|
||||
} // namespace esphome
|
||||
|
||||
#endif // defined(USE_ARDUINO)
|
||||
149
esphome/components/mqtt/mqtt_backend_idf.cpp
Normal file
149
esphome/components/mqtt/mqtt_backend_idf.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
#include <string>
|
||||
#include "mqtt_backend_idf.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace mqtt {
|
||||
|
||||
static const char *const TAG = "mqtt.idf";
|
||||
|
||||
bool MQTTBackendIDF::initialize_() {
|
||||
mqtt_cfg_.user_context = (void *) this;
|
||||
mqtt_cfg_.buffer_size = MQTT_BUFFER_SIZE;
|
||||
|
||||
mqtt_cfg_.host = this->host_.c_str();
|
||||
mqtt_cfg_.port = this->port_;
|
||||
mqtt_cfg_.keepalive = this->keep_alive_;
|
||||
mqtt_cfg_.disable_clean_session = !this->clean_session_;
|
||||
|
||||
if (!this->username_.empty()) {
|
||||
mqtt_cfg_.username = this->username_.c_str();
|
||||
if (!this->password_.empty()) {
|
||||
mqtt_cfg_.password = this->password_.c_str();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->lwt_topic_.empty()) {
|
||||
mqtt_cfg_.lwt_topic = this->lwt_topic_.c_str();
|
||||
this->mqtt_cfg_.lwt_qos = this->lwt_qos_;
|
||||
this->mqtt_cfg_.lwt_retain = this->lwt_retain_;
|
||||
|
||||
if (!this->lwt_message_.empty()) {
|
||||
mqtt_cfg_.lwt_msg = this->lwt_message_.c_str();
|
||||
mqtt_cfg_.lwt_msg_len = this->lwt_message_.size();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->client_id_.empty()) {
|
||||
mqtt_cfg_.client_id = this->client_id_.c_str();
|
||||
}
|
||||
if (ca_certificate_.has_value()) {
|
||||
mqtt_cfg_.cert_pem = ca_certificate_.value().c_str();
|
||||
mqtt_cfg_.skip_cert_common_name_check = skip_cert_cn_check_;
|
||||
mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_SSL;
|
||||
} else {
|
||||
mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_TCP;
|
||||
}
|
||||
auto *mqtt_client = esp_mqtt_client_init(&mqtt_cfg_);
|
||||
if (mqtt_client) {
|
||||
handler_.reset(mqtt_client);
|
||||
is_initalized_ = true;
|
||||
esp_mqtt_client_register_event(mqtt_client, MQTT_EVENT_ANY, mqtt_event_handler, this);
|
||||
return true;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to initialize IDF-MQTT");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void MQTTBackendIDF::loop() {
|
||||
// process new events
|
||||
// handle only 1 message per loop iteration
|
||||
if (!mqtt_events_.empty()) {
|
||||
auto &event = mqtt_events_.front();
|
||||
mqtt_event_handler_(event);
|
||||
mqtt_events_.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void MQTTBackendIDF::mqtt_event_handler_(const esp_mqtt_event_t &event) {
|
||||
ESP_LOGV(TAG, "Event dispatched from event loop event_id=%d", event.event_id);
|
||||
switch (event.event_id) {
|
||||
case MQTT_EVENT_BEFORE_CONNECT:
|
||||
ESP_LOGV(TAG, "MQTT_EVENT_BEFORE_CONNECT");
|
||||
break;
|
||||
|
||||
case MQTT_EVENT_CONNECTED:
|
||||
ESP_LOGV(TAG, "MQTT_EVENT_CONNECTED");
|
||||
// TODO session present check
|
||||
this->is_connected_ = true;
|
||||
this->on_connect_.call(!mqtt_cfg_.disable_clean_session);
|
||||
break;
|
||||
case MQTT_EVENT_DISCONNECTED:
|
||||
ESP_LOGV(TAG, "MQTT_EVENT_DISCONNECTED");
|
||||
// TODO is there a way to get the disconnect reason?
|
||||
this->is_connected_ = false;
|
||||
this->on_disconnect_.call(MQTTClientDisconnectReason::TCP_DISCONNECTED);
|
||||
break;
|
||||
|
||||
case MQTT_EVENT_SUBSCRIBED:
|
||||
ESP_LOGV(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event.msg_id);
|
||||
// hardcode QoS to 0. QoS is not used in this context but required to mirror the AsyncMqtt interface
|
||||
this->on_subscribe_.call((int) event.msg_id, 0);
|
||||
break;
|
||||
case MQTT_EVENT_UNSUBSCRIBED:
|
||||
ESP_LOGV(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event.msg_id);
|
||||
this->on_unsubscribe_.call((int) event.msg_id);
|
||||
break;
|
||||
case MQTT_EVENT_PUBLISHED:
|
||||
ESP_LOGV(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event.msg_id);
|
||||
this->on_publish_.call((int) event.msg_id);
|
||||
break;
|
||||
case MQTT_EVENT_DATA: {
|
||||
static std::string topic;
|
||||
if (event.topic) {
|
||||
// not 0 terminated - create a string from it
|
||||
topic = std::string(event.topic, event.topic_len);
|
||||
}
|
||||
ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str());
|
||||
auto data_len = event.data_len;
|
||||
if (data_len == 0)
|
||||
data_len = strlen(event.data);
|
||||
this->on_message_.call(event.topic ? const_cast<char *>(topic.c_str()) : nullptr, event.data, data_len,
|
||||
event.current_data_offset, event.total_data_len);
|
||||
} break;
|
||||
case MQTT_EVENT_ERROR:
|
||||
ESP_LOGE(TAG, "MQTT_EVENT_ERROR");
|
||||
if (event.error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
|
||||
ESP_LOGE(TAG, "Last error code reported from esp-tls: 0x%x", event.error_handle->esp_tls_last_esp_err);
|
||||
ESP_LOGE(TAG, "Last tls stack error number: 0x%x", event.error_handle->esp_tls_stack_err);
|
||||
ESP_LOGE(TAG, "Last captured errno : %d (%s)", event.error_handle->esp_transport_sock_errno,
|
||||
strerror(event.error_handle->esp_transport_sock_errno));
|
||||
} else if (event.error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
|
||||
ESP_LOGE(TAG, "Connection refused error: 0x%x", event.error_handle->connect_return_code);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Unknown error type: 0x%x", event.error_handle->error_type);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ESP_LOGV(TAG, "Other event id:%d", event.event_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// static - Dispatch event to instance method
|
||||
void MQTTBackendIDF::mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) {
|
||||
MQTTBackendIDF *instance = static_cast<MQTTBackendIDF *>(handler_args);
|
||||
// queue event to decouple processing
|
||||
if (instance) {
|
||||
auto event = *static_cast<esp_mqtt_event_t *>(event_data);
|
||||
instance->mqtt_events_.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mqtt
|
||||
} // namespace esphome
|
||||
#endif // USE_ESP_IDF
|
||||
143
esphome/components/mqtt/mqtt_backend_idf.h
Normal file
143
esphome/components/mqtt/mqtt_backend_idf.h
Normal file
@@ -0,0 +1,143 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
#include <string>
|
||||
#include <queue>
|
||||
#include <mqtt_client.h>
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "mqtt_backend.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace mqtt {
|
||||
|
||||
class MQTTBackendIDF final : public MQTTBackend {
|
||||
public:
|
||||
static const size_t MQTT_BUFFER_SIZE = 4096;
|
||||
|
||||
void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; }
|
||||
void set_client_id(const char *client_id) final { this->client_id_ = client_id; }
|
||||
void set_clean_session(bool clean_session) final { this->clean_session_ = clean_session; }
|
||||
|
||||
void set_credentials(const char *username, const char *password) final {
|
||||
if (username)
|
||||
this->username_ = username;
|
||||
if (password)
|
||||
this->password_ = password;
|
||||
}
|
||||
void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final {
|
||||
if (topic)
|
||||
this->lwt_topic_ = topic;
|
||||
this->lwt_qos_ = qos;
|
||||
if (payload)
|
||||
this->lwt_message_ = payload;
|
||||
this->lwt_retain_ = retain;
|
||||
}
|
||||
void set_server(network::IPAddress ip, uint16_t port) final {
|
||||
this->host_ = ip.str();
|
||||
this->port_ = port;
|
||||
}
|
||||
void set_server(const char *host, uint16_t port) final {
|
||||
this->host_ = host;
|
||||
this->port_ = port;
|
||||
}
|
||||
void set_on_connect(std::function<on_connect_callback_t> &&callback) final {
|
||||
this->on_connect_.add(std::move(callback));
|
||||
}
|
||||
void set_on_disconnect(std::function<on_disconnect_callback_t> &&callback) final {
|
||||
this->on_disconnect_.add(std::move(callback));
|
||||
}
|
||||
void set_on_subscribe(std::function<on_subscribe_callback_t> &&callback) final {
|
||||
this->on_subscribe_.add(std::move(callback));
|
||||
}
|
||||
void set_on_unsubscribe(std::function<on_unsubscribe_callback_t> &&callback) final {
|
||||
this->on_unsubscribe_.add(std::move(callback));
|
||||
}
|
||||
void set_on_message(std::function<on_message_callback_t> &&callback) final {
|
||||
this->on_message_.add(std::move(callback));
|
||||
}
|
||||
void set_on_publish(std::function<on_publish_user_callback_t> &&callback) final {
|
||||
this->on_publish_.add(std::move(callback));
|
||||
}
|
||||
bool connected() const final { return this->is_connected_; }
|
||||
|
||||
void connect() final {
|
||||
if (!is_initalized_) {
|
||||
if (initialize_()) {
|
||||
esp_mqtt_client_start(handler_.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
void disconnect() final {
|
||||
if (is_initalized_)
|
||||
esp_mqtt_client_disconnect(handler_.get());
|
||||
}
|
||||
|
||||
bool subscribe(const char *topic, uint8_t qos) final {
|
||||
return esp_mqtt_client_subscribe(handler_.get(), topic, qos) != -1;
|
||||
}
|
||||
bool unsubscribe(const char *topic) final { return esp_mqtt_client_unsubscribe(handler_.get(), topic) != -1; }
|
||||
|
||||
bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final {
|
||||
#if defined(USE_MQTT_IDF_ENQUEUE)
|
||||
// use the non-blocking version
|
||||
// it can delay sending a couple of seconds but won't block
|
||||
return esp_mqtt_client_enqueue(handler_.get(), topic, payload, length, qos, retain, true) != -1;
|
||||
#else
|
||||
// might block for several seconds, either due to network timeout (10s)
|
||||
// or if publishing payloads longer than internal buffer (due to message fragmentation)
|
||||
return esp_mqtt_client_publish(handler_.get(), topic, payload, length, qos, retain) != -1;
|
||||
#endif
|
||||
}
|
||||
using MQTTBackend::publish;
|
||||
|
||||
void loop() final;
|
||||
|
||||
void set_ca_certificate(const std::string &cert) { ca_certificate_ = cert; }
|
||||
void set_skip_cert_cn_check(bool skip_check) { skip_cert_cn_check_ = skip_check; }
|
||||
|
||||
protected:
|
||||
bool initialize_();
|
||||
void mqtt_event_handler_(const esp_mqtt_event_t &event);
|
||||
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data);
|
||||
|
||||
struct MqttClientDeleter {
|
||||
void operator()(esp_mqtt_client *client_handler) { esp_mqtt_client_destroy(client_handler); }
|
||||
};
|
||||
using ClientHandler_ = std::unique_ptr<esp_mqtt_client, MqttClientDeleter>;
|
||||
ClientHandler_ handler_;
|
||||
|
||||
bool is_connected_{false};
|
||||
bool is_initalized_{false};
|
||||
|
||||
esp_mqtt_client_config_t mqtt_cfg_{};
|
||||
|
||||
std::string host_;
|
||||
uint16_t port_;
|
||||
std::string username_;
|
||||
std::string password_;
|
||||
std::string lwt_topic_;
|
||||
std::string lwt_message_;
|
||||
uint8_t lwt_qos_;
|
||||
bool lwt_retain_;
|
||||
std::string client_id_;
|
||||
uint16_t keep_alive_;
|
||||
bool clean_session_;
|
||||
optional<std::string> ca_certificate_;
|
||||
bool skip_cert_cn_check_{false};
|
||||
|
||||
// callbacks
|
||||
CallbackManager<on_connect_callback_t> on_connect_;
|
||||
CallbackManager<on_disconnect_callback_t> on_disconnect_;
|
||||
CallbackManager<on_subscribe_callback_t> on_subscribe_;
|
||||
CallbackManager<on_unsubscribe_callback_t> on_unsubscribe_;
|
||||
CallbackManager<on_message_callback_t> on_message_;
|
||||
CallbackManager<on_publish_user_callback_t> on_publish_;
|
||||
std::queue<esp_mqtt_event_t> mqtt_events_;
|
||||
};
|
||||
|
||||
} // namespace mqtt
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
@@ -27,21 +27,21 @@ MQTTClientComponent::MQTTClientComponent() {
|
||||
// Connection
|
||||
void MQTTClientComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up MQTT...");
|
||||
this->mqtt_client_.onMessage([this](char const *topic, char *payload, AsyncMqttClientMessageProperties properties,
|
||||
size_t len, size_t index, size_t total) {
|
||||
if (index == 0)
|
||||
this->payload_buffer_.reserve(total);
|
||||
this->mqtt_backend_.set_on_message(
|
||||
[this](const char *topic, const char *payload, size_t len, size_t index, size_t total) {
|
||||
if (index == 0)
|
||||
this->payload_buffer_.reserve(total);
|
||||
|
||||
// append new payload, may contain incomplete MQTT message
|
||||
this->payload_buffer_.append(payload, len);
|
||||
// append new payload, may contain incomplete MQTT message
|
||||
this->payload_buffer_.append(payload, len);
|
||||
|
||||
// MQTT fully received
|
||||
if (len + index == total) {
|
||||
this->on_message(topic, this->payload_buffer_);
|
||||
this->payload_buffer_.clear();
|
||||
}
|
||||
});
|
||||
this->mqtt_client_.onDisconnect([this](AsyncMqttClientDisconnectReason reason) {
|
||||
// MQTT fully received
|
||||
if (len + index == total) {
|
||||
this->on_message(topic, this->payload_buffer_);
|
||||
this->payload_buffer_.clear();
|
||||
}
|
||||
});
|
||||
this->mqtt_backend_.set_on_disconnect([this](MQTTClientDisconnectReason reason) {
|
||||
this->state_ = MQTT_CLIENT_DISCONNECTED;
|
||||
this->disconnect_reason_ = reason;
|
||||
});
|
||||
@@ -49,8 +49,10 @@ void MQTTClientComponent::setup() {
|
||||
if (this->is_log_message_enabled() && logger::global_logger != nullptr) {
|
||||
logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
|
||||
if (level <= this->log_level_ && this->is_connected()) {
|
||||
this->publish(this->log_message_.topic, message, strlen(message), this->log_message_.qos,
|
||||
this->log_message_.retain);
|
||||
this->publish({.topic = this->log_message_.topic,
|
||||
.payload = message,
|
||||
.qos = this->log_message_.qos,
|
||||
.retain = this->log_message_.retain});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -173,9 +175,9 @@ void MQTTClientComponent::start_connect_() {
|
||||
|
||||
ESP_LOGI(TAG, "Connecting to MQTT...");
|
||||
// Force disconnect first
|
||||
this->mqtt_client_.disconnect(true);
|
||||
this->mqtt_backend_.disconnect();
|
||||
|
||||
this->mqtt_client_.setClientId(this->credentials_.client_id.c_str());
|
||||
this->mqtt_backend_.set_client_id(this->credentials_.client_id.c_str());
|
||||
const char *username = nullptr;
|
||||
if (!this->credentials_.username.empty())
|
||||
username = this->credentials_.username.c_str();
|
||||
@@ -183,24 +185,24 @@ void MQTTClientComponent::start_connect_() {
|
||||
if (!this->credentials_.password.empty())
|
||||
password = this->credentials_.password.c_str();
|
||||
|
||||
this->mqtt_client_.setCredentials(username, password);
|
||||
this->mqtt_backend_.set_credentials(username, password);
|
||||
|
||||
this->mqtt_client_.setServer((uint32_t) this->ip_, this->credentials_.port);
|
||||
this->mqtt_backend_.set_server((uint32_t) this->ip_, this->credentials_.port);
|
||||
if (!this->last_will_.topic.empty()) {
|
||||
this->mqtt_client_.setWill(this->last_will_.topic.c_str(), this->last_will_.qos, this->last_will_.retain,
|
||||
this->last_will_.payload.c_str(), this->last_will_.payload.length());
|
||||
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());
|
||||
}
|
||||
|
||||
this->mqtt_client_.connect();
|
||||
this->mqtt_backend_.connect();
|
||||
this->state_ = MQTT_CLIENT_CONNECTING;
|
||||
this->connect_begin_ = millis();
|
||||
}
|
||||
bool MQTTClientComponent::is_connected() {
|
||||
return this->state_ == MQTT_CLIENT_CONNECTED && this->mqtt_client_.connected();
|
||||
return this->state_ == MQTT_CLIENT_CONNECTED && this->mqtt_backend_.connected();
|
||||
}
|
||||
|
||||
void MQTTClientComponent::check_connected() {
|
||||
if (!this->mqtt_client_.connected()) {
|
||||
if (!this->mqtt_backend_.connected()) {
|
||||
if (millis() - this->connect_begin_ > 60000) {
|
||||
this->state_ = MQTT_CLIENT_DISCONNECTED;
|
||||
this->start_dnslookup_();
|
||||
@@ -222,31 +224,34 @@ void MQTTClientComponent::check_connected() {
|
||||
}
|
||||
|
||||
void MQTTClientComponent::loop() {
|
||||
// Call the backend loop first
|
||||
mqtt_backend_.loop();
|
||||
|
||||
if (this->disconnect_reason_.has_value()) {
|
||||
const LogString *reason_s;
|
||||
switch (*this->disconnect_reason_) {
|
||||
case AsyncMqttClientDisconnectReason::TCP_DISCONNECTED:
|
||||
case MQTTClientDisconnectReason::TCP_DISCONNECTED:
|
||||
reason_s = LOG_STR("TCP disconnected");
|
||||
break;
|
||||
case AsyncMqttClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||
case MQTTClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||
reason_s = LOG_STR("Unacceptable Protocol Version");
|
||||
break;
|
||||
case AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED:
|
||||
case MQTTClientDisconnectReason::MQTT_IDENTIFIER_REJECTED:
|
||||
reason_s = LOG_STR("Identifier Rejected");
|
||||
break;
|
||||
case AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE:
|
||||
case MQTTClientDisconnectReason::MQTT_SERVER_UNAVAILABLE:
|
||||
reason_s = LOG_STR("Server Unavailable");
|
||||
break;
|
||||
case AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS:
|
||||
case MQTTClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS:
|
||||
reason_s = LOG_STR("Malformed Credentials");
|
||||
break;
|
||||
case AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED:
|
||||
case MQTTClientDisconnectReason::MQTT_NOT_AUTHORIZED:
|
||||
reason_s = LOG_STR("Not Authorized");
|
||||
break;
|
||||
case AsyncMqttClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE:
|
||||
case MQTTClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE:
|
||||
reason_s = LOG_STR("Not Enough Space");
|
||||
break;
|
||||
case AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT:
|
||||
case MQTTClientDisconnectReason::TLS_BAD_FINGERPRINT:
|
||||
reason_s = LOG_STR("TLS Bad Fingerprint");
|
||||
break;
|
||||
default:
|
||||
@@ -275,7 +280,7 @@ void MQTTClientComponent::loop() {
|
||||
this->check_connected();
|
||||
break;
|
||||
case MQTT_CLIENT_CONNECTED:
|
||||
if (!this->mqtt_client_.connected()) {
|
||||
if (!this->mqtt_backend_.connected()) {
|
||||
this->state_ = MQTT_CLIENT_DISCONNECTED;
|
||||
ESP_LOGW(TAG, "Lost MQTT Client connection!");
|
||||
this->start_dnslookup_();
|
||||
@@ -302,10 +307,10 @@ bool MQTTClientComponent::subscribe_(const char *topic, uint8_t qos) {
|
||||
if (!this->is_connected())
|
||||
return false;
|
||||
|
||||
uint16_t ret = this->mqtt_client_.subscribe(topic, qos);
|
||||
bool ret = this->mqtt_backend_.subscribe(topic, qos);
|
||||
yield();
|
||||
|
||||
if (ret != 0) {
|
||||
if (ret) {
|
||||
ESP_LOGV(TAG, "subscribe(topic='%s')", topic);
|
||||
} else {
|
||||
delay(5);
|
||||
@@ -360,9 +365,9 @@ void MQTTClientComponent::subscribe_json(const std::string &topic, const mqtt_js
|
||||
}
|
||||
|
||||
void MQTTClientComponent::unsubscribe(const std::string &topic) {
|
||||
uint16_t ret = this->mqtt_client_.unsubscribe(topic.c_str());
|
||||
bool ret = this->mqtt_backend_.unsubscribe(topic.c_str());
|
||||
yield();
|
||||
if (ret != 0) {
|
||||
if (ret) {
|
||||
ESP_LOGV(TAG, "unsubscribe(topic='%s')", topic.c_str());
|
||||
} else {
|
||||
delay(5);
|
||||
@@ -387,34 +392,35 @@ bool MQTTClientComponent::publish(const std::string &topic, const std::string &p
|
||||
|
||||
bool MQTTClientComponent::publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos,
|
||||
bool retain) {
|
||||
return publish({.topic = topic, .payload = payload, .qos = qos, .retain = retain});
|
||||
}
|
||||
|
||||
bool MQTTClientComponent::publish(const MQTTMessage &message) {
|
||||
if (!this->is_connected()) {
|
||||
// critical components will re-transmit their messages
|
||||
return false;
|
||||
}
|
||||
bool logging_topic = topic == this->log_message_.topic;
|
||||
uint16_t ret = this->mqtt_client_.publish(topic.c_str(), qos, retain, payload, payload_length);
|
||||
bool logging_topic = this->log_message_.topic == message.topic;
|
||||
bool ret = this->mqtt_backend_.publish(message);
|
||||
delay(0);
|
||||
if (ret == 0 && !logging_topic && this->is_connected()) {
|
||||
if (!ret && !logging_topic && this->is_connected()) {
|
||||
delay(0);
|
||||
ret = this->mqtt_client_.publish(topic.c_str(), qos, retain, payload, payload_length);
|
||||
ret = this->mqtt_backend_.publish(message);
|
||||
delay(0);
|
||||
}
|
||||
|
||||
if (!logging_topic) {
|
||||
if (ret != 0) {
|
||||
ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d)", topic.c_str(), payload, retain);
|
||||
if (ret) {
|
||||
ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d)", message.topic.c_str(), message.payload.c_str(),
|
||||
message.retain);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). will retry later..", topic.c_str(),
|
||||
payload_length); // NOLINT
|
||||
ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). will retry later..", message.topic.c_str(),
|
||||
message.payload.length());
|
||||
this->status_momentary_warning("publish", 1000);
|
||||
}
|
||||
}
|
||||
return ret != 0;
|
||||
}
|
||||
|
||||
bool MQTTClientComponent::publish(const MQTTMessage &message) {
|
||||
return this->publish(message.topic, message.payload, message.qos, message.retain);
|
||||
}
|
||||
bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos,
|
||||
bool retain) {
|
||||
std::string message = json::build_json(f);
|
||||
@@ -499,10 +505,10 @@ bool MQTTClientComponent::is_log_message_enabled() const { return !this->log_mes
|
||||
void MQTTClientComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
void MQTTClientComponent::register_mqtt_component(MQTTComponent *component) { this->children_.push_back(component); }
|
||||
void MQTTClientComponent::set_log_level(int level) { this->log_level_ = level; }
|
||||
void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_client_.setKeepAlive(keep_alive_s); }
|
||||
void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_backend_.set_keep_alive(keep_alive_s); }
|
||||
void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); }
|
||||
const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; }
|
||||
void MQTTClientComponent::set_topic_prefix(std::string topic_prefix) { this->topic_prefix_ = std::move(topic_prefix); }
|
||||
void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix) { this->topic_prefix_ = topic_prefix; }
|
||||
const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; }
|
||||
void MQTTClientComponent::disable_birth_message() {
|
||||
this->birth_message_.topic = "";
|
||||
@@ -549,7 +555,13 @@ void MQTTClientComponent::set_discovery_info(std::string &&prefix, MQTTDiscovery
|
||||
void MQTTClientComponent::disable_last_will() { this->last_will_.topic = ""; }
|
||||
|
||||
void MQTTClientComponent::disable_discovery() {
|
||||
this->discovery_info_ = MQTTDiscoveryInfo{.prefix = "", .retain = false};
|
||||
this->discovery_info_ = MQTTDiscoveryInfo{
|
||||
.prefix = "",
|
||||
.retain = false,
|
||||
.clean = false,
|
||||
.unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR,
|
||||
.object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR,
|
||||
};
|
||||
}
|
||||
void MQTTClientComponent::on_shutdown() {
|
||||
if (!this->shutdown_message_.topic.empty()) {
|
||||
@@ -557,13 +569,13 @@ void MQTTClientComponent::on_shutdown() {
|
||||
this->publish(this->shutdown_message_);
|
||||
yield();
|
||||
}
|
||||
this->mqtt_client_.disconnect(true);
|
||||
this->mqtt_backend_.disconnect();
|
||||
}
|
||||
|
||||
#if ASYNC_TCP_SSL_ENABLED
|
||||
void MQTTClientComponent::add_ssl_fingerprint(const std::array<uint8_t, SHA1_SIZE> &fingerprint) {
|
||||
this->mqtt_client_.setSecure(true);
|
||||
this->mqtt_client_.addServerFingerprint(fingerprint.data());
|
||||
this->mqtt_backend_.setSecure(true);
|
||||
this->mqtt_backend_.addServerFingerprint(fingerprint.data());
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#include <AsyncMqttClient.h>
|
||||
#if defined(USE_ESP_IDF)
|
||||
#include "mqtt_backend_idf.h"
|
||||
#elif defined(USE_ARDUINO)
|
||||
#include "mqtt_backend_arduino.h"
|
||||
#endif
|
||||
#include "lwip/ip_addr.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -22,14 +26,6 @@ namespace mqtt {
|
||||
using mqtt_callback_t = std::function<void(const std::string &, const std::string &)>;
|
||||
using mqtt_json_callback_t = std::function<void(const std::string &, JsonObject)>;
|
||||
|
||||
/// internal struct for MQTT messages.
|
||||
struct MQTTMessage {
|
||||
std::string topic;
|
||||
std::string payload;
|
||||
uint8_t qos; ///< QoS. Only for last will testaments.
|
||||
bool retain;
|
||||
};
|
||||
|
||||
/// internal struct for MQTT subscriptions.
|
||||
struct MQTTSubscription {
|
||||
std::string topic;
|
||||
@@ -139,7 +135,10 @@ class MQTTClientComponent : public Component {
|
||||
*/
|
||||
void add_ssl_fingerprint(const std::array<uint8_t, SHA1_SIZE> &fingerprint);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); }
|
||||
void set_skip_cert_cn_check(bool skip_check) { this->mqtt_backend_.set_skip_cert_cn_check(skip_check); }
|
||||
#endif
|
||||
const Availability &get_availability();
|
||||
|
||||
/** Set the topic prefix that will be prepended to all topics together with "/". This will, in most cases,
|
||||
@@ -150,7 +149,7 @@ class MQTTClientComponent : public Component {
|
||||
*
|
||||
* @param topic_prefix The topic prefix. The last "/" is appended automatically.
|
||||
*/
|
||||
void set_topic_prefix(std::string topic_prefix);
|
||||
void set_topic_prefix(const std::string &topic_prefix);
|
||||
/// Get the topic prefix of this device, using default if necessary
|
||||
const std::string &get_topic_prefix() const;
|
||||
|
||||
@@ -277,6 +276,8 @@ class MQTTClientComponent : public Component {
|
||||
.prefix = "homeassistant",
|
||||
.retain = true,
|
||||
.clean = false,
|
||||
.unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR,
|
||||
.object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR,
|
||||
};
|
||||
std::string topic_prefix_{};
|
||||
MQTTMessage log_message_;
|
||||
@@ -284,7 +285,12 @@ class MQTTClientComponent : public Component {
|
||||
int log_level_{ESPHOME_LOG_LEVEL};
|
||||
|
||||
std::vector<MQTTSubscription> subscriptions_;
|
||||
AsyncMqttClient mqtt_client_;
|
||||
#if defined(USE_ESP_IDF)
|
||||
MQTTBackendIDF mqtt_backend_;
|
||||
#elif defined(USE_ARDUINO)
|
||||
MQTTBackendArduino mqtt_backend_;
|
||||
#endif
|
||||
|
||||
MQTTClientState state_{MQTT_CLIENT_DISCONNECTED};
|
||||
network::IPAddress ip_;
|
||||
bool dns_resolved_{false};
|
||||
@@ -293,7 +299,7 @@ class MQTTClientComponent : public Component {
|
||||
uint32_t reboot_timeout_{300000};
|
||||
uint32_t connect_begin_;
|
||||
uint32_t last_connected_{0};
|
||||
optional<AsyncMqttClientDisconnectReason> disconnect_reason_{};
|
||||
optional<MQTTClientDisconnectReason> disconnect_reason_{};
|
||||
};
|
||||
|
||||
extern MQTTClientComponent *global_mqtt_client; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -21,7 +21,7 @@ void MQTTSelectComponent::setup() {
|
||||
call.set_option(state);
|
||||
call.perform();
|
||||
});
|
||||
this->select_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); });
|
||||
this->select_->add_on_state_callback([this](const std::string &state, size_t index) { this->publish_state(state); });
|
||||
}
|
||||
|
||||
void MQTTSelectComponent::dump_config() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
@@ -5,3 +6,7 @@ CODEOWNERS = ["@jesserockz"]
|
||||
nfc_ns = cg.esphome_ns.namespace("nfc")
|
||||
|
||||
NfcTag = nfc_ns.class_("NfcTag")
|
||||
|
||||
NfcOnTagTrigger = nfc_ns.class_(
|
||||
"NfcOnTagTrigger", automation.Trigger.template(cg.std_string, NfcTag)
|
||||
)
|
||||
|
||||
9
esphome/components/nfc/automation.cpp
Normal file
9
esphome/components/nfc/automation.cpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#include "automation.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace nfc {
|
||||
|
||||
void NfcOnTagTrigger::process(const std::unique_ptr<NfcTag> &tag) { this->trigger(format_uid(tag->get_uid()), *tag); }
|
||||
|
||||
} // namespace nfc
|
||||
} // namespace esphome
|
||||
17
esphome/components/nfc/automation.h
Normal file
17
esphome/components/nfc/automation.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
#include "nfc.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace nfc {
|
||||
|
||||
class NfcOnTagTrigger : public Trigger<std::string, NfcTag> {
|
||||
public:
|
||||
void process(const std::unique_ptr<NfcTag> &tag);
|
||||
};
|
||||
|
||||
} // namespace nfc
|
||||
} // namespace esphome
|
||||
@@ -14,6 +14,8 @@ from esphome.const import (
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_MQTT_ID,
|
||||
CONF_VALUE,
|
||||
CONF_OPERATION,
|
||||
CONF_CYCLE,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
@@ -35,6 +37,7 @@ ValueRangeTrigger = number_ns.class_(
|
||||
|
||||
# Actions
|
||||
NumberSetAction = number_ns.class_("NumberSetAction", automation.Action)
|
||||
NumberOperationAction = number_ns.class_("NumberOperationAction", automation.Action)
|
||||
|
||||
# Conditions
|
||||
NumberInRangeCondition = number_ns.class_(
|
||||
@@ -49,6 +52,15 @@ NUMBER_MODES = {
|
||||
"SLIDER": NumberMode.NUMBER_MODE_SLIDER,
|
||||
}
|
||||
|
||||
NumberOperation = number_ns.enum("NumberOperation")
|
||||
|
||||
NUMBER_OPERATION_OPTIONS = {
|
||||
"INCREMENT": NumberOperation.NUMBER_OP_INCREMENT,
|
||||
"DECREMENT": NumberOperation.NUMBER_OP_DECREMENT,
|
||||
"TO_MIN": NumberOperation.NUMBER_OP_TO_MIN,
|
||||
"TO_MAX": NumberOperation.NUMBER_OP_TO_MAX,
|
||||
}
|
||||
|
||||
icon = cv.icon
|
||||
|
||||
NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
|
||||
@@ -63,8 +75,8 @@ NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).e
|
||||
cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger),
|
||||
cv.Optional(CONF_ABOVE): cv.float_,
|
||||
cv.Optional(CONF_BELOW): cv.float_,
|
||||
cv.Optional(CONF_ABOVE): cv.templatable(cv.float_),
|
||||
cv.Optional(CONF_BELOW): cv.templatable(cv.float_),
|
||||
},
|
||||
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
|
||||
),
|
||||
@@ -159,12 +171,18 @@ async def to_code(config):
|
||||
cg.add_global(number_ns.using)
|
||||
|
||||
|
||||
OPERATION_BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Number),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"number.set",
|
||||
NumberSetAction,
|
||||
cv.Schema(
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Number),
|
||||
cv.Required(CONF_VALUE): cv.templatable(cv.float_),
|
||||
}
|
||||
),
|
||||
@@ -175,3 +193,85 @@ async def number_set_to_code(config, action_id, template_arg, args):
|
||||
template_ = await cg.templatable(config[CONF_VALUE], args, float)
|
||||
cg.add(var.set_value(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"number.increment",
|
||||
NumberOperationAction,
|
||||
automation.maybe_simple_id(
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MODE, default="INCREMENT"): cv.one_of(
|
||||
"INCREMENT", upper=True
|
||||
),
|
||||
cv.Optional(CONF_CYCLE, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
),
|
||||
)
|
||||
@automation.register_action(
|
||||
"number.decrement",
|
||||
NumberOperationAction,
|
||||
automation.maybe_simple_id(
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MODE, default="DECREMENT"): cv.one_of(
|
||||
"DECREMENT", upper=True
|
||||
),
|
||||
cv.Optional(CONF_CYCLE, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
),
|
||||
)
|
||||
@automation.register_action(
|
||||
"number.to_min",
|
||||
NumberOperationAction,
|
||||
automation.maybe_simple_id(
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MODE, default="TO_MIN"): cv.one_of(
|
||||
"TO_MIN", upper=True
|
||||
),
|
||||
}
|
||||
)
|
||||
),
|
||||
)
|
||||
@automation.register_action(
|
||||
"number.to_max",
|
||||
NumberOperationAction,
|
||||
automation.maybe_simple_id(
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MODE, default="TO_MAX"): cv.one_of(
|
||||
"TO_MAX", upper=True
|
||||
),
|
||||
}
|
||||
)
|
||||
),
|
||||
)
|
||||
@automation.register_action(
|
||||
"number.operation",
|
||||
NumberOperationAction,
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_OPERATION): cv.templatable(
|
||||
cv.enum(NUMBER_OPERATION_OPTIONS, upper=True)
|
||||
),
|
||||
cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def number_to_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
if CONF_OPERATION in config:
|
||||
to_ = await cg.templatable(config[CONF_OPERATION], args, NumberOperation)
|
||||
cg.add(var.set_operation(to_))
|
||||
if CONF_CYCLE in config:
|
||||
cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool)
|
||||
cg.add(var.set_cycle(cycle_))
|
||||
if CONF_MODE in config:
|
||||
cg.add(var.set_operation(NUMBER_OPERATION_OPTIONS[config[CONF_MODE]]))
|
||||
if CONF_CYCLE in config:
|
||||
cg.add(var.set_cycle(config[CONF_CYCLE]))
|
||||
return var
|
||||
|
||||
@@ -29,6 +29,25 @@ template<typename... Ts> class NumberSetAction : public Action<Ts...> {
|
||||
Number *number_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class NumberOperationAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit NumberOperationAction(Number *number) : number_(number) {}
|
||||
TEMPLATABLE_VALUE(NumberOperation, operation)
|
||||
TEMPLATABLE_VALUE(bool, cycle)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto call = this->number_->make_call();
|
||||
call.with_operation(this->operation_.value(x...));
|
||||
if (this->cycle_.has_value()) {
|
||||
call.with_cycle(this->cycle_.value(x...));
|
||||
}
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
Number *number_;
|
||||
};
|
||||
|
||||
class ValueRangeTrigger : public Trigger<float>, public Component {
|
||||
public:
|
||||
explicit ValueRangeTrigger(Number *parent) : parent_(parent) {}
|
||||
|
||||
@@ -6,30 +6,6 @@ namespace number {
|
||||
|
||||
static const char *const TAG = "number";
|
||||
|
||||
void NumberCall::perform() {
|
||||
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
|
||||
if (!this->value_.has_value() || std::isnan(*this->value_)) {
|
||||
ESP_LOGW(TAG, "No value set for NumberCall");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &traits = this->parent_->traits;
|
||||
auto value = *this->value_;
|
||||
|
||||
float min_value = traits.get_min_value();
|
||||
if (value < min_value) {
|
||||
ESP_LOGW(TAG, " Value %f must not be less than minimum %f", value, min_value);
|
||||
return;
|
||||
}
|
||||
float max_value = traits.get_max_value();
|
||||
if (value > max_value) {
|
||||
ESP_LOGW(TAG, " Value %f must not be greater than maximum %f", value, max_value);
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, " Value: %f", *this->value_);
|
||||
this->parent_->control(*this->value_);
|
||||
}
|
||||
|
||||
void Number::publish_state(float state) {
|
||||
this->has_state_ = true;
|
||||
this->state = state;
|
||||
@@ -41,15 +17,6 @@ void Number::add_on_state_callback(std::function<void(float)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
std::string NumberTraits::get_unit_of_measurement() {
|
||||
if (this->unit_of_measurement_.has_value())
|
||||
return *this->unit_of_measurement_;
|
||||
return "";
|
||||
}
|
||||
void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) {
|
||||
this->unit_of_measurement_ = unit_of_measurement;
|
||||
}
|
||||
|
||||
uint32_t Number::hash_base() { return 2282307003UL; }
|
||||
|
||||
} // namespace number
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "number_call.h"
|
||||
#include "number_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
@@ -20,54 +22,6 @@ namespace number {
|
||||
|
||||
class Number;
|
||||
|
||||
class NumberCall {
|
||||
public:
|
||||
explicit NumberCall(Number *parent) : parent_(parent) {}
|
||||
void perform();
|
||||
|
||||
NumberCall &set_value(float value) {
|
||||
value_ = value;
|
||||
return *this;
|
||||
}
|
||||
const optional<float> &get_value() const { return value_; }
|
||||
|
||||
protected:
|
||||
Number *const parent_;
|
||||
optional<float> value_;
|
||||
};
|
||||
|
||||
enum NumberMode : uint8_t {
|
||||
NUMBER_MODE_AUTO = 0,
|
||||
NUMBER_MODE_BOX = 1,
|
||||
NUMBER_MODE_SLIDER = 2,
|
||||
};
|
||||
|
||||
class NumberTraits {
|
||||
public:
|
||||
void set_min_value(float min_value) { min_value_ = min_value; }
|
||||
float get_min_value() const { return min_value_; }
|
||||
void set_max_value(float max_value) { max_value_ = max_value; }
|
||||
float get_max_value() const { return max_value_; }
|
||||
void set_step(float step) { step_ = step; }
|
||||
float get_step() const { return step_; }
|
||||
|
||||
/// Get the unit of measurement, using the manual override if set.
|
||||
std::string get_unit_of_measurement();
|
||||
/// Manually set the unit of measurement.
|
||||
void set_unit_of_measurement(const std::string &unit_of_measurement);
|
||||
|
||||
// Get/set the frontend mode.
|
||||
NumberMode get_mode() const { return this->mode_; }
|
||||
void set_mode(NumberMode mode) { this->mode_ = mode; }
|
||||
|
||||
protected:
|
||||
float min_value_ = NAN;
|
||||
float max_value_ = NAN;
|
||||
float step_ = NAN;
|
||||
optional<std::string> unit_of_measurement_; ///< Unit of measurement override
|
||||
NumberMode mode_{NUMBER_MODE_AUTO};
|
||||
};
|
||||
|
||||
/** Base-class for all numbers.
|
||||
*
|
||||
* A number can use publish_state to send out a new value.
|
||||
@@ -79,7 +33,6 @@ class Number : public EntityBase {
|
||||
void publish_state(float state);
|
||||
|
||||
NumberCall make_call() { return NumberCall(this); }
|
||||
void set(float value) { make_call().set_value(value).perform(); }
|
||||
|
||||
void add_on_state_callback(std::function<void(float)> &&callback);
|
||||
|
||||
|
||||
118
esphome/components/number/number_call.cpp
Normal file
118
esphome/components/number/number_call.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#include "number_call.h"
|
||||
#include "number.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
static const char *const TAG = "number";
|
||||
|
||||
NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); }
|
||||
|
||||
NumberCall &NumberCall::number_increment(bool cycle) {
|
||||
return this->with_operation(NUMBER_OP_INCREMENT).with_cycle(cycle);
|
||||
}
|
||||
|
||||
NumberCall &NumberCall::number_decrement(bool cycle) {
|
||||
return this->with_operation(NUMBER_OP_DECREMENT).with_cycle(cycle);
|
||||
}
|
||||
|
||||
NumberCall &NumberCall::number_to_min() { return this->with_operation(NUMBER_OP_TO_MIN); }
|
||||
|
||||
NumberCall &NumberCall::number_to_max() { return this->with_operation(NUMBER_OP_TO_MAX); }
|
||||
|
||||
NumberCall &NumberCall::with_operation(NumberOperation operation) {
|
||||
this->operation_ = operation;
|
||||
return *this;
|
||||
}
|
||||
|
||||
NumberCall &NumberCall::with_value(float value) {
|
||||
this->value_ = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
NumberCall &NumberCall::with_cycle(bool cycle) {
|
||||
this->cycle_ = cycle;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void NumberCall::perform() {
|
||||
auto *parent = this->parent_;
|
||||
const auto *name = parent->get_name().c_str();
|
||||
const auto &traits = parent->traits;
|
||||
|
||||
if (this->operation_ == NUMBER_OP_NONE) {
|
||||
ESP_LOGW(TAG, "'%s' - NumberCall performed without selecting an operation", name);
|
||||
return;
|
||||
}
|
||||
|
||||
float target_value = NAN;
|
||||
float min_value = traits.get_min_value();
|
||||
float max_value = traits.get_max_value();
|
||||
|
||||
if (this->operation_ == NUMBER_OP_SET) {
|
||||
ESP_LOGD(TAG, "'%s' - Setting number value", name);
|
||||
if (!this->value_.has_value() || std::isnan(*this->value_)) {
|
||||
ESP_LOGW(TAG, "'%s' - No value set for NumberCall", name);
|
||||
return;
|
||||
}
|
||||
target_value = this->value_.value();
|
||||
} else if (this->operation_ == NUMBER_OP_TO_MIN) {
|
||||
if (std::isnan(min_value)) {
|
||||
ESP_LOGW(TAG, "'%s' - Can't set to min value through NumberCall: no min_value defined", name);
|
||||
} else {
|
||||
target_value = min_value;
|
||||
}
|
||||
} else if (this->operation_ == NUMBER_OP_TO_MAX) {
|
||||
if (std::isnan(max_value)) {
|
||||
ESP_LOGW(TAG, "'%s' - Can't set to max value through NumberCall: no max_value defined", name);
|
||||
} else {
|
||||
target_value = max_value;
|
||||
}
|
||||
} else if (this->operation_ == NUMBER_OP_INCREMENT) {
|
||||
ESP_LOGD(TAG, "'%s' - Increment number, with%s cycling", name, this->cycle_ ? "" : "out");
|
||||
if (!parent->has_state()) {
|
||||
ESP_LOGW(TAG, "'%s' - Can't increment number through NumberCall: no active state to modify", name);
|
||||
return;
|
||||
}
|
||||
auto step = traits.get_step();
|
||||
target_value = parent->state + (std::isnan(step) ? 1 : step);
|
||||
if (target_value > max_value) {
|
||||
if (this->cycle_ && !std::isnan(min_value)) {
|
||||
target_value = min_value;
|
||||
} else {
|
||||
target_value = max_value;
|
||||
}
|
||||
}
|
||||
} else if (this->operation_ == NUMBER_OP_DECREMENT) {
|
||||
ESP_LOGD(TAG, "'%s' - Decrement number, with%s cycling", name, this->cycle_ ? "" : "out");
|
||||
if (!parent->has_state()) {
|
||||
ESP_LOGW(TAG, "'%s' - Can't decrement number through NumberCall: no active state to modify", name);
|
||||
return;
|
||||
}
|
||||
auto step = traits.get_step();
|
||||
target_value = parent->state - (std::isnan(step) ? 1 : step);
|
||||
if (target_value < min_value) {
|
||||
if (this->cycle_ && !std::isnan(max_value)) {
|
||||
target_value = max_value;
|
||||
} else {
|
||||
target_value = min_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (target_value < min_value) {
|
||||
ESP_LOGW(TAG, "'%s' - Value %f must not be less than minimum %f", name, target_value, min_value);
|
||||
return;
|
||||
}
|
||||
if (target_value > max_value) {
|
||||
ESP_LOGW(TAG, "'%s' - Value %f must not be greater than maximum %f", name, target_value, max_value);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, " New number value: %f", target_value);
|
||||
this->parent_->control(target_value);
|
||||
}
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
43
esphome/components/number/number_call.h
Normal file
43
esphome/components/number/number_call.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "number_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
class Number;
|
||||
|
||||
enum NumberOperation {
|
||||
NUMBER_OP_NONE,
|
||||
NUMBER_OP_SET,
|
||||
NUMBER_OP_INCREMENT,
|
||||
NUMBER_OP_DECREMENT,
|
||||
NUMBER_OP_TO_MIN,
|
||||
NUMBER_OP_TO_MAX,
|
||||
};
|
||||
|
||||
class NumberCall {
|
||||
public:
|
||||
explicit NumberCall(Number *parent) : parent_(parent) {}
|
||||
void perform();
|
||||
|
||||
NumberCall &set_value(float value);
|
||||
NumberCall &number_increment(bool cycle);
|
||||
NumberCall &number_decrement(bool cycle);
|
||||
NumberCall &number_to_min();
|
||||
NumberCall &number_to_max();
|
||||
|
||||
NumberCall &with_operation(NumberOperation operation);
|
||||
NumberCall &with_value(float value);
|
||||
NumberCall &with_cycle(bool cycle);
|
||||
|
||||
protected:
|
||||
Number *const parent_;
|
||||
NumberOperation operation_{NUMBER_OP_NONE};
|
||||
optional<float> value_;
|
||||
bool cycle_;
|
||||
};
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
20
esphome/components/number/number_traits.cpp
Normal file
20
esphome/components/number/number_traits.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "number_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
static const char *const TAG = "number";
|
||||
|
||||
void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) {
|
||||
this->unit_of_measurement_ = unit_of_measurement;
|
||||
}
|
||||
|
||||
std::string NumberTraits::get_unit_of_measurement() {
|
||||
if (this->unit_of_measurement_.has_value())
|
||||
return *this->unit_of_measurement_;
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
44
esphome/components/number/number_traits.h
Normal file
44
esphome/components/number/number_traits.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
|
||||
enum NumberMode : uint8_t {
|
||||
NUMBER_MODE_AUTO = 0,
|
||||
NUMBER_MODE_BOX = 1,
|
||||
NUMBER_MODE_SLIDER = 2,
|
||||
};
|
||||
|
||||
class NumberTraits {
|
||||
public:
|
||||
// Set/get the number value boundaries.
|
||||
void set_min_value(float min_value) { min_value_ = min_value; }
|
||||
float get_min_value() const { return min_value_; }
|
||||
void set_max_value(float max_value) { max_value_ = max_value; }
|
||||
float get_max_value() const { return max_value_; }
|
||||
|
||||
// Set/get the step size for incrementing or decrementing the number value.
|
||||
void set_step(float step) { step_ = step; }
|
||||
float get_step() const { return step_; }
|
||||
|
||||
/// Manually set the unit of measurement.
|
||||
void set_unit_of_measurement(const std::string &unit_of_measurement);
|
||||
/// Get the unit of measurement, using the manual override if set.
|
||||
std::string get_unit_of_measurement();
|
||||
|
||||
// Set/get the frontend mode.
|
||||
void set_mode(NumberMode mode) { this->mode_ = mode; }
|
||||
NumberMode get_mode() const { return this->mode_; }
|
||||
|
||||
protected:
|
||||
float min_value_ = NAN;
|
||||
float max_value_ = NAN;
|
||||
float step_ = NAN;
|
||||
optional<std::string> unit_of_measurement_; ///< Unit of measurement override
|
||||
NumberMode mode_{NUMBER_MODE_AUTO};
|
||||
};
|
||||
|
||||
} // namespace number
|
||||
} // namespace esphome
|
||||
@@ -473,6 +473,8 @@ bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_
|
||||
App.reboot();
|
||||
});
|
||||
|
||||
// Delay here to allow power to stabilise before Wi-Fi/Ethernet is initialised.
|
||||
delay(300); // NOLINT
|
||||
App.setup();
|
||||
|
||||
ESP_LOGI(TAG, "Waiting for OTA attempt.");
|
||||
|
||||
@@ -49,6 +49,47 @@ void PMSX003Component::set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sens
|
||||
|
||||
void PMSX003Component::loop() {
|
||||
const uint32_t now = millis();
|
||||
|
||||
// If we update less often than it takes the device to stabilise, spin the fan down
|
||||
// rather than running it constantly. It does take some time to stabilise, so we
|
||||
// need to keep track of what state we're in.
|
||||
if (this->update_interval_ > PMS_STABILISING_MS) {
|
||||
if (this->initialised_ == 0) {
|
||||
this->send_command_(PMS_CMD_AUTO_MANUAL, 0);
|
||||
this->send_command_(PMS_CMD_ON_STANDBY, 1);
|
||||
this->initialised_ = 1;
|
||||
}
|
||||
switch (this->state_) {
|
||||
case PMSX003_STATE_IDLE:
|
||||
// Power on the sensor now so it'll be ready when we hit the update time
|
||||
if (now - this->last_update_ < (this->update_interval_ - PMS_STABILISING_MS))
|
||||
return;
|
||||
|
||||
this->state_ = PMSX003_STATE_STABILISING;
|
||||
this->send_command_(PMS_CMD_ON_STANDBY, 1);
|
||||
this->fan_on_time_ = now;
|
||||
return;
|
||||
case PMSX003_STATE_STABILISING:
|
||||
// wait for the sensor to be stable
|
||||
if (now - this->fan_on_time_ < PMS_STABILISING_MS)
|
||||
return;
|
||||
// consume any command responses that are in the serial buffer
|
||||
while (this->available())
|
||||
this->read_byte(&this->data_[0]);
|
||||
// Trigger a new read
|
||||
this->send_command_(PMS_CMD_TRIG_MANUAL, 0);
|
||||
this->state_ = PMSX003_STATE_WAITING;
|
||||
break;
|
||||
case PMSX003_STATE_WAITING:
|
||||
// Just go ahead and read stuff
|
||||
break;
|
||||
}
|
||||
} else if (now - this->last_update_ < this->update_interval_) {
|
||||
// Otherwise just leave the sensor powered up and come back when we hit the update
|
||||
// time
|
||||
return;
|
||||
}
|
||||
|
||||
if (now - this->last_transmission_ >= 500) {
|
||||
// last transmission too long ago. Reset RX index.
|
||||
this->data_index_ = 0;
|
||||
@@ -65,6 +106,7 @@ void PMSX003Component::loop() {
|
||||
// finished
|
||||
this->parse_data_();
|
||||
this->data_index_ = 0;
|
||||
this->last_update_ = now;
|
||||
} else if (!*check) {
|
||||
// wrong data
|
||||
this->data_index_ = 0;
|
||||
@@ -131,6 +173,25 @@ optional<bool> PMSX003Component::check_byte_() {
|
||||
return {};
|
||||
}
|
||||
|
||||
void PMSX003Component::send_command_(uint8_t cmd, uint16_t data) {
|
||||
this->data_index_ = 0;
|
||||
this->data_[data_index_++] = 0x42;
|
||||
this->data_[data_index_++] = 0x4D;
|
||||
this->data_[data_index_++] = cmd;
|
||||
this->data_[data_index_++] = (data >> 8) & 0xFF;
|
||||
this->data_[data_index_++] = (data >> 0) & 0xFF;
|
||||
int sum = 0;
|
||||
for (int i = 0; i < data_index_; i++) {
|
||||
sum += this->data_[i];
|
||||
}
|
||||
this->data_[data_index_++] = (sum >> 8) & 0xFF;
|
||||
this->data_[data_index_++] = (sum >> 0) & 0xFF;
|
||||
for (int i = 0; i < data_index_; i++) {
|
||||
this->write_byte(this->data_[i]);
|
||||
}
|
||||
this->data_index_ = 0;
|
||||
}
|
||||
|
||||
void PMSX003Component::parse_data_() {
|
||||
switch (this->type_) {
|
||||
case PMSX003_TYPE_5003ST: {
|
||||
@@ -218,6 +279,13 @@ void PMSX003Component::parse_data_() {
|
||||
}
|
||||
}
|
||||
|
||||
// Spin down the sensor again if we aren't going to need it until more time has
|
||||
// passed than it takes to stabilise
|
||||
if (this->update_interval_ > PMS_STABILISING_MS) {
|
||||
this->send_command_(PMS_CMD_ON_STANDBY, 0);
|
||||
this->state_ = PMSX003_STATE_IDLE;
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
}
|
||||
uint16_t PMSX003Component::get_16_bit_uint_(uint8_t start_index) {
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
namespace esphome {
|
||||
namespace pmsx003 {
|
||||
|
||||
// known command bytes
|
||||
#define PMS_CMD_AUTO_MANUAL 0xE1 // data=0: perform measurement manually, data=1: perform measurement automatically
|
||||
#define PMS_CMD_TRIG_MANUAL 0xE2 // trigger a manual measurement
|
||||
#define PMS_CMD_ON_STANDBY 0xE4 // data=0: go to standby mode, data=1: go to normal mode
|
||||
|
||||
static const uint16_t PMS_STABILISING_MS = 30000; // time taken for the sensor to become stable after power on
|
||||
|
||||
enum PMSX003Type {
|
||||
PMSX003_TYPE_X003 = 0,
|
||||
PMSX003_TYPE_5003T,
|
||||
@@ -14,6 +21,12 @@ enum PMSX003Type {
|
||||
PMSX003_TYPE_5003S,
|
||||
};
|
||||
|
||||
enum PMSX003State {
|
||||
PMSX003_STATE_IDLE = 0,
|
||||
PMSX003_STATE_STABILISING,
|
||||
PMSX003_STATE_WAITING,
|
||||
};
|
||||
|
||||
class PMSX003Component : public uart::UARTDevice, public Component {
|
||||
public:
|
||||
PMSX003Component() = default;
|
||||
@@ -23,6 +36,8 @@ class PMSX003Component : public uart::UARTDevice, public Component {
|
||||
|
||||
void set_type(PMSX003Type type) { type_ = type; }
|
||||
|
||||
void set_update_interval(uint32_t val) { update_interval_ = val; };
|
||||
|
||||
void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor);
|
||||
void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor);
|
||||
void set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor);
|
||||
@@ -45,11 +60,17 @@ class PMSX003Component : public uart::UARTDevice, public Component {
|
||||
protected:
|
||||
optional<bool> check_byte_();
|
||||
void parse_data_();
|
||||
void send_command_(uint8_t cmd, uint16_t data);
|
||||
uint16_t get_16_bit_uint_(uint8_t start_index);
|
||||
|
||||
uint8_t data_[64];
|
||||
uint8_t data_index_{0};
|
||||
uint8_t initialised_{0};
|
||||
uint32_t fan_on_time_{0};
|
||||
uint32_t last_update_{0};
|
||||
uint32_t last_transmission_{0};
|
||||
uint32_t update_interval_{0};
|
||||
PMSX003State state_{PMSX003_STATE_IDLE};
|
||||
PMSX003Type type_;
|
||||
|
||||
// "Standard Particle"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, uart
|
||||
|
||||
from esphome.const import (
|
||||
CONF_FORMALDEHYDE,
|
||||
CONF_HUMIDITY,
|
||||
@@ -17,6 +18,7 @@ from esphome.const import (
|
||||
CONF_PM_2_5UM,
|
||||
CONF_PM_5_0UM,
|
||||
CONF_PM_10_0UM,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TYPE,
|
||||
DEVICE_CLASS_PM1,
|
||||
@@ -44,6 +46,7 @@ TYPE_PMS5003ST = "PMS5003ST"
|
||||
TYPE_PMS5003S = "PMS5003S"
|
||||
|
||||
PMSX003Type = pmsx003_ns.enum("PMSX003Type")
|
||||
|
||||
PMSX003_TYPES = {
|
||||
TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003,
|
||||
TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T,
|
||||
@@ -68,6 +71,17 @@ def validate_pmsx003_sensors(value):
|
||||
return value
|
||||
|
||||
|
||||
def validate_update_interval(value):
|
||||
value = cv.positive_time_period_milliseconds(value)
|
||||
if value == cv.time_period("0s"):
|
||||
return value
|
||||
if value < cv.time_period("30s"):
|
||||
raise cv.Invalid(
|
||||
"Update interval must be greater than or equal to 30 seconds if set."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -157,6 +171,7 @@ CONFIG_SCHEMA = (
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_UPDATE_INTERVAL, default="0s"): validate_update_interval,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -164,6 +179,17 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s")
|
||||
schema = uart.final_validate_device_schema(
|
||||
"pmsx003", baud_rate=9600, require_rx=True, require_tx=require_tx
|
||||
)
|
||||
schema(config)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
@@ -230,3 +256,5 @@ async def to_code(config):
|
||||
if CONF_FORMALDEHYDE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE])
|
||||
cg.add(var.set_formaldehyde_sensor(sens))
|
||||
|
||||
cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
|
||||
|
||||
@@ -14,9 +14,6 @@ CONF_ON_FINISHED_WRITE = "on_finished_write"
|
||||
pn532_ns = cg.esphome_ns.namespace("pn532")
|
||||
PN532 = pn532_ns.class_("PN532", cg.PollingComponent)
|
||||
|
||||
PN532OnTagTrigger = pn532_ns.class_(
|
||||
"PN532OnTagTrigger", automation.Trigger.template(cg.std_string, nfc.NfcTag)
|
||||
)
|
||||
PN532OnFinishedWriteTrigger = pn532_ns.class_(
|
||||
"PN532OnFinishedWriteTrigger", automation.Trigger.template()
|
||||
)
|
||||
@@ -30,7 +27,7 @@ PN532_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(): cv.declare_id(PN532),
|
||||
cv.Optional(CONF_ON_TAG): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532OnTagTrigger),
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation(
|
||||
@@ -42,7 +39,7 @@ PN532_SCHEMA = cv.Schema(
|
||||
),
|
||||
cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532OnTagTrigger),
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger),
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
@@ -144,9 +144,9 @@ void PN532::loop() {
|
||||
}
|
||||
|
||||
if (nfcid.size() == this->current_uid_.size()) {
|
||||
bool same_uid = false;
|
||||
bool same_uid = true;
|
||||
for (size_t i = 0; i < nfcid.size(); i++)
|
||||
same_uid |= nfcid[i] == this->current_uid_[i];
|
||||
same_uid &= nfcid[i] == this->current_uid_[i];
|
||||
if (same_uid)
|
||||
return;
|
||||
}
|
||||
@@ -376,9 +376,6 @@ bool PN532BinarySensor::process(std::vector<uint8_t> &data) {
|
||||
this->found_ = true;
|
||||
return true;
|
||||
}
|
||||
void PN532OnTagTrigger::process(const std::unique_ptr<nfc::NfcTag> &tag) {
|
||||
this->trigger(nfc::format_uid(tag->get_uid()), *tag);
|
||||
}
|
||||
|
||||
} // namespace pn532
|
||||
} // namespace esphome
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/nfc/nfc_tag.h"
|
||||
#include "esphome/components/nfc/nfc.h"
|
||||
#include "esphome/components/nfc/automation.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace pn532 {
|
||||
@@ -16,7 +17,6 @@ static const uint8_t PN532_COMMAND_INDATAEXCHANGE = 0x40;
|
||||
static const uint8_t PN532_COMMAND_INLISTPASSIVETARGET = 0x4A;
|
||||
|
||||
class PN532BinarySensor;
|
||||
class PN532OnTagTrigger;
|
||||
|
||||
class PN532 : public PollingComponent {
|
||||
public:
|
||||
@@ -30,8 +30,8 @@ class PN532 : public PollingComponent {
|
||||
void loop() override;
|
||||
|
||||
void register_tag(PN532BinarySensor *tag) { this->binary_sensors_.push_back(tag); }
|
||||
void register_ontag_trigger(PN532OnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); }
|
||||
void register_ontagremoved_trigger(PN532OnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); }
|
||||
void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); }
|
||||
void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); }
|
||||
|
||||
void add_on_finished_write_callback(std::function<void()> callback) {
|
||||
this->on_finished_write_callback_.add(std::move(callback));
|
||||
@@ -79,8 +79,8 @@ class PN532 : public PollingComponent {
|
||||
|
||||
bool requested_read_{false};
|
||||
std::vector<PN532BinarySensor *> binary_sensors_;
|
||||
std::vector<PN532OnTagTrigger *> triggers_ontag_;
|
||||
std::vector<PN532OnTagTrigger *> triggers_ontagremoved_;
|
||||
std::vector<nfc::NfcOnTagTrigger *> triggers_ontag_;
|
||||
std::vector<nfc::NfcOnTagTrigger *> triggers_ontagremoved_;
|
||||
std::vector<uint8_t> current_uid_;
|
||||
nfc::NdefMessage *next_task_message_to_write_;
|
||||
enum NfcTask {
|
||||
@@ -115,11 +115,6 @@ class PN532BinarySensor : public binary_sensor::BinarySensor {
|
||||
bool found_{false};
|
||||
};
|
||||
|
||||
class PN532OnTagTrigger : public Trigger<std::string, nfc::NfcTag> {
|
||||
public:
|
||||
void process(const std::unique_ptr<nfc::NfcTag> &tag);
|
||||
};
|
||||
|
||||
class PN532OnFinishedWriteTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit PN532OnFinishedWriteTrigger(PN532 *parent) {
|
||||
|
||||
1
esphome/components/qmp6988/__init__.py
Normal file
1
esphome/components/qmp6988/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@andrewpc"]
|
||||
397
esphome/components/qmp6988/qmp6988.cpp
Normal file
397
esphome/components/qmp6988/qmp6988.cpp
Normal file
@@ -0,0 +1,397 @@
|
||||
#include "qmp6988.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome {
|
||||
namespace qmp6988 {
|
||||
|
||||
static const uint8_t QMP6988_CHIP_ID = 0x5C;
|
||||
|
||||
static const uint8_t QMP6988_CHIP_ID_REG = 0xD1; /* Chip ID confirmation Register */
|
||||
static const uint8_t QMP6988_RESET_REG = 0xE0; /* Device reset register */
|
||||
static const uint8_t QMP6988_DEVICE_STAT_REG = 0xF3; /* Device state register */
|
||||
static const uint8_t QMP6988_CTRLMEAS_REG = 0xF4; /* Measurement Condition Control Register */
|
||||
/* data */
|
||||
static const uint8_t QMP6988_PRESSURE_MSB_REG = 0xF7; /* Pressure MSB Register */
|
||||
static const uint8_t QMP6988_TEMPERATURE_MSB_REG = 0xFA; /* Temperature MSB Reg */
|
||||
|
||||
/* compensation calculation */
|
||||
static const uint8_t QMP6988_CALIBRATION_DATA_START = 0xA0; /* QMP6988 compensation coefficients */
|
||||
static const uint8_t QMP6988_CALIBRATION_DATA_LENGTH = 25;
|
||||
|
||||
static const uint8_t SHIFT_RIGHT_4_POSITION = 4;
|
||||
static const uint8_t SHIFT_LEFT_2_POSITION = 2;
|
||||
static const uint8_t SHIFT_LEFT_4_POSITION = 4;
|
||||
static const uint8_t SHIFT_LEFT_5_POSITION = 5;
|
||||
static const uint8_t SHIFT_LEFT_8_POSITION = 8;
|
||||
static const uint8_t SHIFT_LEFT_12_POSITION = 12;
|
||||
static const uint8_t SHIFT_LEFT_16_POSITION = 16;
|
||||
|
||||
/* power mode */
|
||||
static const uint8_t QMP6988_SLEEP_MODE = 0x00;
|
||||
static const uint8_t QMP6988_FORCED_MODE = 0x01;
|
||||
static const uint8_t QMP6988_NORMAL_MODE = 0x03;
|
||||
|
||||
static const uint8_t QMP6988_CTRLMEAS_REG_MODE_POS = 0;
|
||||
static const uint8_t QMP6988_CTRLMEAS_REG_MODE_MSK = 0x03;
|
||||
static const uint8_t QMP6988_CTRLMEAS_REG_MODE_LEN = 2;
|
||||
|
||||
static const uint8_t QMP6988_CTRLMEAS_REG_OSRST_POS = 5;
|
||||
static const uint8_t QMP6988_CTRLMEAS_REG_OSRST_MSK = 0xE0;
|
||||
static const uint8_t QMP6988_CTRLMEAS_REG_OSRST_LEN = 3;
|
||||
|
||||
static const uint8_t QMP6988_CTRLMEAS_REG_OSRSP_POS = 2;
|
||||
static const uint8_t QMP6988_CTRLMEAS_REG_OSRSP_MSK = 0x1C;
|
||||
static const uint8_t QMP6988_CTRLMEAS_REG_OSRSP_LEN = 3;
|
||||
|
||||
static const uint8_t QMP6988_CONFIG_REG = 0xF1; /*IIR filter co-efficient setting Register*/
|
||||
static const uint8_t QMP6988_CONFIG_REG_FILTER_POS = 0;
|
||||
static const uint8_t QMP6988_CONFIG_REG_FILTER_MSK = 0x07;
|
||||
static const uint8_t QMP6988_CONFIG_REG_FILTER_LEN = 3;
|
||||
|
||||
static const uint32_t SUBTRACTOR = 8388608;
|
||||
|
||||
static const char *const TAG = "qmp6988";
|
||||
|
||||
static const char *oversampling_to_str(QMP6988Oversampling oversampling) {
|
||||
switch (oversampling) {
|
||||
case QMP6988_OVERSAMPLING_SKIPPED:
|
||||
return "None";
|
||||
case QMP6988_OVERSAMPLING_1X:
|
||||
return "1x";
|
||||
case QMP6988_OVERSAMPLING_2X:
|
||||
return "2x";
|
||||
case QMP6988_OVERSAMPLING_4X:
|
||||
return "4x";
|
||||
case QMP6988_OVERSAMPLING_8X:
|
||||
return "8x";
|
||||
case QMP6988_OVERSAMPLING_16X:
|
||||
return "16x";
|
||||
case QMP6988_OVERSAMPLING_32X:
|
||||
return "32x";
|
||||
case QMP6988_OVERSAMPLING_64X:
|
||||
return "64x";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *iir_filter_to_str(QMP6988IIRFilter filter) {
|
||||
switch (filter) {
|
||||
case QMP6988_IIR_FILTER_OFF:
|
||||
return "OFF";
|
||||
case QMP6988_IIR_FILTER_2X:
|
||||
return "2x";
|
||||
case QMP6988_IIR_FILTER_4X:
|
||||
return "4x";
|
||||
case QMP6988_IIR_FILTER_8X:
|
||||
return "8x";
|
||||
case QMP6988_IIR_FILTER_16X:
|
||||
return "16x";
|
||||
case QMP6988_IIR_FILTER_32X:
|
||||
return "32x";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
bool QMP6988Component::device_check_() {
|
||||
uint8_t ret = 0;
|
||||
|
||||
ret = this->read_register(QMP6988_CHIP_ID_REG, &(qmp6988_data_.chip_id), 1);
|
||||
if (ret != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "%s: read chip ID (0xD1) failed", __func__);
|
||||
}
|
||||
ESP_LOGD(TAG, "qmp6988 read chip id = 0x%x", qmp6988_data_.chip_id);
|
||||
|
||||
return qmp6988_data_.chip_id == QMP6988_CHIP_ID;
|
||||
}
|
||||
|
||||
bool QMP6988Component::get_calibration_data_() {
|
||||
uint8_t status = 0;
|
||||
// BITFIELDS temp_COE;
|
||||
uint8_t a_data_uint8_tr[QMP6988_CALIBRATION_DATA_LENGTH] = {0};
|
||||
int len;
|
||||
|
||||
for (len = 0; len < QMP6988_CALIBRATION_DATA_LENGTH; len += 1) {
|
||||
status = this->read_register(QMP6988_CALIBRATION_DATA_START + len, &a_data_uint8_tr[len], 1);
|
||||
if (status != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "qmp6988 read calibration data (0xA0) error!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
qmp6988_data_.qmp6988_cali.COE_a0 =
|
||||
(QMP6988_S32_t)(((a_data_uint8_tr[18] << SHIFT_LEFT_12_POSITION) |
|
||||
(a_data_uint8_tr[19] << SHIFT_LEFT_4_POSITION) | (a_data_uint8_tr[24] & 0x0f))
|
||||
<< 12);
|
||||
qmp6988_data_.qmp6988_cali.COE_a0 = qmp6988_data_.qmp6988_cali.COE_a0 >> 12;
|
||||
|
||||
qmp6988_data_.qmp6988_cali.COE_a1 =
|
||||
(QMP6988_S16_t)(((a_data_uint8_tr[20]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[21]);
|
||||
qmp6988_data_.qmp6988_cali.COE_a2 =
|
||||
(QMP6988_S16_t)(((a_data_uint8_tr[22]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[23]);
|
||||
|
||||
qmp6988_data_.qmp6988_cali.COE_b00 =
|
||||
(QMP6988_S32_t)(((a_data_uint8_tr[0] << SHIFT_LEFT_12_POSITION) | (a_data_uint8_tr[1] << SHIFT_LEFT_4_POSITION) |
|
||||
((a_data_uint8_tr[24] & 0xf0) >> SHIFT_RIGHT_4_POSITION))
|
||||
<< 12);
|
||||
qmp6988_data_.qmp6988_cali.COE_b00 = qmp6988_data_.qmp6988_cali.COE_b00 >> 12;
|
||||
|
||||
qmp6988_data_.qmp6988_cali.COE_bt1 =
|
||||
(QMP6988_S16_t)(((a_data_uint8_tr[2]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[3]);
|
||||
qmp6988_data_.qmp6988_cali.COE_bt2 =
|
||||
(QMP6988_S16_t)(((a_data_uint8_tr[4]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[5]);
|
||||
qmp6988_data_.qmp6988_cali.COE_bp1 =
|
||||
(QMP6988_S16_t)(((a_data_uint8_tr[6]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[7]);
|
||||
qmp6988_data_.qmp6988_cali.COE_b11 =
|
||||
(QMP6988_S16_t)(((a_data_uint8_tr[8]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[9]);
|
||||
qmp6988_data_.qmp6988_cali.COE_bp2 =
|
||||
(QMP6988_S16_t)(((a_data_uint8_tr[10]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[11]);
|
||||
qmp6988_data_.qmp6988_cali.COE_b12 =
|
||||
(QMP6988_S16_t)(((a_data_uint8_tr[12]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[13]);
|
||||
qmp6988_data_.qmp6988_cali.COE_b21 =
|
||||
(QMP6988_S16_t)(((a_data_uint8_tr[14]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[15]);
|
||||
qmp6988_data_.qmp6988_cali.COE_bp3 =
|
||||
(QMP6988_S16_t)(((a_data_uint8_tr[16]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[17]);
|
||||
|
||||
ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n");
|
||||
ESP_LOGV(TAG, "COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_a0,
|
||||
qmp6988_data_.qmp6988_cali.COE_a1, qmp6988_data_.qmp6988_cali.COE_a2, qmp6988_data_.qmp6988_cali.COE_b00);
|
||||
ESP_LOGV(TAG, "COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bt1,
|
||||
qmp6988_data_.qmp6988_cali.COE_bt2, qmp6988_data_.qmp6988_cali.COE_bp1, qmp6988_data_.qmp6988_cali.COE_b11);
|
||||
ESP_LOGV(TAG, "COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bp2,
|
||||
qmp6988_data_.qmp6988_cali.COE_b12, qmp6988_data_.qmp6988_cali.COE_b21, qmp6988_data_.qmp6988_cali.COE_bp3);
|
||||
ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n");
|
||||
|
||||
qmp6988_data_.ik.a0 = qmp6988_data_.qmp6988_cali.COE_a0; // 20Q4
|
||||
qmp6988_data_.ik.b00 = qmp6988_data_.qmp6988_cali.COE_b00; // 20Q4
|
||||
|
||||
qmp6988_data_.ik.a1 = 3608L * (QMP6988_S32_t) qmp6988_data_.qmp6988_cali.COE_a1 - 1731677965L; // 31Q23
|
||||
qmp6988_data_.ik.a2 = 16889L * (QMP6988_S32_t) qmp6988_data_.qmp6988_cali.COE_a2 - 87619360L; // 30Q47
|
||||
|
||||
qmp6988_data_.ik.bt1 = 2982L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bt1 + 107370906L; // 28Q15
|
||||
qmp6988_data_.ik.bt2 = 329854L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bt2 + 108083093L; // 34Q38
|
||||
qmp6988_data_.ik.bp1 = 19923L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp1 + 1133836764L; // 31Q20
|
||||
qmp6988_data_.ik.b11 = 2406L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b11 + 118215883L; // 28Q34
|
||||
qmp6988_data_.ik.bp2 = 3079L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp2 - 181579595L; // 29Q43
|
||||
qmp6988_data_.ik.b12 = 6846L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b12 + 85590281L; // 29Q53
|
||||
qmp6988_data_.ik.b21 = 13836L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60
|
||||
qmp6988_data_.ik.bp3 = 2915L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65
|
||||
ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n");
|
||||
ESP_LOGV(TAG, "a0[%d] a1[%d] a2[%d] b00[%d]\r\n", qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2,
|
||||
qmp6988_data_.ik.b00);
|
||||
ESP_LOGV(TAG, "bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\r\n", qmp6988_data_.ik.bt1, qmp6988_data_.ik.bt2,
|
||||
qmp6988_data_.ik.bp1, qmp6988_data_.ik.b11);
|
||||
ESP_LOGV(TAG, "bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]\r\n", qmp6988_data_.ik.bp2, qmp6988_data_.ik.b12,
|
||||
qmp6988_data_.ik.b21, qmp6988_data_.ik.bp3);
|
||||
ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
QMP6988_S16_t QMP6988Component::get_compensated_temperature_(qmp6988_ik_data_t *ik, QMP6988_S32_t dt) {
|
||||
QMP6988_S16_t ret;
|
||||
QMP6988_S64_t wk1, wk2;
|
||||
|
||||
// wk1: 60Q4 // bit size
|
||||
wk1 = ((QMP6988_S64_t) ik->a1 * (QMP6988_S64_t) dt); // 31Q23+24-1=54 (54Q23)
|
||||
wk2 = ((QMP6988_S64_t) ik->a2 * (QMP6988_S64_t) dt) >> 14; // 30Q47+24-1=53 (39Q33)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dt) >> 10; // 39Q33+24-1=62 (52Q23)
|
||||
wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04)
|
||||
ret = (QMP6988_S16_t)((ik->a0 + wk2) >> 4); // 21Q4 -> 17Q0
|
||||
return ret;
|
||||
}
|
||||
|
||||
QMP6988_S32_t QMP6988Component::get_compensated_pressure_(qmp6988_ik_data_t *ik, QMP6988_S32_t dp, QMP6988_S16_t tx) {
|
||||
QMP6988_S32_t ret;
|
||||
QMP6988_S64_t wk1, wk2, wk3;
|
||||
|
||||
// wk1 = 48Q16 // bit size
|
||||
wk1 = ((QMP6988_S64_t) ik->bt1 * (QMP6988_S64_t) tx); // 28Q15+16-1=43 (43Q15)
|
||||
wk2 = ((QMP6988_S64_t) ik->bp1 * (QMP6988_S64_t) dp) >> 5; // 31Q20+24-1=54 (49Q15)
|
||||
wk1 += wk2; // 43,49->50Q15
|
||||
wk2 = ((QMP6988_S64_t) ik->bt2 * (QMP6988_S64_t) tx) >> 1; // 34Q38+16-1=49 (48Q37)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) tx) >> 8; // 48Q37+16-1=63 (55Q29)
|
||||
wk3 = wk2; // 55Q29
|
||||
wk2 = ((QMP6988_S64_t) ik->b11 * (QMP6988_S64_t) tx) >> 4; // 28Q34+16-1=43 (39Q30)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29)
|
||||
wk3 += wk2; // 55,61->62Q29
|
||||
wk2 = ((QMP6988_S64_t) ik->bp2 * (QMP6988_S64_t) dp) >> 13; // 29Q43+24-1=52 (39Q30)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29)
|
||||
wk3 += wk2; // 62,61->63Q29
|
||||
wk1 += wk3 >> 14; // Q29 >> 14 -> Q15
|
||||
wk2 = ((QMP6988_S64_t) ik->b12 * (QMP6988_S64_t) tx); // 29Q53+16-1=45 (45Q53)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) tx) >> 22; // 45Q53+16-1=61 (39Q31)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q31+24-1=62 (61Q30)
|
||||
wk3 = wk2; // 61Q30
|
||||
wk2 = ((QMP6988_S64_t) ik->b21 * (QMP6988_S64_t) tx) >> 6; // 29Q60+16-1=45 (39Q54)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 23; // 39Q54+24-1=62 (39Q31)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q31+24-1=62 (61Q20)
|
||||
wk3 += wk2; // 61,61->62Q30
|
||||
wk2 = ((QMP6988_S64_t) ik->bp3 * (QMP6988_S64_t) dp) >> 12; // 28Q65+24-1=51 (39Q53)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 23; // 39Q53+24-1=62 (39Q30)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp); // 39Q30+24-1=62 (62Q30)
|
||||
wk3 += wk2; // 62,62->63Q30
|
||||
wk1 += wk3 >> 15; // Q30 >> 15 = Q15
|
||||
wk1 /= 32767L;
|
||||
wk1 >>= 11; // Q15 >> 7 = Q4
|
||||
wk1 += ik->b00; // Q4 + 20Q4
|
||||
// wk1 >>= 4; // 28Q4 -> 24Q0
|
||||
ret = (QMP6988_S32_t) wk1;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void QMP6988Component::software_reset_() {
|
||||
uint8_t ret = 0;
|
||||
|
||||
ret = this->write_byte(QMP6988_RESET_REG, 0xe6);
|
||||
if (ret != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Software Reset (0xe6) failed");
|
||||
}
|
||||
delay(10);
|
||||
|
||||
this->write_byte(QMP6988_RESET_REG, 0x00);
|
||||
}
|
||||
|
||||
void QMP6988Component::set_power_mode_(uint8_t power_mode) {
|
||||
uint8_t data;
|
||||
|
||||
ESP_LOGD(TAG, "Setting Power mode to: %d", power_mode);
|
||||
|
||||
qmp6988_data_.power_mode = power_mode;
|
||||
this->read_register(QMP6988_CTRLMEAS_REG, &data, 1);
|
||||
data = data & 0xfc;
|
||||
if (power_mode == QMP6988_SLEEP_MODE) {
|
||||
data |= 0x00;
|
||||
} else if (power_mode == QMP6988_FORCED_MODE) {
|
||||
data |= 0x01;
|
||||
} else if (power_mode == QMP6988_NORMAL_MODE) {
|
||||
data |= 0x03;
|
||||
}
|
||||
this->write_byte(QMP6988_CTRLMEAS_REG, data);
|
||||
|
||||
ESP_LOGD(TAG, "Set Power mode 0xf4=0x%x \r\n", data);
|
||||
|
||||
delay(10);
|
||||
}
|
||||
|
||||
void QMP6988Component::write_filter_(unsigned char filter) {
|
||||
uint8_t data;
|
||||
|
||||
data = (filter & 0x03);
|
||||
this->write_byte(QMP6988_CONFIG_REG, data);
|
||||
delay(10);
|
||||
}
|
||||
|
||||
void QMP6988Component::write_oversampling_pressure_(unsigned char oversampling_p) {
|
||||
uint8_t data;
|
||||
|
||||
this->read_register(QMP6988_CTRLMEAS_REG, &data, 1);
|
||||
data &= 0xe3;
|
||||
data |= (oversampling_p << 2);
|
||||
this->write_byte(QMP6988_CTRLMEAS_REG, data);
|
||||
delay(10);
|
||||
}
|
||||
|
||||
void QMP6988Component::write_oversampling_temperature_(unsigned char oversampling_t) {
|
||||
uint8_t data;
|
||||
|
||||
this->read_register(QMP6988_CTRLMEAS_REG, &data, 1);
|
||||
data &= 0x1f;
|
||||
data |= (oversampling_t << 5);
|
||||
this->write_byte(QMP6988_CTRLMEAS_REG, data);
|
||||
delay(10);
|
||||
}
|
||||
|
||||
void QMP6988Component::set_temperature_oversampling(QMP6988Oversampling oversampling_t) {
|
||||
this->temperature_oversampling_ = oversampling_t;
|
||||
}
|
||||
|
||||
void QMP6988Component::set_pressure_oversampling(QMP6988Oversampling oversampling_p) {
|
||||
this->pressure_oversampling_ = oversampling_p;
|
||||
}
|
||||
|
||||
void QMP6988Component::set_iir_filter(QMP6988IIRFilter iirfilter) { this->iir_filter_ = iirfilter; }
|
||||
|
||||
void QMP6988Component::calculate_altitude_(float pressure, float temp) {
|
||||
float altitude;
|
||||
altitude = (pow((101325 / pressure), 1 / 5.257) - 1) * (temp + 273.15) / 0.0065;
|
||||
this->qmp6988_data_.altitude = altitude;
|
||||
}
|
||||
|
||||
void QMP6988Component::calculate_pressure_() {
|
||||
uint8_t err = 0;
|
||||
QMP6988_U32_t p_read, t_read;
|
||||
QMP6988_S32_t p_raw, t_raw;
|
||||
uint8_t a_data_uint8_tr[6] = {0};
|
||||
QMP6988_S32_t t_int, p_int;
|
||||
this->qmp6988_data_.temperature = 0;
|
||||
this->qmp6988_data_.pressure = 0;
|
||||
|
||||
err = this->read_register(QMP6988_PRESSURE_MSB_REG, a_data_uint8_tr, 6);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Error reading raw pressure/temp values");
|
||||
return;
|
||||
}
|
||||
p_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[0])) << SHIFT_LEFT_16_POSITION) |
|
||||
(((QMP6988_U16_t)(a_data_uint8_tr[1])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[2]));
|
||||
p_raw = (QMP6988_S32_t)(p_read - SUBTRACTOR);
|
||||
|
||||
t_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[3])) << SHIFT_LEFT_16_POSITION) |
|
||||
(((QMP6988_U16_t)(a_data_uint8_tr[4])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[5]));
|
||||
t_raw = (QMP6988_S32_t)(t_read - SUBTRACTOR);
|
||||
|
||||
t_int = this->get_compensated_temperature_(&(qmp6988_data_.ik), t_raw);
|
||||
p_int = this->get_compensated_pressure_(&(qmp6988_data_.ik), p_raw, t_int);
|
||||
|
||||
this->qmp6988_data_.temperature = (float) t_int / 256.0f;
|
||||
this->qmp6988_data_.pressure = (float) p_int / 16.0f;
|
||||
}
|
||||
|
||||
void QMP6988Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up QMP6988");
|
||||
|
||||
bool ret;
|
||||
ret = this->device_check_();
|
||||
if (!ret) {
|
||||
ESP_LOGCONFIG(TAG, "Setup failed - device not found");
|
||||
}
|
||||
|
||||
this->software_reset_();
|
||||
this->get_calibration_data_();
|
||||
this->set_power_mode_(QMP6988_NORMAL_MODE);
|
||||
this->write_filter_(iir_filter_);
|
||||
this->write_oversampling_pressure_(this->pressure_oversampling_);
|
||||
this->write_oversampling_temperature_(this->temperature_oversampling_);
|
||||
}
|
||||
|
||||
void QMP6988Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "QMP6988:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication with QMP6988 failed!");
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Temperature Oversampling: %s", oversampling_to_str(this->temperature_oversampling_));
|
||||
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Pressure Oversampling: %s", oversampling_to_str(this->pressure_oversampling_));
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_));
|
||||
}
|
||||
|
||||
float QMP6988Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void QMP6988Component::update() {
|
||||
this->calculate_pressure_();
|
||||
float pressurehectopascals = this->qmp6988_data_.pressure / 100;
|
||||
float temperature = this->qmp6988_data_.temperature;
|
||||
|
||||
ESP_LOGD(TAG, "Temperature=%.2f°C, Pressure=%.2fhPa", temperature, pressurehectopascals);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
if (this->pressure_sensor_ != nullptr)
|
||||
this->pressure_sensor_->publish_state(pressurehectopascals);
|
||||
}
|
||||
|
||||
} // namespace qmp6988
|
||||
} // namespace esphome
|
||||
116
esphome/components/qmp6988/qmp6988.h
Normal file
116
esphome/components/qmp6988/qmp6988.h
Normal file
@@ -0,0 +1,116 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace qmp6988 {
|
||||
|
||||
#define QMP6988_U16_t unsigned short
|
||||
#define QMP6988_S16_t short
|
||||
#define QMP6988_U32_t unsigned int
|
||||
#define QMP6988_S32_t int
|
||||
#define QMP6988_U64_t unsigned long long
|
||||
#define QMP6988_S64_t long long
|
||||
|
||||
/* oversampling */
|
||||
enum QMP6988Oversampling {
|
||||
QMP6988_OVERSAMPLING_SKIPPED = 0x00,
|
||||
QMP6988_OVERSAMPLING_1X = 0x01,
|
||||
QMP6988_OVERSAMPLING_2X = 0x02,
|
||||
QMP6988_OVERSAMPLING_4X = 0x03,
|
||||
QMP6988_OVERSAMPLING_8X = 0x04,
|
||||
QMP6988_OVERSAMPLING_16X = 0x05,
|
||||
QMP6988_OVERSAMPLING_32X = 0x06,
|
||||
QMP6988_OVERSAMPLING_64X = 0x07,
|
||||
};
|
||||
|
||||
/* filter */
|
||||
enum QMP6988IIRFilter {
|
||||
QMP6988_IIR_FILTER_OFF = 0x00,
|
||||
QMP6988_IIR_FILTER_2X = 0x01,
|
||||
QMP6988_IIR_FILTER_4X = 0x02,
|
||||
QMP6988_IIR_FILTER_8X = 0x03,
|
||||
QMP6988_IIR_FILTER_16X = 0x04,
|
||||
QMP6988_IIR_FILTER_32X = 0x05,
|
||||
};
|
||||
|
||||
using qmp6988_cali_data_t = struct Qmp6988CaliData {
|
||||
QMP6988_S32_t COE_a0;
|
||||
QMP6988_S16_t COE_a1;
|
||||
QMP6988_S16_t COE_a2;
|
||||
QMP6988_S32_t COE_b00;
|
||||
QMP6988_S16_t COE_bt1;
|
||||
QMP6988_S16_t COE_bt2;
|
||||
QMP6988_S16_t COE_bp1;
|
||||
QMP6988_S16_t COE_b11;
|
||||
QMP6988_S16_t COE_bp2;
|
||||
QMP6988_S16_t COE_b12;
|
||||
QMP6988_S16_t COE_b21;
|
||||
QMP6988_S16_t COE_bp3;
|
||||
};
|
||||
|
||||
using qmp6988_fk_data_t = struct Qmp6988FkData {
|
||||
float a0, b00;
|
||||
float a1, a2, bt1, bt2, bp1, b11, bp2, b12, b21, bp3;
|
||||
};
|
||||
|
||||
using qmp6988_ik_data_t = struct Qmp6988IkData {
|
||||
QMP6988_S32_t a0, b00;
|
||||
QMP6988_S32_t a1, a2;
|
||||
QMP6988_S64_t bt1, bt2, bp1, b11, bp2, b12, b21, bp3;
|
||||
};
|
||||
|
||||
using qmp6988_data_t = struct Qmp6988Data {
|
||||
uint8_t chip_id;
|
||||
uint8_t power_mode;
|
||||
float temperature;
|
||||
float pressure;
|
||||
float altitude;
|
||||
qmp6988_cali_data_t qmp6988_cali;
|
||||
qmp6988_ik_data_t ik;
|
||||
};
|
||||
|
||||
class QMP6988Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
|
||||
void set_iir_filter(QMP6988IIRFilter iirfilter);
|
||||
void set_temperature_oversampling(QMP6988Oversampling oversampling_t);
|
||||
void set_pressure_oversampling(QMP6988Oversampling oversampling_p);
|
||||
|
||||
protected:
|
||||
qmp6988_data_t qmp6988_data_;
|
||||
sensor::Sensor *temperature_sensor_;
|
||||
sensor::Sensor *pressure_sensor_;
|
||||
|
||||
QMP6988Oversampling temperature_oversampling_{QMP6988_OVERSAMPLING_16X};
|
||||
QMP6988Oversampling pressure_oversampling_{QMP6988_OVERSAMPLING_16X};
|
||||
QMP6988IIRFilter iir_filter_{QMP6988_IIR_FILTER_OFF};
|
||||
|
||||
void software_reset_();
|
||||
bool get_calibration_data_();
|
||||
bool device_check_();
|
||||
void set_power_mode_(uint8_t power_mode);
|
||||
void write_oversampling_temperature_(unsigned char oversampling_t);
|
||||
void write_oversampling_pressure_(unsigned char oversampling_p);
|
||||
void write_filter_(unsigned char filter);
|
||||
void calculate_pressure_();
|
||||
void calculate_altitude_(float pressure, float temp);
|
||||
|
||||
QMP6988_S32_t get_compensated_pressure_(qmp6988_ik_data_t *ik, QMP6988_S32_t dp, QMP6988_S16_t tx);
|
||||
QMP6988_S16_t get_compensated_temperature_(qmp6988_ik_data_t *ik, QMP6988_S32_t dt);
|
||||
};
|
||||
|
||||
} // namespace qmp6988
|
||||
} // namespace esphome
|
||||
101
esphome/components/qmp6988/sensor.py
Normal file
101
esphome/components/qmp6988/sensor.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_HECTOPASCAL,
|
||||
CONF_IIR_FILTER,
|
||||
CONF_OVERSAMPLING,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
qmp6988_ns = cg.esphome_ns.namespace("qmp6988")
|
||||
QMP6988Component = qmp6988_ns.class_(
|
||||
"QMP6988Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
QMP6988Oversampling = qmp6988_ns.enum("QMP6988Oversampling")
|
||||
OVERSAMPLING_OPTIONS = {
|
||||
"NONE": QMP6988Oversampling.QMP6988_OVERSAMPLING_SKIPPED,
|
||||
"1X": QMP6988Oversampling.QMP6988_OVERSAMPLING_1X,
|
||||
"2X": QMP6988Oversampling.QMP6988_OVERSAMPLING_2X,
|
||||
"4X": QMP6988Oversampling.QMP6988_OVERSAMPLING_4X,
|
||||
"8X": QMP6988Oversampling.QMP6988_OVERSAMPLING_8X,
|
||||
"16X": QMP6988Oversampling.QMP6988_OVERSAMPLING_16X,
|
||||
"32X": QMP6988Oversampling.QMP6988_OVERSAMPLING_32X,
|
||||
"64X": QMP6988Oversampling.QMP6988_OVERSAMPLING_64X,
|
||||
}
|
||||
|
||||
QMP6988IIRFilter = qmp6988_ns.enum("QMP6988IIRFilter")
|
||||
IIR_FILTER_OPTIONS = {
|
||||
"OFF": QMP6988IIRFilter.QMP6988_IIR_FILTER_OFF,
|
||||
"2X": QMP6988IIRFilter.QMP6988_IIR_FILTER_2X,
|
||||
"4X": QMP6988IIRFilter.QMP6988_IIR_FILTER_4X,
|
||||
"8X": QMP6988IIRFilter.QMP6988_IIR_FILTER_8X,
|
||||
"16X": QMP6988IIRFilter.QMP6988_IIR_FILTER_16X,
|
||||
"32X": QMP6988IIRFilter.QMP6988_IIR_FILTER_32X,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(QMP6988Component),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_OVERSAMPLING, default="8X"): cv.enum(
|
||||
OVERSAMPLING_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HECTOPASCAL,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_OVERSAMPLING, default="8X"): cv.enum(
|
||||
OVERSAMPLING_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
|
||||
IIR_FILTER_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x70))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if CONF_TEMPERATURE in config:
|
||||
conf = config[CONF_TEMPERATURE]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
cg.add(var.set_temperature_oversampling(conf[CONF_OVERSAMPLING]))
|
||||
|
||||
if CONF_PRESSURE in config:
|
||||
conf = config[CONF_PRESSURE]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_pressure_sensor(sens))
|
||||
cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING]))
|
||||
|
||||
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))
|
||||
@@ -1,3 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/rc522/rc522.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
|
||||
namespace esphome {
|
||||
/**
|
||||
* Library based on https://github.com/miguelbalboa/rfid
|
||||
* and adapted to ESPHome by @glmnet
|
||||
@@ -6,14 +13,6 @@
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/rc522/rc522.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace rc522_spi {
|
||||
|
||||
class RC522Spi : public rc522::RC522,
|
||||
|
||||
@@ -40,6 +40,24 @@ namespace remote_base {
|
||||
|
||||
static const char *const TAG = "remote.pronto";
|
||||
|
||||
bool ProntoData::operator==(const ProntoData &rhs) const {
|
||||
std::vector<uint16_t> data1 = encode_pronto(data);
|
||||
std::vector<uint16_t> data2 = encode_pronto(rhs.data);
|
||||
|
||||
uint32_t total_diff = 0;
|
||||
// Don't need to check the last one, it's the large gap at the end.
|
||||
for (std::vector<uint16_t>::size_type i = 0; i < data1.size() - 1; ++i) {
|
||||
int diff = data2[i] - data1[i];
|
||||
diff *= diff;
|
||||
if (diff > 9)
|
||||
return false;
|
||||
|
||||
total_diff += diff;
|
||||
}
|
||||
|
||||
return total_diff <= data1.size() * 3;
|
||||
}
|
||||
|
||||
// DO NOT EXPORT from this file
|
||||
static const uint16_t MICROSECONDS_T_MAX = 0xFFFFU;
|
||||
static const uint16_t LEARNED_TOKEN = 0x0000U;
|
||||
@@ -52,6 +70,7 @@ static const uint32_t REFERENCE_FREQUENCY = 4145146UL;
|
||||
static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0;
|
||||
static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL;
|
||||
static const uint16_t PRONTO_DEFAULT_GAP = 45000;
|
||||
static const uint16_t MARK_EXCESS_MICROS = 20;
|
||||
|
||||
static uint16_t to_frequency_k_hz(uint16_t code) {
|
||||
if (code == 0)
|
||||
@@ -107,7 +126,7 @@ void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::vector<uin
|
||||
}
|
||||
}
|
||||
|
||||
void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::string &str) {
|
||||
std::vector<uint16_t> encode_pronto(const std::string &str) {
|
||||
size_t len = str.length() / (DIGITS_IN_PRONTO_NUMBER + 1) + 1;
|
||||
std::vector<uint16_t> data;
|
||||
const char *p = str.c_str();
|
||||
@@ -122,12 +141,90 @@ void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::string &st
|
||||
data.push_back(x); // If input is conforming, there can be no overflow!
|
||||
p = *endptr;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::string &str) {
|
||||
std::vector<uint16_t> data = encode_pronto(str);
|
||||
send_pronto_(dst, data);
|
||||
}
|
||||
|
||||
void ProntoProtocol::encode(RemoteTransmitData *dst, const ProntoData &data) { send_pronto_(dst, data.data); }
|
||||
|
||||
optional<ProntoData> ProntoProtocol::decode(RemoteReceiveData src) { return {}; }
|
||||
uint16_t ProntoProtocol::effective_frequency_(uint16_t frequency) {
|
||||
return frequency > 0 ? frequency : FALLBACK_FREQUENCY;
|
||||
}
|
||||
|
||||
uint16_t ProntoProtocol::to_timebase_(uint16_t frequency) {
|
||||
return MICROSECONDS_IN_SECONDS / effective_frequency_(frequency);
|
||||
}
|
||||
|
||||
uint16_t ProntoProtocol::to_frequency_code_(uint16_t frequency) {
|
||||
return REFERENCE_FREQUENCY / effective_frequency_(frequency);
|
||||
}
|
||||
|
||||
std::string ProntoProtocol::dump_digit_(uint8_t x) {
|
||||
return std::string(1, (char) (x <= 9 ? ('0' + x) : ('A' + (x - 10))));
|
||||
}
|
||||
|
||||
std::string ProntoProtocol::dump_number_(uint16_t number, bool end /* = false */) {
|
||||
std::string num;
|
||||
|
||||
for (uint8_t i = 0; i < DIGITS_IN_PRONTO_NUMBER; ++i) {
|
||||
uint8_t shifts = BITS_IN_HEXADECIMAL * (DIGITS_IN_PRONTO_NUMBER - 1 - i);
|
||||
num += dump_digit_((number >> shifts) & HEX_MASK);
|
||||
}
|
||||
|
||||
if (!end)
|
||||
num += ' ';
|
||||
|
||||
return num;
|
||||
}
|
||||
|
||||
std::string ProntoProtocol::dump_duration_(uint32_t duration, uint16_t timebase, bool end /* = false */) {
|
||||
return dump_number_((duration + timebase / 2) / timebase, end);
|
||||
}
|
||||
|
||||
std::string ProntoProtocol::compensate_and_dump_sequence_(std::vector<int32_t> *data, uint16_t timebase) {
|
||||
std::string out;
|
||||
|
||||
for (std::vector<int32_t>::size_type i = 0; i < data->size() - 1; i++) {
|
||||
int32_t t_length = data->at(i);
|
||||
uint32_t t_duration;
|
||||
if (t_length > 0) {
|
||||
// Mark
|
||||
t_duration = t_length - MARK_EXCESS_MICROS;
|
||||
} else {
|
||||
t_duration = -t_length + MARK_EXCESS_MICROS;
|
||||
}
|
||||
out += dump_duration_(t_duration, timebase);
|
||||
}
|
||||
|
||||
// append minimum gap
|
||||
out += dump_duration_(PRONTO_DEFAULT_GAP, timebase, true);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
optional<ProntoData> ProntoProtocol::decode(RemoteReceiveData src) {
|
||||
ProntoData out;
|
||||
|
||||
uint16_t frequency = 38000U;
|
||||
std::vector<int32_t> *data = src.get_raw_data();
|
||||
std::string prontodata;
|
||||
|
||||
prontodata += dump_number_(frequency > 0 ? LEARNED_TOKEN : LEARNED_NON_MODULATED_TOKEN);
|
||||
prontodata += dump_number_(to_frequency_code_(frequency));
|
||||
prontodata += dump_number_((data->size() + 1) / 2);
|
||||
prontodata += dump_number_(0);
|
||||
uint16_t timebase = to_timebase_(frequency);
|
||||
prontodata += compensate_and_dump_sequence_(data, timebase);
|
||||
|
||||
out.data = prontodata;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void ProntoProtocol::dump(const ProntoData &data) { ESP_LOGD(TAG, "Received Pronto: data=%s", data.data.c_str()); }
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
namespace esphome {
|
||||
namespace remote_base {
|
||||
|
||||
std::vector<uint16_t> encode_pronto(const std::string &str);
|
||||
|
||||
struct ProntoData {
|
||||
std::string data;
|
||||
|
||||
bool operator==(const ProntoData &rhs) const { return data == rhs.data; }
|
||||
bool operator==(const ProntoData &rhs) const;
|
||||
};
|
||||
|
||||
class ProntoProtocol : public RemoteProtocol<ProntoData> {
|
||||
@@ -17,6 +19,14 @@ class ProntoProtocol : public RemoteProtocol<ProntoData> {
|
||||
void send_pronto_(RemoteTransmitData *dst, const std::vector<uint16_t> &data);
|
||||
void send_pronto_(RemoteTransmitData *dst, const std::string &str);
|
||||
|
||||
uint16_t effective_frequency_(uint16_t frequency);
|
||||
uint16_t to_timebase_(uint16_t frequency);
|
||||
uint16_t to_frequency_code_(uint16_t frequency);
|
||||
std::string dump_digit_(uint8_t x);
|
||||
std::string dump_number_(uint16_t number, bool end = false);
|
||||
std::string dump_duration_(uint32_t duration, uint16_t timebase, bool end = false);
|
||||
std::string compensate_and_dump_sequence_(std::vector<int32_t> *data, uint16_t timebase);
|
||||
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const ProntoData &data) override;
|
||||
optional<ProntoData> decode(RemoteReceiveData src) override;
|
||||
|
||||
@@ -33,14 +33,8 @@ void SCD30Component::setup() {
|
||||
#endif
|
||||
|
||||
/// Firmware version identification
|
||||
if (!this->write_command_(SCD30_CMD_GET_FIRMWARE_VERSION)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint16_t raw_firmware_version[3];
|
||||
|
||||
if (!this->read_data_(raw_firmware_version, 3)) {
|
||||
if (!this->get_register(SCD30_CMD_GET_FIRMWARE_VERSION, raw_firmware_version, 3)) {
|
||||
this->error_code_ = FIRMWARE_IDENTIFICATION_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
@@ -49,7 +43,7 @@ void SCD30Component::setup() {
|
||||
uint16_t(raw_firmware_version[0] & 0xFF));
|
||||
|
||||
if (this->temperature_offset_ != 0) {
|
||||
if (!this->write_command_(SCD30_CMD_TEMPERATURE_OFFSET, (uint16_t)(temperature_offset_ * 100.0))) {
|
||||
if (!this->write_command(SCD30_CMD_TEMPERATURE_OFFSET, (uint16_t)(temperature_offset_ * 100.0))) {
|
||||
ESP_LOGE(TAG, "Sensor SCD30 error setting temperature offset.");
|
||||
this->error_code_ = MEASUREMENT_INIT_FAILED;
|
||||
this->mark_failed();
|
||||
@@ -69,7 +63,7 @@ void SCD30Component::setup() {
|
||||
delay(30);
|
||||
#endif
|
||||
|
||||
if (!this->write_command_(SCD30_CMD_MEASUREMENT_INTERVAL, update_interval_)) {
|
||||
if (!this->write_command(SCD30_CMD_MEASUREMENT_INTERVAL, update_interval_)) {
|
||||
ESP_LOGE(TAG, "Sensor SCD30 error setting update interval.");
|
||||
this->error_code_ = MEASUREMENT_INIT_FAILED;
|
||||
this->mark_failed();
|
||||
@@ -81,7 +75,7 @@ void SCD30Component::setup() {
|
||||
|
||||
// The start measurement command disables the altitude compensation, if any, so we only set it if it's turned on
|
||||
if (this->altitude_compensation_ != 0xFFFF) {
|
||||
if (!this->write_command_(SCD30_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) {
|
||||
if (!this->write_command(SCD30_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) {
|
||||
ESP_LOGE(TAG, "Sensor SCD30 error setting altitude compensation.");
|
||||
this->error_code_ = MEASUREMENT_INIT_FAILED;
|
||||
this->mark_failed();
|
||||
@@ -92,7 +86,7 @@ void SCD30Component::setup() {
|
||||
delay(30);
|
||||
#endif
|
||||
|
||||
if (!this->write_command_(SCD30_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) {
|
||||
if (!this->write_command(SCD30_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) {
|
||||
ESP_LOGE(TAG, "Sensor SCD30 error setting automatic self calibration.");
|
||||
this->error_code_ = MEASUREMENT_INIT_FAILED;
|
||||
this->mark_failed();
|
||||
@@ -103,7 +97,7 @@ void SCD30Component::setup() {
|
||||
#endif
|
||||
|
||||
/// Sensor initialization
|
||||
if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, this->ambient_pressure_compensation_)) {
|
||||
if (!this->write_command(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, this->ambient_pressure_compensation_)) {
|
||||
ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements.");
|
||||
this->error_code_ = MEASUREMENT_INIT_FAILED;
|
||||
this->mark_failed();
|
||||
@@ -151,14 +145,14 @@ void SCD30Component::dump_config() {
|
||||
}
|
||||
|
||||
void SCD30Component::update() {
|
||||
uint16_t raw_read_status[1];
|
||||
if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) {
|
||||
uint16_t raw_read_status;
|
||||
if (!this->read_data(raw_read_status) || raw_read_status == 0x00) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, "Data not ready yet!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->write_command_(SCD30_CMD_READ_MEASUREMENT)) {
|
||||
if (!this->write_command(SCD30_CMD_READ_MEASUREMENT)) {
|
||||
ESP_LOGW(TAG, "Error reading measurement!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
@@ -166,7 +160,7 @@ void SCD30Component::update() {
|
||||
|
||||
this->set_timeout(50, [this]() {
|
||||
uint16_t raw_data[6];
|
||||
if (!this->read_data_(raw_data, 6)) {
|
||||
if (!this->read_data(raw_data, 6)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
@@ -197,77 +191,16 @@ void SCD30Component::update() {
|
||||
}
|
||||
|
||||
bool SCD30Component::is_data_ready_() {
|
||||
if (!this->write_command_(SCD30_CMD_GET_DATA_READY_STATUS)) {
|
||||
if (!this->write_command(SCD30_CMD_GET_DATA_READY_STATUS)) {
|
||||
return false;
|
||||
}
|
||||
delay(4);
|
||||
uint16_t is_data_ready;
|
||||
if (!this->read_data_(&is_data_ready, 1)) {
|
||||
if (!this->read_data(&is_data_ready, 1)) {
|
||||
return false;
|
||||
}
|
||||
return is_data_ready == 1;
|
||||
}
|
||||
|
||||
bool SCD30Component::write_command_(uint16_t command) {
|
||||
// Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
|
||||
return this->write_byte(command >> 8, command & 0xFF);
|
||||
}
|
||||
|
||||
bool SCD30Component::write_command_(uint16_t command, uint16_t data) {
|
||||
uint8_t raw[5];
|
||||
raw[0] = command >> 8;
|
||||
raw[1] = command & 0xFF;
|
||||
raw[2] = data >> 8;
|
||||
raw[3] = data & 0xFF;
|
||||
raw[4] = sht_crc_(raw[2], raw[3]);
|
||||
return this->write(raw, 5) == i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
uint8_t SCD30Component::sht_crc_(uint8_t data1, uint8_t data2) {
|
||||
uint8_t bit;
|
||||
uint8_t crc = 0xFF;
|
||||
|
||||
crc ^= data1;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
crc ^= data2;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool SCD30Component::read_data_(uint16_t *data, uint8_t len) {
|
||||
const uint8_t num_bytes = len * 3;
|
||||
std::vector<uint8_t> buf(num_bytes);
|
||||
|
||||
if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
const uint8_t j = 3 * i;
|
||||
uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
|
||||
if (crc != buf[j + 2]) {
|
||||
ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
|
||||
return false;
|
||||
}
|
||||
data[i] = (buf[j] << 8) | buf[j + 1];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace scd30
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace scd30 {
|
||||
|
||||
/// This class implements support for the Sensirion scd30 i2c GAS (VOC and CO2eq) sensors.
|
||||
class SCD30Component : public Component, public i2c::I2CDevice {
|
||||
class SCD30Component : public Component, public sensirion_common::SensirionI2CDevice {
|
||||
public:
|
||||
void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
|
||||
@@ -27,10 +27,6 @@ class SCD30Component : public Component, public i2c::I2CDevice {
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
protected:
|
||||
bool write_command_(uint16_t command);
|
||||
bool write_command_(uint16_t command, uint16_t data);
|
||||
bool read_data_(uint16_t *data, uint8_t len);
|
||||
uint8_t sht_crc_(uint8_t data1, uint8_t data2);
|
||||
bool is_data_ready_();
|
||||
|
||||
enum ErrorCode {
|
||||
|
||||
@@ -2,6 +2,7 @@ from esphome import core
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components import sensirion_common
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_HUMIDITY,
|
||||
@@ -18,9 +19,12 @@ from esphome.const import (
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
scd30_ns = cg.esphome_ns.namespace("scd30")
|
||||
SCD30Component = scd30_ns.class_("SCD30Component", cg.Component, i2c.I2CDevice)
|
||||
SCD30Component = scd30_ns.class_(
|
||||
"SCD30Component", cg.Component, sensirion_common.SensirionI2CDevice
|
||||
)
|
||||
|
||||
CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration"
|
||||
CONF_ALTITUDE_COMPENSATION = "altitude_compensation"
|
||||
|
||||
28
esphome/components/scd4x/automation.h
Normal file
28
esphome/components/scd4x/automation.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "scd4x.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace scd4x {
|
||||
|
||||
template<typename... Ts> class PerformForcedCalibrationAction : public Action<Ts...>, public Parented<SCD4XComponent> {
|
||||
public:
|
||||
void play(Ts... x) override {
|
||||
if (this->value_.has_value()) {
|
||||
this->parent_->perform_forced_calibration(value_.value());
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
TEMPLATABLE_VALUE(uint16_t, value)
|
||||
};
|
||||
|
||||
template<typename... Ts> class FactoryResetAction : public Action<Ts...>, public Parented<SCD4XComponent> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->factory_reset(); }
|
||||
};
|
||||
|
||||
} // namespace scd4x
|
||||
} // namespace esphome
|
||||
@@ -13,55 +13,34 @@ static const uint16_t SCD4X_CMD_ALTITUDE_COMPENSATION = 0x2427;
|
||||
static const uint16_t SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION = 0xe000;
|
||||
static const uint16_t SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION = 0x2416;
|
||||
static const uint16_t SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS = 0x21b1;
|
||||
static const uint16_t SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS = 0x21ac;
|
||||
static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT = 0x219d; // SCD41 only
|
||||
static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY = 0x2196;
|
||||
static const uint16_t SCD4X_CMD_GET_DATA_READY_STATUS = 0xe4b8;
|
||||
static const uint16_t SCD4X_CMD_READ_MEASUREMENT = 0xec05;
|
||||
static const uint16_t SCD4X_CMD_PERFORM_FORCED_CALIBRATION = 0x362f;
|
||||
static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86;
|
||||
|
||||
static const uint16_t SCD4X_CMD_FACTORY_RESET = 0x3632;
|
||||
static const uint16_t SCD4X_CMD_GET_FEATURESET = 0x202f;
|
||||
static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f;
|
||||
static const uint16_t SCD41_ID = 0x1408;
|
||||
static const uint16_t SCD40_ID = 0x440;
|
||||
|
||||
void SCD4XComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up scd4x...");
|
||||
|
||||
// the sensor needs 1000 ms to enter the idle state
|
||||
this->set_timeout(1000, [this]() {
|
||||
// Check if measurement is ready before reading the value
|
||||
if (!this->write_command_(SCD4X_CMD_GET_DATA_READY_STATUS)) {
|
||||
ESP_LOGE(TAG, "Failed to write data ready status command");
|
||||
this->status_clear_error();
|
||||
if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
|
||||
ESP_LOGE(TAG, "Failed to stop measurements");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t raw_read_status[1];
|
||||
if (!this->read_data_(raw_read_status, 1)) {
|
||||
ESP_LOGE(TAG, "Failed to read data ready status");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t stop_measurement_delay = 0;
|
||||
// In order to query the device periodic measurement must be ceased
|
||||
if (raw_read_status[0]) {
|
||||
ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement");
|
||||
if (!this->write_command_(SCD4X_CMD_STOP_MEASUREMENTS)) {
|
||||
ESP_LOGE(TAG, "Failed to stop measurements");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after
|
||||
// issuing the stop_periodic_measurement command
|
||||
stop_measurement_delay = 500;
|
||||
}
|
||||
this->set_timeout(stop_measurement_delay, [this]() {
|
||||
if (!this->write_command_(SCD4X_CMD_GET_SERIAL_NUMBER)) {
|
||||
ESP_LOGE(TAG, "Failed to write get serial command");
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after
|
||||
// issuing the stop_periodic_measurement command
|
||||
this->set_timeout(500, [this]() {
|
||||
uint16_t raw_serial_number[3];
|
||||
if (!this->read_data_(raw_serial_number, 3)) {
|
||||
if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) {
|
||||
ESP_LOGE(TAG, "Failed to read serial number");
|
||||
this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
|
||||
this->mark_failed();
|
||||
@@ -70,8 +49,8 @@ void SCD4XComponent::setup() {
|
||||
ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", (uint16_t(raw_serial_number[0]) >> 8),
|
||||
uint16_t(raw_serial_number[0] & 0xFF), (uint16_t(raw_serial_number[1]) >> 8));
|
||||
|
||||
if (!this->write_command_(SCD4X_CMD_TEMPERATURE_OFFSET,
|
||||
(uint16_t)(temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) {
|
||||
if (!this->write_command(SCD4X_CMD_TEMPERATURE_OFFSET,
|
||||
(uint16_t)(temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) {
|
||||
ESP_LOGE(TAG, "Error setting temperature offset.");
|
||||
this->error_code_ = MEASUREMENT_INIT_FAILED;
|
||||
this->mark_failed();
|
||||
@@ -88,7 +67,7 @@ void SCD4XComponent::setup() {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!this->write_command_(SCD4X_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) {
|
||||
if (!this->write_command(SCD4X_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) {
|
||||
ESP_LOGE(TAG, "Error setting altitude compensation.");
|
||||
this->error_code_ = MEASUREMENT_INIT_FAILED;
|
||||
this->mark_failed();
|
||||
@@ -96,22 +75,16 @@ void SCD4XComponent::setup() {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->write_command_(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) {
|
||||
if (!this->write_command(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) {
|
||||
ESP_LOGE(TAG, "Error setting automatic self calibration.");
|
||||
this->error_code_ = MEASUREMENT_INIT_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Finally start sensor measurements
|
||||
if (!this->write_command_(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) {
|
||||
ESP_LOGE(TAG, "Error starting continuous measurements.");
|
||||
this->error_code_ = MEASUREMENT_INIT_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
initialized_ = true;
|
||||
// Finally start sensor measurements
|
||||
this->start_measurement_();
|
||||
ESP_LOGD(TAG, "Sensor initialized");
|
||||
});
|
||||
});
|
||||
@@ -137,12 +110,31 @@ void SCD4XComponent::dump_config() {
|
||||
}
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_));
|
||||
if (this->ambient_pressure_compensation_) {
|
||||
ESP_LOGCONFIG(TAG, " Altitude compensation disabled");
|
||||
ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_);
|
||||
if (this->ambient_pressure_source_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Dynamic ambient pressure compensation using sensor '%s'",
|
||||
this->ambient_pressure_source_->get_name().c_str());
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Ambient pressure compensation disabled");
|
||||
ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_);
|
||||
if (this->ambient_pressure_compensation_) {
|
||||
ESP_LOGCONFIG(TAG, " Altitude compensation disabled");
|
||||
ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Ambient pressure compensation disabled");
|
||||
ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_);
|
||||
}
|
||||
}
|
||||
switch (this->measurement_mode_) {
|
||||
case PERIODIC:
|
||||
ESP_LOGCONFIG(TAG, " Measurement mode: periodic (5s)");
|
||||
break;
|
||||
case LOW_POWER_PERIODIC:
|
||||
ESP_LOGCONFIG(TAG, " Measurement mode: low power periodic (30s)");
|
||||
break;
|
||||
case SINGLE_SHOT:
|
||||
ESP_LOGCONFIG(TAG, " Measurement mode: single shot");
|
||||
break;
|
||||
case SINGLE_SHOT_RHT_ONLY:
|
||||
ESP_LOGCONFIG(TAG, " Measurement mode: single shot rht only");
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Temperature offset: %.2f °C", this->temperature_offset_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
@@ -163,47 +155,105 @@ void SCD4XComponent::update() {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if data is ready
|
||||
if (!this->write_command_(SCD4X_CMD_GET_DATA_READY_STATUS)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
uint32_t wait_time = 0;
|
||||
if (this->measurement_mode_ == SINGLE_SHOT || this->measurement_mode_ == SINGLE_SHOT_RHT_ONLY) {
|
||||
start_measurement_();
|
||||
wait_time =
|
||||
this->measurement_mode_ == SINGLE_SHOT ? 5000 : 50; // Single shot measurement takes 5 secs rht mode 50 ms
|
||||
}
|
||||
this->set_timeout(wait_time, [this]() {
|
||||
// Check if data is ready
|
||||
if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t raw_read_status[1];
|
||||
if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, "Data not ready yet!");
|
||||
return;
|
||||
}
|
||||
uint16_t raw_read_status;
|
||||
|
||||
if (!this->write_command_(SCD4X_CMD_READ_MEASUREMENT)) {
|
||||
ESP_LOGW(TAG, "Error reading measurement!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
if (!this->read_data(raw_read_status) || raw_read_status == 0x00) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, "Data not ready yet!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read off sensor data
|
||||
uint16_t raw_data[3];
|
||||
if (!this->read_data_(raw_data, 3)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) {
|
||||
ESP_LOGW(TAG, "Error reading measurement!");
|
||||
this->status_set_warning();
|
||||
return; // NO RETRY
|
||||
}
|
||||
// Read off sensor data
|
||||
uint16_t raw_data[3];
|
||||
if (!this->read_data(raw_data, 3)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
if (this->co2_sensor_ != nullptr)
|
||||
this->co2_sensor_->publish_state(raw_data[0]);
|
||||
|
||||
if (this->co2_sensor_ != nullptr)
|
||||
this->co2_sensor_->publish_state(raw_data[0]);
|
||||
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16);
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
}
|
||||
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
const float humidity = (100.0f * raw_data[2]) / (1 << 16);
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16);
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
}
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
const float humidity = (100.0f * raw_data[2]) / (1 << 16);
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
}
|
||||
this->status_clear_warning();
|
||||
}); // set_timeout
|
||||
}
|
||||
|
||||
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
|
||||
concentration before performing a forced recalibration.
|
||||
*/
|
||||
if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
|
||||
ESP_LOGE(TAG, "Failed to stop measurements");
|
||||
this->status_set_warning();
|
||||
}
|
||||
this->set_timeout(500, [this, current_co2_concentration]() {
|
||||
if (this->write_command(SCD4X_CMD_PERFORM_FORCED_CALIBRATION, current_co2_concentration)) {
|
||||
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
|
||||
delay(400); // NOLINT'
|
||||
if (!this->start_measurement_()) {
|
||||
return false;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "forced calibration complete");
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "force calibration failed");
|
||||
this->error_code_ = FRC_FAILED;
|
||||
this->status_set_warning();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SCD4XComponent::factory_reset() {
|
||||
if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
|
||||
ESP_LOGE(TAG, "Failed to stop measurements");
|
||||
this->status_set_warning();
|
||||
return false;
|
||||
}
|
||||
|
||||
this->set_timeout(500, [this]() {
|
||||
if (!this->write_command(SCD4X_CMD_FACTORY_RESET)) {
|
||||
ESP_LOGE(TAG, "Failed to send factory reset command");
|
||||
this->status_set_warning();
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Factory reset complete");
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Note pressure in bar here. Convert to hPa
|
||||
void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) {
|
||||
ambient_pressure_compensation_ = true;
|
||||
@@ -218,7 +268,7 @@ void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) {
|
||||
}
|
||||
|
||||
bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_hpa) {
|
||||
if (this->write_command_(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) {
|
||||
if (this->write_command(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) {
|
||||
ESP_LOGD(TAG, "setting ambient pressure compensation to %d hPa", pressure_in_hpa);
|
||||
return true;
|
||||
} else {
|
||||
@@ -227,69 +277,37 @@ bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t SCD4XComponent::sht_crc_(uint8_t data1, uint8_t data2) {
|
||||
uint8_t bit;
|
||||
uint8_t crc = 0xFF;
|
||||
bool SCD4XComponent::start_measurement_() {
|
||||
uint16_t measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS;
|
||||
switch (this->measurement_mode_) {
|
||||
case PERIODIC:
|
||||
measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS;
|
||||
break;
|
||||
case LOW_POWER_PERIODIC:
|
||||
measurement_command = SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS;
|
||||
break;
|
||||
case SINGLE_SHOT:
|
||||
measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT;
|
||||
break;
|
||||
case SINGLE_SHOT_RHT_ONLY:
|
||||
measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY;
|
||||
break;
|
||||
}
|
||||
|
||||
crc ^= data1;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
static uint8_t remaining_retries = 3;
|
||||
while (remaining_retries) {
|
||||
if (!this->write_command(measurement_command)) {
|
||||
ESP_LOGE(TAG, "Error starting measurements.");
|
||||
this->error_code_ = MEASUREMENT_INIT_FAILED;
|
||||
this->status_set_warning();
|
||||
if (--remaining_retries == 0)
|
||||
return false;
|
||||
delay(50); // NOLINT wait 50 ms and try again
|
||||
}
|
||||
this->status_clear_warning();
|
||||
return true;
|
||||
}
|
||||
|
||||
crc ^= data2;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool SCD4XComponent::read_data_(uint16_t *data, uint8_t len) {
|
||||
const uint8_t num_bytes = len * 3;
|
||||
std::vector<uint8_t> buf(num_bytes);
|
||||
|
||||
if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
const uint8_t j = 3 * i;
|
||||
uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
|
||||
if (crc != buf[j + 2]) {
|
||||
ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
|
||||
return false;
|
||||
}
|
||||
data[i] = (buf[j] << 8) | buf[j + 1];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SCD4XComponent::write_command_(uint16_t command) {
|
||||
const uint8_t num_bytes = 2;
|
||||
uint8_t buffer[num_bytes];
|
||||
|
||||
buffer[0] = (command >> 8);
|
||||
buffer[1] = command & 0xff;
|
||||
|
||||
return this->write(buffer, num_bytes) == i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
bool SCD4XComponent::write_command_(uint16_t command, uint16_t data) {
|
||||
uint8_t raw[5];
|
||||
raw[0] = command >> 8;
|
||||
raw[1] = command & 0xFF;
|
||||
raw[2] = data >> 8;
|
||||
raw[3] = data & 0xFF;
|
||||
raw[4] = sht_crc_(raw[2], raw[3]);
|
||||
return this->write(raw, 5) == i2c::ERROR_OK;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace scd4x
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace scd4x {
|
||||
|
||||
enum ERRORCODE { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN };
|
||||
enum ERRORCODE {
|
||||
COMMUNICATION_FAILED,
|
||||
SERIAL_NUMBER_IDENTIFICATION_FAILED,
|
||||
MEASUREMENT_INIT_FAILED,
|
||||
FRC_FAILED,
|
||||
UNKNOWN
|
||||
};
|
||||
enum MeasurementMode { PERIODIC, LOW_POWER_PERIODIC, SINGLE_SHOT, SINGLE_SHOT_RHT_ONLY };
|
||||
|
||||
class SCD4XComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
public:
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void setup() override;
|
||||
@@ -25,14 +33,13 @@ class SCD4XComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; }
|
||||
void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; };
|
||||
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
|
||||
void set_measurement_mode(MeasurementMode mode) { measurement_mode_ = mode; }
|
||||
bool perform_forced_calibration(uint16_t current_co2_concentration);
|
||||
bool factory_reset();
|
||||
|
||||
protected:
|
||||
uint8_t sht_crc_(uint8_t data1, uint8_t data2);
|
||||
bool read_data_(uint16_t *data, uint8_t len);
|
||||
bool write_command_(uint16_t command);
|
||||
bool write_command_(uint16_t command, uint16_t data);
|
||||
bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa);
|
||||
|
||||
bool start_measurement_();
|
||||
ERRORCODE error_code_;
|
||||
|
||||
bool initialized_{false};
|
||||
@@ -42,7 +49,7 @@ class SCD4XComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
bool ambient_pressure_compensation_;
|
||||
uint16_t ambient_pressure_;
|
||||
bool enable_asc_;
|
||||
|
||||
MeasurementMode measurement_mode_{PERIODIC};
|
||||
sensor::Sensor *co2_sensor_{nullptr};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components import sensirion_common
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_CO2,
|
||||
CONF_HUMIDITY,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_VALUE,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -19,17 +23,37 @@ from esphome.const import (
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@sjtrny"]
|
||||
CODEOWNERS = ["@sjtrny", "@martgras"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
scd4x_ns = cg.esphome_ns.namespace("scd4x")
|
||||
SCD4XComponent = scd4x_ns.class_("SCD4XComponent", cg.PollingComponent, i2c.I2CDevice)
|
||||
SCD4XComponent = scd4x_ns.class_(
|
||||
"SCD4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
|
||||
)
|
||||
MeasurementMode = scd4x_ns.enum("MEASUREMENT_MODE")
|
||||
MEASUREMENT_MODE_OPTIONS = {
|
||||
"periodic": MeasurementMode.PERIODIC,
|
||||
"low_power_periodic": MeasurementMode.LOW_POWER_PERIODIC,
|
||||
"single_shot": MeasurementMode.SINGLE_SHOT,
|
||||
"single_shot_rht_only": MeasurementMode.SINGLE_SHOT_RHT_ONLY,
|
||||
}
|
||||
|
||||
|
||||
# Actions
|
||||
PerformForcedCalibrationAction = scd4x_ns.class_(
|
||||
"PerformForcedCalibrationAction", automation.Action
|
||||
)
|
||||
FactoryResetAction = scd4x_ns.class_("FactoryResetAction", automation.Action)
|
||||
|
||||
|
||||
CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration"
|
||||
CONF_ALTITUDE_COMPENSATION = "altitude_compensation"
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation"
|
||||
CONF_TEMPERATURE_OFFSET = "temperature_offset"
|
||||
CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source"
|
||||
CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration"
|
||||
CONF_MEASUREMENT_MODE = "measurement_mode"
|
||||
CONF_TEMPERATURE_OFFSET = "temperature_offset"
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
@@ -66,6 +90,9 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id(
|
||||
sensor.Sensor
|
||||
),
|
||||
cv.Optional(CONF_MEASUREMENT_MODE, default="periodic"): cv.enum(
|
||||
MEASUREMENT_MODE_OPTIONS, lower=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
@@ -103,3 +130,42 @@ async def to_code(config):
|
||||
if CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE in config:
|
||||
sens = await cg.get_variable(config[CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE])
|
||||
cg.add(var.set_ambient_pressure_source(sens))
|
||||
|
||||
cg.add(var.set_measurement_mode(config[CONF_MEASUREMENT_MODE]))
|
||||
|
||||
|
||||
SCD4X_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(SCD4XComponent),
|
||||
cv.Required(CONF_VALUE): cv.templatable(cv.positive_int),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"scd4x.perform_forced_calibration",
|
||||
PerformForcedCalibrationAction,
|
||||
SCD4X_ACTION_SCHEMA,
|
||||
)
|
||||
async def scd4x_frc_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint16)
|
||||
cg.add(var.set_value(template_))
|
||||
return var
|
||||
|
||||
|
||||
SCD4X_RESET_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(SCD4XComponent),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"scd4x.factory_reset", FactoryResetAction, SCD4X_RESET_ACTION_SCHEMA
|
||||
)
|
||||
async def scd4x_reset_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
@@ -7,55 +7,50 @@ namespace esphome {
|
||||
namespace sdp3x {
|
||||
|
||||
static const char *const TAG = "sdp3x.sensor";
|
||||
static const uint8_t SDP3X_SOFT_RESET[2] = {0x00, 0x06};
|
||||
static const uint8_t SDP3X_READ_ID1[2] = {0x36, 0x7C};
|
||||
static const uint8_t SDP3X_READ_ID2[2] = {0xE1, 0x02};
|
||||
static const uint8_t SDP3X_START_DP_AVG[2] = {0x36, 0x15};
|
||||
static const uint8_t SDP3X_START_MASS_FLOW_AVG[2] = {0x36, 0x03};
|
||||
static const uint8_t SDP3X_STOP_MEAS[2] = {0x3F, 0xF9};
|
||||
static const uint16_t SDP3X_SOFT_RESET = 0x0006;
|
||||
static const uint16_t SDP3X_READ_ID1 = 0x367C;
|
||||
static const uint16_t SDP3X_READ_ID2 = 0xE102;
|
||||
static const uint16_t SDP3X_START_DP_AVG = 0x3615;
|
||||
static const uint16_t SDP3X_START_MASS_FLOW_AVG = 0x3603;
|
||||
static const uint16_t SDP3X_STOP_MEAS = 0x3FF9;
|
||||
|
||||
void SDP3XComponent::update() { this->read_pressure_(); }
|
||||
|
||||
void SDP3XComponent::setup() {
|
||||
ESP_LOGD(TAG, "Setting up SDP3X...");
|
||||
|
||||
if (this->write(SDP3X_STOP_MEAS, 2) != i2c::ERROR_OK) {
|
||||
if (!this->write_command(SDP3X_STOP_MEAS)) {
|
||||
ESP_LOGW(TAG, "Stop SDP3X failed!"); // This sometimes fails for no good reason
|
||||
}
|
||||
|
||||
if (this->write(SDP3X_SOFT_RESET, 2) != i2c::ERROR_OK) {
|
||||
if (!this->write_command(SDP3X_SOFT_RESET)) {
|
||||
ESP_LOGW(TAG, "Soft Reset SDP3X failed!"); // This sometimes fails for no good reason
|
||||
}
|
||||
|
||||
this->set_timeout(20, [this] {
|
||||
if (this->write(SDP3X_READ_ID1, 2) != i2c::ERROR_OK) {
|
||||
if (!this->write_command(SDP3X_READ_ID1)) {
|
||||
ESP_LOGE(TAG, "Read ID1 SDP3X failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if (this->write(SDP3X_READ_ID2, 2) != i2c::ERROR_OK) {
|
||||
if (!this->write_command(SDP3X_READ_ID2)) {
|
||||
ESP_LOGE(TAG, "Read ID2 SDP3X failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t data[18];
|
||||
if (this->read(data, 18) != i2c::ERROR_OK) {
|
||||
uint16_t data[6];
|
||||
if (this->read_data(data, 6) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Read ID SDP3X failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]))) {
|
||||
ESP_LOGE(TAG, "CRC ID SDP3X failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// SDP8xx
|
||||
// ref:
|
||||
// https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/8_Differential_Pressure/Datasheets/Sensirion_Differential_Pressure_Datasheet_SDP8xx_Digital.pdf
|
||||
if (data[2] == 0x02) {
|
||||
switch (data[3]) {
|
||||
if (data[1] >> 8 == 0x02) {
|
||||
switch (data[1] & 0xFF) {
|
||||
case 0x01: // SDP800-500Pa
|
||||
ESP_LOGCONFIG(TAG, "Sensor is SDP800-500Pa");
|
||||
break;
|
||||
@@ -75,15 +70,16 @@ void SDP3XComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Sensor is SDP810-125Pa");
|
||||
break;
|
||||
}
|
||||
} else if (data[2] == 0x01) {
|
||||
if (data[3] == 0x01) {
|
||||
} else if (data[1] >> 8 == 0x01) {
|
||||
if ((data[1] & 0xFF) == 0x01) {
|
||||
ESP_LOGCONFIG(TAG, "Sensor is SDP31-500Pa");
|
||||
} else if (data[3] == 0x02) {
|
||||
} else if ((data[1] & 0xFF) == 0x02) {
|
||||
ESP_LOGCONFIG(TAG, "Sensor is SDP32-125Pa");
|
||||
}
|
||||
}
|
||||
|
||||
if (this->write(measurement_mode_ == DP_AVG ? SDP3X_START_DP_AVG : SDP3X_START_MASS_FLOW_AVG, 2) != i2c::ERROR_OK) {
|
||||
if (this->write_command(measurement_mode_ == DP_AVG ? SDP3X_START_DP_AVG : SDP3X_START_MASS_FLOW_AVG) !=
|
||||
i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Start Measurements SDP3X failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
@@ -101,22 +97,16 @@ void SDP3XComponent::dump_config() {
|
||||
}
|
||||
|
||||
void SDP3XComponent::read_pressure_() {
|
||||
uint8_t data[9];
|
||||
if (this->read(data, 9) != i2c::ERROR_OK) {
|
||||
uint16_t data[3];
|
||||
if (this->read_data(data, 3) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Couldn't read SDP3X data!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]) && check_crc_(&data[6], 2, data[8]))) {
|
||||
ESP_LOGW(TAG, "Invalid SDP3X data!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
int16_t pressure_raw = encode_uint16(data[0], data[1]);
|
||||
int16_t temperature_raw = encode_uint16(data[3], data[4]);
|
||||
int16_t scale_factor_raw = encode_uint16(data[6], data[7]);
|
||||
int16_t pressure_raw = data[0];
|
||||
int16_t temperature_raw = data[1];
|
||||
int16_t scale_factor_raw = data[2];
|
||||
// scale factor is in Pa - convert to hPa
|
||||
float pressure = pressure_raw / (scale_factor_raw * 100.0f);
|
||||
ESP_LOGV(TAG, "Got raw pressure=%d, raw scale factor =%d, raw temperature=%d ", pressure_raw, scale_factor_raw,
|
||||
@@ -129,26 +119,5 @@ void SDP3XComponent::read_pressure_() {
|
||||
|
||||
float SDP3XComponent::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
// Check CRC function from SDP3X sample code provided by sensirion
|
||||
// Returns true if a checksum is OK
|
||||
bool SDP3XComponent::check_crc_(const uint8_t data[], uint8_t size, uint8_t checksum) {
|
||||
uint8_t crc = 0xFF;
|
||||
|
||||
// calculates 8-Bit checksum with given polynomial 0x31 (x^8 + x^5 + x^4 + 1)
|
||||
for (int i = 0; i < size; i++) {
|
||||
crc ^= (data[i]);
|
||||
for (uint8_t bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x31;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// verify checksum
|
||||
return (crc == checksum);
|
||||
}
|
||||
|
||||
} // namespace sdp3x
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sdp3x {
|
||||
|
||||
enum MeasurementMode { MASS_FLOW_AVG, DP_AVG };
|
||||
|
||||
class SDP3XComponent : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor {
|
||||
class SDP3XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice, public sensor::Sensor {
|
||||
public:
|
||||
/// Schedule temperature+pressure readings.
|
||||
void update() override;
|
||||
@@ -23,8 +23,6 @@ class SDP3XComponent : public PollingComponent, public i2c::I2CDevice, public se
|
||||
protected:
|
||||
/// Internal method to read the pressure from the component after it has been scheduled.
|
||||
void read_pressure_();
|
||||
|
||||
bool check_crc_(const uint8_t data[], uint8_t size, uint8_t checksum);
|
||||
MeasurementMode measurement_mode_;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components import sensirion_common
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
@@ -8,10 +9,13 @@ from esphome.const import (
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
CODEOWNERS = ["@Azimath"]
|
||||
|
||||
sdp3x_ns = cg.esphome_ns.namespace("sdp3x")
|
||||
SDP3XComponent = sdp3x_ns.class_("SDP3XComponent", cg.PollingComponent, i2c.I2CDevice)
|
||||
SDP3XComponent = sdp3x_ns.class_(
|
||||
"SDP3XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
|
||||
)
|
||||
|
||||
|
||||
MeasurementMode = sdp3x_ns.enum("MeasurementMode")
|
||||
|
||||
@@ -9,6 +9,10 @@ from esphome.const import (
|
||||
CONF_OPTION,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_CYCLE,
|
||||
CONF_MODE,
|
||||
CONF_OPERATION,
|
||||
CONF_INDEX,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
@@ -22,14 +26,27 @@ SelectPtr = Select.operator("ptr")
|
||||
|
||||
# Triggers
|
||||
SelectStateTrigger = select_ns.class_(
|
||||
"SelectStateTrigger", automation.Trigger.template(cg.float_)
|
||||
"SelectStateTrigger",
|
||||
automation.Trigger.template(cg.std_string, cg.size_t),
|
||||
)
|
||||
|
||||
# Actions
|
||||
SelectSetAction = select_ns.class_("SelectSetAction", automation.Action)
|
||||
SelectSetIndexAction = select_ns.class_("SelectSetIndexAction", automation.Action)
|
||||
SelectOperationAction = select_ns.class_("SelectOperationAction", automation.Action)
|
||||
|
||||
# Enums
|
||||
SelectOperation = select_ns.enum("SelectOperation")
|
||||
SELECT_OPERATION_OPTIONS = {
|
||||
"NEXT": SelectOperation.SELECT_OP_NEXT,
|
||||
"PREVIOUS": SelectOperation.SELECT_OP_PREVIOUS,
|
||||
"FIRST": SelectOperation.SELECT_OP_FIRST,
|
||||
"LAST": SelectOperation.SELECT_OP_LAST,
|
||||
}
|
||||
|
||||
icon = cv.icon
|
||||
|
||||
|
||||
SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent),
|
||||
@@ -50,7 +67,9 @@ async def setup_select_core_(var, config, *, options: List[str]):
|
||||
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.std_string, "x"), (cg.size_t, "i")], conf
|
||||
)
|
||||
|
||||
if CONF_MQTT_ID in config:
|
||||
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
|
||||
@@ -76,12 +95,18 @@ async def to_code(config):
|
||||
cg.add_global(select_ns.using)
|
||||
|
||||
|
||||
OPERATION_BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Select),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"select.set",
|
||||
SelectSetAction,
|
||||
cv.Schema(
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Select),
|
||||
cv.Required(CONF_OPTION): cv.templatable(cv.string_strict),
|
||||
}
|
||||
),
|
||||
@@ -92,3 +117,96 @@ async def select_set_to_code(config, action_id, template_arg, args):
|
||||
template_ = await cg.templatable(config[CONF_OPTION], args, cg.std_string)
|
||||
cg.add(var.set_option(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"select.set_index",
|
||||
SelectSetIndexAction,
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_INDEX): cv.templatable(cv.positive_int),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def select_set_index_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
template_ = await cg.templatable(config[CONF_INDEX], args, cg.size_t)
|
||||
cg.add(var.set_index(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"select.operation",
|
||||
SelectOperationAction,
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_OPERATION): cv.templatable(
|
||||
cv.enum(SELECT_OPERATION_OPTIONS, upper=True)
|
||||
),
|
||||
cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean),
|
||||
}
|
||||
),
|
||||
)
|
||||
@automation.register_action(
|
||||
"select.next",
|
||||
SelectOperationAction,
|
||||
automation.maybe_simple_id(
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MODE, default="NEXT"): cv.one_of("NEXT", upper=True),
|
||||
cv.Optional(CONF_CYCLE, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
),
|
||||
)
|
||||
@automation.register_action(
|
||||
"select.previous",
|
||||
SelectOperationAction,
|
||||
automation.maybe_simple_id(
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MODE, default="PREVIOUS"): cv.one_of(
|
||||
"PREVIOUS", upper=True
|
||||
),
|
||||
cv.Optional(CONF_CYCLE, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
),
|
||||
)
|
||||
@automation.register_action(
|
||||
"select.first",
|
||||
SelectOperationAction,
|
||||
automation.maybe_simple_id(
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MODE, default="FIRST"): cv.one_of("FIRST", upper=True),
|
||||
}
|
||||
)
|
||||
),
|
||||
)
|
||||
@automation.register_action(
|
||||
"select.last",
|
||||
SelectOperationAction,
|
||||
automation.maybe_simple_id(
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MODE, default="LAST"): cv.one_of("LAST", upper=True),
|
||||
}
|
||||
)
|
||||
),
|
||||
)
|
||||
async def select_operation_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
if CONF_OPERATION in config:
|
||||
op_ = await cg.templatable(config[CONF_OPERATION], args, SelectOperation)
|
||||
cg.add(var.set_operation(op_))
|
||||
if CONF_CYCLE in config:
|
||||
cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool)
|
||||
cg.add(var.set_cycle(cycle_))
|
||||
if CONF_MODE in config:
|
||||
cg.add(var.set_operation(SELECT_OPERATION_OPTIONS[config[CONF_MODE]]))
|
||||
if CONF_CYCLE in config:
|
||||
cg.add(var.set_cycle(config[CONF_CYCLE]))
|
||||
return var
|
||||
|
||||
@@ -7,16 +7,16 @@
|
||||
namespace esphome {
|
||||
namespace select {
|
||||
|
||||
class SelectStateTrigger : public Trigger<std::string> {
|
||||
class SelectStateTrigger : public Trigger<std::string, size_t> {
|
||||
public:
|
||||
explicit SelectStateTrigger(Select *parent) {
|
||||
parent->add_on_state_callback([this](const std::string &value) { this->trigger(value); });
|
||||
parent->add_on_state_callback([this](const std::string &value, size_t index) { this->trigger(value, index); });
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class SelectSetAction : public Action<Ts...> {
|
||||
public:
|
||||
SelectSetAction(Select *select) : select_(select) {}
|
||||
explicit SelectSetAction(Select *select) : select_(select) {}
|
||||
TEMPLATABLE_VALUE(std::string, option)
|
||||
|
||||
void play(Ts... x) override {
|
||||
@@ -29,5 +29,39 @@ template<typename... Ts> class SelectSetAction : public Action<Ts...> {
|
||||
Select *select_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class SelectSetIndexAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit SelectSetIndexAction(Select *select) : select_(select) {}
|
||||
TEMPLATABLE_VALUE(size_t, index)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto call = this->select_->make_call();
|
||||
call.set_index(this->index_.value(x...));
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
Select *select_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class SelectOperationAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit SelectOperationAction(Select *select) : select_(select) {}
|
||||
TEMPLATABLE_VALUE(bool, cycle)
|
||||
TEMPLATABLE_VALUE(SelectOperation, operation)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto call = this->select_->make_call();
|
||||
call.with_operation(this->operation_.value(x...));
|
||||
if (this->cycle_.has_value()) {
|
||||
call.with_cycle(this->cycle_.value(x...));
|
||||
}
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
Select *select_;
|
||||
};
|
||||
|
||||
} // namespace select
|
||||
} // namespace esphome
|
||||
|
||||
@@ -6,37 +6,58 @@ namespace select {
|
||||
|
||||
static const char *const TAG = "select";
|
||||
|
||||
void SelectCall::perform() {
|
||||
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
|
||||
if (!this->option_.has_value()) {
|
||||
ESP_LOGW(TAG, "No value set for SelectCall");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &traits = this->parent_->traits;
|
||||
auto value = *this->option_;
|
||||
auto options = traits.get_options();
|
||||
|
||||
if (std::find(options.begin(), options.end(), value) == options.end()) {
|
||||
ESP_LOGW(TAG, " Option %s is not a valid option.", value.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, " Option: %s", (*this->option_).c_str());
|
||||
this->parent_->control(*this->option_);
|
||||
}
|
||||
|
||||
void Select::publish_state(const std::string &state) {
|
||||
this->has_state_ = true;
|
||||
this->state = state;
|
||||
ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str());
|
||||
this->state_callback_.call(state);
|
||||
auto index = this->index_of(state);
|
||||
const auto *name = this->get_name().c_str();
|
||||
if (index.has_value()) {
|
||||
this->has_state_ = true;
|
||||
this->state = state;
|
||||
ESP_LOGD(TAG, "'%s': Sending state %s (index %d)", name, state.c_str(), index.value());
|
||||
this->state_callback_.call(state, index.value());
|
||||
} else {
|
||||
ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Select::add_on_state_callback(std::function<void(std::string)> &&callback) {
|
||||
void Select::add_on_state_callback(std::function<void(std::string, size_t)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
bool Select::has_option(const std::string &option) const { return this->index_of(option).has_value(); }
|
||||
|
||||
bool Select::has_index(size_t index) const { return index < this->size(); }
|
||||
|
||||
size_t Select::size() const {
|
||||
auto options = traits.get_options();
|
||||
return options.size();
|
||||
}
|
||||
|
||||
optional<size_t> Select::index_of(const std::string &option) const {
|
||||
auto options = traits.get_options();
|
||||
auto it = std::find(options.begin(), options.end(), option);
|
||||
if (it == options.end()) {
|
||||
return {};
|
||||
}
|
||||
return std::distance(options.begin(), it);
|
||||
}
|
||||
|
||||
optional<size_t> Select::active_index() const {
|
||||
if (this->has_state()) {
|
||||
return this->index_of(this->state);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
optional<std::string> Select::at(size_t index) const {
|
||||
if (this->has_index(index)) {
|
||||
auto options = traits.get_options();
|
||||
return options.at(index);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Select::hash_base() { return 2812997003UL; }
|
||||
|
||||
} // namespace select
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "select_call.h"
|
||||
#include "select_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace select {
|
||||
@@ -17,33 +17,6 @@ namespace select {
|
||||
} \
|
||||
}
|
||||
|
||||
class Select;
|
||||
|
||||
class SelectCall {
|
||||
public:
|
||||
explicit SelectCall(Select *parent) : parent_(parent) {}
|
||||
void perform();
|
||||
|
||||
SelectCall &set_option(const std::string &option) {
|
||||
option_ = option;
|
||||
return *this;
|
||||
}
|
||||
const optional<std::string> &get_option() const { return option_; }
|
||||
|
||||
protected:
|
||||
Select *const parent_;
|
||||
optional<std::string> option_;
|
||||
};
|
||||
|
||||
class SelectTraits {
|
||||
public:
|
||||
void set_options(std::vector<std::string> options) { this->options_ = std::move(options); }
|
||||
std::vector<std::string> get_options() const { return this->options_; }
|
||||
|
||||
protected:
|
||||
std::vector<std::string> options_;
|
||||
};
|
||||
|
||||
/** Base-class for all selects.
|
||||
*
|
||||
* A select can use publish_state to send out a new value.
|
||||
@@ -51,19 +24,36 @@ class SelectTraits {
|
||||
class Select : public EntityBase {
|
||||
public:
|
||||
std::string state;
|
||||
SelectTraits traits;
|
||||
|
||||
void publish_state(const std::string &state);
|
||||
|
||||
SelectCall make_call() { return SelectCall(this); }
|
||||
void set(const std::string &value) { make_call().set_option(value).perform(); }
|
||||
|
||||
void add_on_state_callback(std::function<void(std::string)> &&callback);
|
||||
|
||||
SelectTraits traits;
|
||||
|
||||
/// Return whether this select has gotten a full state yet.
|
||||
/// Return whether this select component has gotten a full state yet.
|
||||
bool has_state() const { return has_state_; }
|
||||
|
||||
/// Instantiate a SelectCall object to modify this select component's state.
|
||||
SelectCall make_call() { return SelectCall(this); }
|
||||
|
||||
/// Return whether this select component contains the provided option.
|
||||
bool has_option(const std::string &option) const;
|
||||
|
||||
/// Return whether this select component contains the provided index offset.
|
||||
bool has_index(size_t index) const;
|
||||
|
||||
/// Return the number of options in this select component.
|
||||
size_t size() const;
|
||||
|
||||
/// Find the (optional) index offset of the provided option value.
|
||||
optional<size_t> index_of(const std::string &option) const;
|
||||
|
||||
/// Return the (optional) index offset of the currently active option.
|
||||
optional<size_t> active_index() const;
|
||||
|
||||
/// Return the (optional) option value at the provided index offset.
|
||||
optional<std::string> at(size_t index) const;
|
||||
|
||||
void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
|
||||
|
||||
protected:
|
||||
friend class SelectCall;
|
||||
|
||||
@@ -77,7 +67,7 @@ class Select : public EntityBase {
|
||||
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void(std::string)> state_callback_;
|
||||
CallbackManager<void(std::string, size_t)> state_callback_;
|
||||
bool has_state_{false};
|
||||
};
|
||||
|
||||
|
||||
120
esphome/components/select/select_call.cpp
Normal file
120
esphome/components/select/select_call.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "select_call.h"
|
||||
#include "select.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace select {
|
||||
|
||||
static const char *const TAG = "select";
|
||||
|
||||
SelectCall &SelectCall::set_option(const std::string &option) {
|
||||
return with_operation(SELECT_OP_SET).with_option(option);
|
||||
}
|
||||
|
||||
SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); }
|
||||
|
||||
SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); }
|
||||
|
||||
SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); }
|
||||
|
||||
SelectCall &SelectCall::select_first() { return with_operation(SELECT_OP_FIRST); }
|
||||
|
||||
SelectCall &SelectCall::select_last() { return with_operation(SELECT_OP_LAST); }
|
||||
|
||||
SelectCall &SelectCall::with_operation(SelectOperation operation) {
|
||||
this->operation_ = operation;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SelectCall &SelectCall::with_cycle(bool cycle) {
|
||||
this->cycle_ = cycle;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SelectCall &SelectCall::with_option(const std::string &option) {
|
||||
this->option_ = option;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SelectCall &SelectCall::with_index(size_t index) {
|
||||
this->index_ = index;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void SelectCall::perform() {
|
||||
auto *parent = this->parent_;
|
||||
const auto *name = parent->get_name().c_str();
|
||||
const auto &traits = parent->traits;
|
||||
auto options = traits.get_options();
|
||||
|
||||
if (this->operation_ == SELECT_OP_NONE) {
|
||||
ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name);
|
||||
return;
|
||||
}
|
||||
if (options.empty()) {
|
||||
ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string target_value;
|
||||
|
||||
if (this->operation_ == SELECT_OP_SET) {
|
||||
ESP_LOGD(TAG, "'%s' - Setting", name);
|
||||
if (!this->option_.has_value()) {
|
||||
ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name);
|
||||
return;
|
||||
}
|
||||
target_value = this->option_.value();
|
||||
} else if (this->operation_ == SELECT_OP_SET_INDEX) {
|
||||
if (!this->index_.has_value()) {
|
||||
ESP_LOGW(TAG, "'%s' - No index value set for SelectCall", name);
|
||||
return;
|
||||
}
|
||||
if (this->index_.value() >= options.size()) {
|
||||
ESP_LOGW(TAG, "'%s' - Index value %d out of bounds", name, this->index_.value());
|
||||
return;
|
||||
}
|
||||
target_value = options[this->index_.value()];
|
||||
} else if (this->operation_ == SELECT_OP_FIRST) {
|
||||
target_value = options.front();
|
||||
} else if (this->operation_ == SELECT_OP_LAST) {
|
||||
target_value = options.back();
|
||||
} else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) {
|
||||
auto cycle = this->cycle_;
|
||||
ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous",
|
||||
cycle ? "" : "out");
|
||||
if (!parent->has_state()) {
|
||||
target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back();
|
||||
} else {
|
||||
auto index = parent->index_of(parent->state);
|
||||
if (index.has_value()) {
|
||||
auto size = options.size();
|
||||
if (cycle) {
|
||||
auto use_index = (size + index.value() + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size;
|
||||
target_value = options[use_index];
|
||||
} else {
|
||||
if (this->operation_ == SELECT_OP_PREVIOUS && index.value() > 0) {
|
||||
target_value = options[index.value() - 1];
|
||||
} else if (this->operation_ == SELECT_OP_NEXT && index.value() < options.size() - 1) {
|
||||
target_value = options[index.value() + 1];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (std::find(options.begin(), options.end(), target_value) == options.end()) {
|
||||
ESP_LOGW(TAG, "'%s' - Option %s is not a valid option", name, target_value.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, target_value.c_str());
|
||||
parent->control(target_value);
|
||||
}
|
||||
|
||||
} // namespace select
|
||||
} // namespace esphome
|
||||
47
esphome/components/select/select_call.h
Normal file
47
esphome/components/select/select_call.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace select {
|
||||
|
||||
class Select;
|
||||
|
||||
enum SelectOperation {
|
||||
SELECT_OP_NONE,
|
||||
SELECT_OP_SET,
|
||||
SELECT_OP_SET_INDEX,
|
||||
SELECT_OP_NEXT,
|
||||
SELECT_OP_PREVIOUS,
|
||||
SELECT_OP_FIRST,
|
||||
SELECT_OP_LAST
|
||||
};
|
||||
|
||||
class SelectCall {
|
||||
public:
|
||||
explicit SelectCall(Select *parent) : parent_(parent) {}
|
||||
void perform();
|
||||
|
||||
SelectCall &set_option(const std::string &option);
|
||||
SelectCall &set_index(size_t index);
|
||||
|
||||
SelectCall &select_next(bool cycle);
|
||||
SelectCall &select_previous(bool cycle);
|
||||
SelectCall &select_first();
|
||||
SelectCall &select_last();
|
||||
|
||||
SelectCall &with_operation(SelectOperation operation);
|
||||
SelectCall &with_cycle(bool cycle);
|
||||
SelectCall &with_option(const std::string &option);
|
||||
SelectCall &with_index(size_t index);
|
||||
|
||||
protected:
|
||||
Select *const parent_;
|
||||
optional<std::string> option_;
|
||||
optional<size_t> index_;
|
||||
SelectOperation operation_{SELECT_OP_NONE};
|
||||
bool cycle_;
|
||||
};
|
||||
|
||||
} // namespace select
|
||||
} // namespace esphome
|
||||
11
esphome/components/select/select_traits.cpp
Normal file
11
esphome/components/select/select_traits.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include "select_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace select {
|
||||
|
||||
void SelectTraits::set_options(std::vector<std::string> options) { this->options_ = std::move(options); }
|
||||
|
||||
std::vector<std::string> SelectTraits::get_options() const { return this->options_; }
|
||||
|
||||
} // namespace select
|
||||
} // namespace esphome
|
||||
19
esphome/components/select/select_traits.h
Normal file
19
esphome/components/select/select_traits.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace esphome {
|
||||
namespace select {
|
||||
|
||||
class SelectTraits {
|
||||
public:
|
||||
void set_options(std::vector<std::string> options);
|
||||
std::vector<std::string> get_options() const;
|
||||
|
||||
protected:
|
||||
std::vector<std::string> options_;
|
||||
};
|
||||
|
||||
} // namespace select
|
||||
} // namespace esphome
|
||||
0
esphome/components/sen5x/__init__.py
Normal file
0
esphome/components/sen5x/__init__.py
Normal file
21
esphome/components/sen5x/automation.h
Normal file
21
esphome/components/sen5x/automation.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "sen5x.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sen5x {
|
||||
|
||||
template<typename... Ts> class StartFanAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit StartFanAction(SEN5XComponent *sen5x) : sen5x_(sen5x) {}
|
||||
|
||||
void play(Ts... x) override { this->sen5x_->start_fan_cleaning(); }
|
||||
|
||||
protected:
|
||||
SEN5XComponent *sen5x_;
|
||||
};
|
||||
|
||||
} // namespace sen5x
|
||||
} // namespace esphome
|
||||
413
esphome/components/sen5x/sen5x.cpp
Normal file
413
esphome/components/sen5x/sen5x.cpp
Normal file
@@ -0,0 +1,413 @@
|
||||
#include "sen5x.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sen5x {
|
||||
|
||||
static const char *const TAG = "sen5x";
|
||||
|
||||
static const uint16_t SEN5X_CMD_AUTO_CLEANING_INTERVAL = 0x8004;
|
||||
static const uint16_t SEN5X_CMD_GET_DATA_READY_STATUS = 0x0202;
|
||||
static const uint16_t SEN5X_CMD_GET_FIRMWARE_VERSION = 0xD100;
|
||||
static const uint16_t SEN5X_CMD_GET_PRODUCT_NAME = 0xD014;
|
||||
static const uint16_t SEN5X_CMD_GET_SERIAL_NUMBER = 0xD033;
|
||||
static const uint16_t SEN5X_CMD_NOX_ALGORITHM_TUNING = 0x60E1;
|
||||
static const uint16_t SEN5X_CMD_READ_MEASUREMENT = 0x03C4;
|
||||
static const uint16_t SEN5X_CMD_RHT_ACCELERATION_MODE = 0x60F7;
|
||||
static const uint16_t SEN5X_CMD_START_CLEANING_FAN = 0x5607;
|
||||
static const uint16_t SEN5X_CMD_START_MEASUREMENTS = 0x0021;
|
||||
static const uint16_t SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY = 0x0037;
|
||||
static const uint16_t SEN5X_CMD_STOP_MEASUREMENTS = 0x3f86;
|
||||
static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2;
|
||||
static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181;
|
||||
static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
|
||||
|
||||
void SEN5XComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up sen5x...");
|
||||
|
||||
// the sensor needs 1000 ms to enter the idle state
|
||||
this->set_timeout(1000, [this]() {
|
||||
// Check if measurement is ready before reading the value
|
||||
if (!this->write_command(SEN5X_CMD_GET_DATA_READY_STATUS)) {
|
||||
ESP_LOGE(TAG, "Failed to write data ready status command");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t raw_read_status;
|
||||
if (!this->read_data(raw_read_status)) {
|
||||
ESP_LOGE(TAG, "Failed to read data ready status");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t stop_measurement_delay = 0;
|
||||
// In order to query the device periodic measurement must be ceased
|
||||
if (raw_read_status) {
|
||||
ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement");
|
||||
if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) {
|
||||
ESP_LOGE(TAG, "Failed to stop measurements");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// According to the SEN5x datasheet the sensor will only respond to other commands after waiting 200 ms after
|
||||
// issuing the stop_periodic_measurement command
|
||||
stop_measurement_delay = 200;
|
||||
}
|
||||
this->set_timeout(stop_measurement_delay, [this]() {
|
||||
uint16_t raw_serial_number[3];
|
||||
if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) {
|
||||
ESP_LOGE(TAG, "Failed to read serial number");
|
||||
this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->serial_number_[0] = static_cast<bool>(uint16_t(raw_serial_number[0]) & 0xFF);
|
||||
this->serial_number_[1] = static_cast<uint16_t>(raw_serial_number[0] & 0xFF);
|
||||
this->serial_number_[2] = static_cast<uint16_t>(raw_serial_number[1] >> 8);
|
||||
ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]);
|
||||
|
||||
uint16_t raw_product_name[16];
|
||||
if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
|
||||
ESP_LOGE(TAG, "Failed to read product name");
|
||||
this->error_code_ = PRODUCT_NAME_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// 2 ASCII bytes are encoded in an int
|
||||
const uint16_t *current_int = raw_product_name;
|
||||
char current_char;
|
||||
uint8_t max = 16;
|
||||
do {
|
||||
// first char
|
||||
current_char = *current_int >> 8;
|
||||
if (current_char) {
|
||||
product_name_.push_back(current_char);
|
||||
// second char
|
||||
current_char = *current_int & 0xFF;
|
||||
if (current_char)
|
||||
product_name_.push_back(current_char);
|
||||
}
|
||||
current_int++;
|
||||
} while (current_char && --max);
|
||||
|
||||
Sen5xType sen5x_type = UNKNOWN;
|
||||
if (product_name_ == "SEN50") {
|
||||
sen5x_type = SEN50;
|
||||
} else {
|
||||
if (product_name_ == "SEN54") {
|
||||
sen5x_type = SEN54;
|
||||
} else {
|
||||
if (product_name_ == "SEN55") {
|
||||
sen5x_type = SEN55;
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Productname %s", product_name_.c_str());
|
||||
}
|
||||
if (this->humidity_sensor_ && sen5x_type == SEN50) {
|
||||
ESP_LOGE(TAG, "For Relative humidity a SEN54 OR SEN55 is required. You are using a <%s> sensor",
|
||||
this->product_name_.c_str());
|
||||
this->humidity_sensor_ = nullptr; // mark as not used
|
||||
}
|
||||
if (this->temperature_sensor_ && sen5x_type == SEN50) {
|
||||
ESP_LOGE(TAG, "For Temperature a SEN54 OR SEN55 is required. You are using a <%s> sensor",
|
||||
this->product_name_.c_str());
|
||||
this->temperature_sensor_ = nullptr; // mark as not used
|
||||
}
|
||||
if (this->voc_sensor_ && sen5x_type == SEN50) {
|
||||
ESP_LOGE(TAG, "For VOC a SEN54 OR SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str());
|
||||
this->voc_sensor_ = nullptr; // mark as not used
|
||||
}
|
||||
if (this->nox_sensor_ && sen5x_type != SEN55) {
|
||||
ESP_LOGE(TAG, "For NOx a SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str());
|
||||
this->nox_sensor_ = nullptr; // mark as not used
|
||||
}
|
||||
|
||||
if (!this->get_register(SEN5X_CMD_GET_FIRMWARE_VERSION, this->firmware_version_, 20)) {
|
||||
ESP_LOGE(TAG, "Failed to read firmware version");
|
||||
this->error_code_ = FIRMWARE_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->firmware_version_ >>= 8;
|
||||
ESP_LOGD(TAG, "Firmware version %d", this->firmware_version_);
|
||||
|
||||
if (this->voc_sensor_ && this->store_baseline_) {
|
||||
// Hash with compilation time
|
||||
// This ensures the baseline storage is cleared after OTA
|
||||
uint32_t hash = fnv1_hash(App.get_compilation_time());
|
||||
this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true);
|
||||
|
||||
if (this->pref_.load(&this->voc_baselines_storage_)) {
|
||||
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0,
|
||||
voc_baselines_storage_.state1);
|
||||
}
|
||||
|
||||
// Initialize storage timestamp
|
||||
this->seconds_since_last_store_ = 0;
|
||||
|
||||
if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
|
||||
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X",
|
||||
this->voc_baselines_storage_.state0, voc_baselines_storage_.state1);
|
||||
uint16_t states[4];
|
||||
|
||||
states[0] = voc_baselines_storage_.state0 >> 16;
|
||||
states[1] = voc_baselines_storage_.state0 & 0xFFFF;
|
||||
states[2] = voc_baselines_storage_.state1 >> 16;
|
||||
states[3] = voc_baselines_storage_.state1 & 0xFFFF;
|
||||
|
||||
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) {
|
||||
ESP_LOGE(TAG, "Failed to set VOC baseline from saved state");
|
||||
}
|
||||
}
|
||||
}
|
||||
bool result;
|
||||
if (this->auto_cleaning_interval_.has_value()) {
|
||||
// override default value
|
||||
result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value());
|
||||
} else {
|
||||
result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
|
||||
}
|
||||
if (result) {
|
||||
delay(20);
|
||||
uint16_t secs[2];
|
||||
if (this->read_data(secs, 2)) {
|
||||
auto_cleaning_interval_ = secs[0] << 16 | secs[1];
|
||||
}
|
||||
}
|
||||
if (acceleration_mode_.has_value()) {
|
||||
result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, acceleration_mode_.value());
|
||||
} else {
|
||||
result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE);
|
||||
}
|
||||
if (!result) {
|
||||
ESP_LOGE(TAG, "Failed to set rh/t acceleration mode");
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
delay(20);
|
||||
if (!acceleration_mode_.has_value()) {
|
||||
uint16_t mode;
|
||||
if (this->read_data(mode)) {
|
||||
this->acceleration_mode_ = RhtAccelerationMode(mode);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to read RHT Acceleration mode");
|
||||
}
|
||||
}
|
||||
if (this->voc_tuning_params_.has_value())
|
||||
this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value());
|
||||
if (this->nox_tuning_params_.has_value())
|
||||
this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
|
||||
|
||||
if (this->temperature_compensation_.has_value())
|
||||
this->write_temperature_compensation_(this->temperature_compensation_.value());
|
||||
|
||||
// Finally start sensor measurements
|
||||
auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY;
|
||||
if (this->pm_1_0_sensor_ || this->pm_2_5_sensor_ || this->pm_4_0_sensor_ || this->pm_10_0_sensor_) {
|
||||
// if any of the gas sensors are active we need a full measurement
|
||||
cmd = SEN5X_CMD_START_MEASUREMENTS;
|
||||
}
|
||||
|
||||
if (!this->write_command(cmd)) {
|
||||
ESP_LOGE(TAG, "Error starting continuous measurements.");
|
||||
this->error_code_ = MEASUREMENT_INIT_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
initialized_ = true;
|
||||
ESP_LOGD(TAG, "Sensor initialized");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void SEN5XComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "sen5x:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
switch (this->error_code_) {
|
||||
case COMMUNICATION_FAILED:
|
||||
ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
|
||||
break;
|
||||
case MEASUREMENT_INIT_FAILED:
|
||||
ESP_LOGW(TAG, "Measurement Initialization failed!");
|
||||
break;
|
||||
case SERIAL_NUMBER_IDENTIFICATION_FAILED:
|
||||
ESP_LOGW(TAG, "Unable to read sensor serial id");
|
||||
break;
|
||||
case PRODUCT_NAME_FAILED:
|
||||
ESP_LOGW(TAG, "Unable to read product name");
|
||||
break;
|
||||
case FIRMWARE_FAILED:
|
||||
ESP_LOGW(TAG, "Unable to read sensor firmware version");
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown setup error!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Productname: %s", this->product_name_.c_str());
|
||||
ESP_LOGCONFIG(TAG, " Firmware version: %d", this->firmware_version_);
|
||||
ESP_LOGCONFIG(TAG, " Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]);
|
||||
if (this->auto_cleaning_interval_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Auto auto cleaning interval %d seconds", auto_cleaning_interval_.value());
|
||||
}
|
||||
if (this->acceleration_mode_.has_value()) {
|
||||
switch (this->acceleration_mode_.value()) {
|
||||
case LOW_ACCELERATION:
|
||||
ESP_LOGCONFIG(TAG, " Low RH/T acceleration mode");
|
||||
break;
|
||||
case MEDIUM_ACCELERATION:
|
||||
ESP_LOGCONFIG(TAG, " Medium RH/T accelertion mode");
|
||||
break;
|
||||
case HIGH_ACCELERATION:
|
||||
ESP_LOGCONFIG(TAG, " High RH/T accelertion mode");
|
||||
break;
|
||||
}
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
|
||||
LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
|
||||
LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_);
|
||||
LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
LOG_SENSOR(" ", "VOC", this->voc_sensor_); // SEN54 and SEN55 only
|
||||
LOG_SENSOR(" ", "NOx", this->nox_sensor_); // SEN55 only
|
||||
}
|
||||
|
||||
void SEN5XComponent::update() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
|
||||
// much
|
||||
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
|
||||
if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
|
||||
// run it a bit later to avoid adding a delay here
|
||||
this->set_timeout(550, [this]() {
|
||||
uint16_t states[4];
|
||||
if (this->read_data(states, 4)) {
|
||||
uint32_t state0 = states[0] << 16 | states[1];
|
||||
uint32_t state1 = states[2] << 16 | states[3];
|
||||
if ((uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state0 - state0)) >
|
||||
MAXIMUM_STORAGE_DIFF ||
|
||||
(uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state1 - state1)) >
|
||||
MAXIMUM_STORAGE_DIFF) {
|
||||
this->seconds_since_last_store_ = 0;
|
||||
this->voc_baselines_storage_.state0 = state0;
|
||||
this->voc_baselines_storage_.state1 = state1;
|
||||
|
||||
if (this->pref_.save(&this->voc_baselines_storage_)) {
|
||||
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0,
|
||||
voc_baselines_storage_.state1);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Could not store VOC baselines");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "write error read measurement (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
this->set_timeout(20, [this]() {
|
||||
uint16_t measurements[8];
|
||||
|
||||
if (!this->read_data(measurements, 8)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "read data error (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
float pm_1_0 = measurements[0] / 10.0;
|
||||
if (measurements[0] == 0xFFFF)
|
||||
pm_1_0 = NAN;
|
||||
float pm_2_5 = measurements[1] / 10.0;
|
||||
if (measurements[1] == 0xFFFF)
|
||||
pm_2_5 = NAN;
|
||||
float pm_4_0 = measurements[2] / 10.0;
|
||||
if (measurements[2] == 0xFFFF)
|
||||
pm_4_0 = NAN;
|
||||
float pm_10_0 = measurements[3] / 10.0;
|
||||
if (measurements[3] == 0xFFFF)
|
||||
pm_10_0 = NAN;
|
||||
float humidity = measurements[4] / 100.0;
|
||||
if (measurements[4] == 0xFFFF)
|
||||
humidity = NAN;
|
||||
float temperature = measurements[5] / 200.0;
|
||||
if (measurements[5] == 0xFFFF)
|
||||
temperature = NAN;
|
||||
float voc = measurements[6] / 10.0;
|
||||
if (measurements[6] == 0xFFFF)
|
||||
voc = NAN;
|
||||
float nox = measurements[7] / 10.0;
|
||||
if (measurements[7] == 0xFFFF)
|
||||
nox = NAN;
|
||||
|
||||
if (this->pm_1_0_sensor_ != nullptr)
|
||||
this->pm_1_0_sensor_->publish_state(pm_1_0);
|
||||
if (this->pm_2_5_sensor_ != nullptr)
|
||||
this->pm_2_5_sensor_->publish_state(pm_2_5);
|
||||
if (this->pm_4_0_sensor_ != nullptr)
|
||||
this->pm_4_0_sensor_->publish_state(pm_4_0);
|
||||
if (this->pm_10_0_sensor_ != nullptr)
|
||||
this->pm_10_0_sensor_->publish_state(pm_10_0);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
if (this->voc_sensor_ != nullptr)
|
||||
this->voc_sensor_->publish_state(voc);
|
||||
if (this->nox_sensor_ != nullptr)
|
||||
this->nox_sensor_->publish_state(nox);
|
||||
this->status_clear_warning();
|
||||
});
|
||||
}
|
||||
|
||||
bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) {
|
||||
uint16_t params[6];
|
||||
params[0] = tuning.index_offset;
|
||||
params[1] = tuning.learning_time_offset_hours;
|
||||
params[2] = tuning.learning_time_gain_hours;
|
||||
params[3] = tuning.gating_max_duration_minutes;
|
||||
params[4] = tuning.std_initial;
|
||||
params[5] = tuning.gain_factor;
|
||||
auto result = write_command(i2c_command, params, 6);
|
||||
if (!result) {
|
||||
ESP_LOGE(TAG, "set tuning parameters failed. i2c command=%0xX, err=%d", i2c_command, this->last_error_);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool SEN5XComponent::write_temperature_compensation_(const TemperatureCompensation &compensation) {
|
||||
uint16_t params[3];
|
||||
params[0] = compensation.offset;
|
||||
params[1] = compensation.normalized_offset_slope;
|
||||
params[2] = compensation.time_constant;
|
||||
if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) {
|
||||
ESP_LOGE(TAG, "set temperature_compensation failed. Err=%d", this->last_error_);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SEN5XComponent::start_fan_cleaning() {
|
||||
if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_);
|
||||
return false;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Fan auto clean started");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace sen5x
|
||||
} // namespace esphome
|
||||
128
esphome/components/sen5x/sen5x.h
Normal file
128
esphome/components/sen5x/sen5x.h
Normal file
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sen5x {
|
||||
|
||||
enum ERRORCODE {
|
||||
COMMUNICATION_FAILED,
|
||||
SERIAL_NUMBER_IDENTIFICATION_FAILED,
|
||||
MEASUREMENT_INIT_FAILED,
|
||||
PRODUCT_NAME_FAILED,
|
||||
FIRMWARE_FAILED,
|
||||
UNKNOWN
|
||||
};
|
||||
|
||||
// Shortest time interval of 3H for storing baseline values.
|
||||
// Prevents wear of the flash because of too many write operations
|
||||
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
|
||||
// Store anyway if the baseline difference exceeds the max storage diff value
|
||||
const uint32_t MAXIMUM_STORAGE_DIFF = 50;
|
||||
|
||||
struct Sen5xBaselines {
|
||||
int32_t state0;
|
||||
int32_t state1;
|
||||
} PACKED; // NOLINT
|
||||
|
||||
enum RhtAccelerationMode : uint16_t { LOW_ACCELERATION = 0, MEDIUM_ACCELERATION = 1, HIGH_ACCELERATION = 2 };
|
||||
|
||||
struct GasTuning {
|
||||
uint16_t index_offset;
|
||||
uint16_t learning_time_offset_hours;
|
||||
uint16_t learning_time_gain_hours;
|
||||
uint16_t gating_max_duration_minutes;
|
||||
uint16_t std_initial;
|
||||
uint16_t gain_factor;
|
||||
};
|
||||
|
||||
struct TemperatureCompensation {
|
||||
uint16_t offset;
|
||||
uint16_t normalized_offset_slope;
|
||||
uint16_t time_constant;
|
||||
};
|
||||
|
||||
class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
public:
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN };
|
||||
|
||||
void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; }
|
||||
void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; }
|
||||
void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; }
|
||||
void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; }
|
||||
|
||||
void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; }
|
||||
void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
|
||||
void set_acceleration_mode(RhtAccelerationMode mode) { acceleration_mode_ = mode; }
|
||||
void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { auto_cleaning_interval_ = auto_cleaning_interval; }
|
||||
void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
|
||||
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
|
||||
uint16_t std_initial, uint16_t gain_factor) {
|
||||
voc_tuning_params_.value().index_offset = index_offset;
|
||||
voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
|
||||
voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
|
||||
voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
|
||||
voc_tuning_params_.value().std_initial = std_initial;
|
||||
voc_tuning_params_.value().gain_factor = gain_factor;
|
||||
}
|
||||
void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
|
||||
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
|
||||
uint16_t gain_factor) {
|
||||
nox_tuning_params_.value().index_offset = index_offset;
|
||||
nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
|
||||
nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
|
||||
nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
|
||||
nox_tuning_params_.value().std_initial = 50;
|
||||
nox_tuning_params_.value().gain_factor = gain_factor;
|
||||
}
|
||||
void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) {
|
||||
temperature_compensation_.value().offset = offset * 200;
|
||||
temperature_compensation_.value().normalized_offset_slope = normalized_offset_slope * 100;
|
||||
temperature_compensation_.value().time_constant = time_constant;
|
||||
}
|
||||
bool start_fan_cleaning();
|
||||
|
||||
protected:
|
||||
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
|
||||
bool write_temperature_compensation_(const TemperatureCompensation &compensation);
|
||||
ERRORCODE error_code_;
|
||||
bool initialized_{false};
|
||||
sensor::Sensor *pm_1_0_sensor_{nullptr};
|
||||
sensor::Sensor *pm_2_5_sensor_{nullptr};
|
||||
sensor::Sensor *pm_4_0_sensor_{nullptr};
|
||||
sensor::Sensor *pm_10_0_sensor_{nullptr};
|
||||
// SEN54 and SEN55 only
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
sensor::Sensor *voc_sensor_{nullptr};
|
||||
// SEN55 only
|
||||
sensor::Sensor *nox_sensor_{nullptr};
|
||||
|
||||
std::string product_name_;
|
||||
uint8_t serial_number_[4];
|
||||
uint16_t firmware_version_;
|
||||
Sen5xBaselines voc_baselines_storage_;
|
||||
bool store_baseline_;
|
||||
uint32_t seconds_since_last_store_;
|
||||
ESPPreferenceObject pref_;
|
||||
optional<RhtAccelerationMode> acceleration_mode_;
|
||||
optional<uint32_t> auto_cleaning_interval_;
|
||||
optional<GasTuning> voc_tuning_params_;
|
||||
optional<GasTuning> nox_tuning_params_;
|
||||
optional<TemperatureCompensation> temperature_compensation_;
|
||||
};
|
||||
|
||||
} // namespace sen5x
|
||||
} // namespace esphome
|
||||
241
esphome/components/sen5x/sensor.py
Normal file
241
esphome/components/sen5x/sensor.py
Normal file
@@ -0,0 +1,241 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor, sensirion_common
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_OFFSET,
|
||||
CONF_PM_1_0,
|
||||
CONF_PM_10_0,
|
||||
CONF_PM_2_5,
|
||||
CONF_PM_4_0,
|
||||
CONF_STORE_BASELINE,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_NITROUS_OXIDE,
|
||||
DEVICE_CLASS_PM1,
|
||||
DEVICE_CLASS_PM10,
|
||||
DEVICE_CLASS_PM25,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
ICON_CHEMICAL_WEAPON,
|
||||
ICON_RADIATOR,
|
||||
ICON_THERMOMETER,
|
||||
ICON_WATER_PERCENT,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@martgras"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
sen5x_ns = cg.esphome_ns.namespace("sen5x")
|
||||
SEN5XComponent = sen5x_ns.class_(
|
||||
"SEN5XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
|
||||
)
|
||||
RhtAccelerationMode = sen5x_ns.enum("RhtAccelerationMode")
|
||||
|
||||
CONF_ACCELERATION_MODE = "acceleration_mode"
|
||||
CONF_ALGORITHM_TUNING = "algorithm_tuning"
|
||||
CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval"
|
||||
CONF_GAIN_FACTOR = "gain_factor"
|
||||
CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes"
|
||||
CONF_INDEX_OFFSET = "index_offset"
|
||||
CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours"
|
||||
CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
|
||||
CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope"
|
||||
CONF_NOX = "nox"
|
||||
CONF_STD_INITIAL = "std_initial"
|
||||
CONF_TEMPERATURE_COMPENSATION = "temperature_compensation"
|
||||
CONF_TIME_CONSTANT = "time_constant"
|
||||
CONF_VOC = "voc"
|
||||
CONF_VOC_BASELINE = "voc_baseline"
|
||||
|
||||
|
||||
# Actions
|
||||
StartFanAction = sen5x_ns.class_("StartFanAction", automation.Action)
|
||||
|
||||
ACCELERATION_MODES = {
|
||||
"low": RhtAccelerationMode.LOW_ACCELERATION,
|
||||
"medium": RhtAccelerationMode.MEDIUM_ACCELERATION,
|
||||
"high": RhtAccelerationMode.HIGH_ACCELERATION,
|
||||
}
|
||||
|
||||
GAS_SENSOR = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_range(1, 250),
|
||||
cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_range(
|
||||
1, 1000
|
||||
),
|
||||
cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_range(
|
||||
1, 1000
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_GATING_MAX_DURATION_MINUTES, default=720
|
||||
): cv.int_range(0, 3000),
|
||||
cv.Optional(CONF_STD_INITIAL, default=50): cv.int_,
|
||||
cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_range(1, 1000),
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SEN5XComponent),
|
||||
cv.Optional(CONF_PM_1_0): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_PM1,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_4_0): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_PM10,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.time_period_in_seconds_,
|
||||
cv.Optional(CONF_VOC): sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(GAS_SENSOR),
|
||||
cv.Optional(CONF_NOX): sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_NITROUS_OXIDE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(GAS_SENSOR),
|
||||
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
|
||||
cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
icon=ICON_WATER_PERCENT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE_COMPENSATION): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_OFFSET, default=0): cv.float_,
|
||||
cv.Optional(CONF_NORMALIZED_OFFSET_SLOPE, default=0): cv.percentage,
|
||||
cv.Optional(CONF_TIME_CONSTANT, default=0): cv.int_,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ACCELERATION_MODE): cv.enum(ACCELERATION_MODES),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x69))
|
||||
)
|
||||
|
||||
SENSOR_MAP = {
|
||||
CONF_PM_1_0: "set_pm_1_0_sensor",
|
||||
CONF_PM_2_5: "set_pm_2_5_sensor",
|
||||
CONF_PM_4_0: "set_pm_4_0_sensor",
|
||||
CONF_PM_10_0: "set_pm_10_0_sensor",
|
||||
CONF_VOC: "set_voc_sensor",
|
||||
CONF_NOX: "set_nox_sensor",
|
||||
CONF_TEMPERATURE: "set_temperature_sensor",
|
||||
CONF_HUMIDITY: "set_humidity_sensor",
|
||||
}
|
||||
|
||||
SETTING_MAP = {
|
||||
CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval",
|
||||
CONF_ACCELERATION_MODE: "set_acceleration_mode",
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
for key, funcName in SETTING_MAP.items():
|
||||
if key in config:
|
||||
cg.add(getattr(var, funcName)(config[key]))
|
||||
|
||||
for key, funcName in SENSOR_MAP.items():
|
||||
if key in config:
|
||||
sens = await sensor.new_sensor(config[key])
|
||||
cg.add(getattr(var, funcName)(sens))
|
||||
|
||||
if CONF_VOC in config and CONF_ALGORITHM_TUNING in config[CONF_VOC]:
|
||||
cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING]
|
||||
cg.add(
|
||||
var.set_voc_algorithm_tuning(
|
||||
cfg[CONF_INDEX_OFFSET],
|
||||
cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
|
||||
cfg[CONF_LEARNING_TIME_GAIN_HOURS],
|
||||
cfg[CONF_GATING_MAX_DURATION_MINUTES],
|
||||
cfg[CONF_STD_INITIAL],
|
||||
cfg[CONF_GAIN_FACTOR],
|
||||
)
|
||||
)
|
||||
if CONF_NOX in config and CONF_ALGORITHM_TUNING in config[CONF_NOX]:
|
||||
cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING]
|
||||
cg.add(
|
||||
var.set_nox_algorithm_tuning(
|
||||
cfg[CONF_INDEX_OFFSET],
|
||||
cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
|
||||
cfg[CONF_LEARNING_TIME_GAIN_HOURS],
|
||||
cfg[CONF_GATING_MAX_DURATION_MINUTES],
|
||||
cfg[CONF_GAIN_FACTOR],
|
||||
)
|
||||
)
|
||||
if CONF_TEMPERATURE_COMPENSATION in config:
|
||||
cg.add(
|
||||
var.set_temperature_compensation(
|
||||
config[CONF_TEMPERATURE_COMPENSATION][CONF_OFFSET],
|
||||
config[CONF_TEMPERATURE_COMPENSATION][CONF_NORMALIZED_OFFSET_SLOPE],
|
||||
config[CONF_TEMPERATURE_COMPENSATION][CONF_TIME_CONSTANT],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
SEN5X_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(SEN5XComponent),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"sen5x.start_fan_autoclean", StartFanAction, SEN5X_ACTION_SCHEMA
|
||||
)
|
||||
async def sen54_fan_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
10
esphome/components/sensirion_common/__init__.py
Normal file
10
esphome/components/sensirion_common/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import esphome.codegen as cg
|
||||
|
||||
from esphome.components import i2c
|
||||
|
||||
|
||||
CODEOWNERS = ["@martgras"]
|
||||
|
||||
sensirion_common_ns = cg.esphome_ns.namespace("sensirion_common")
|
||||
|
||||
SensirionI2CDevice = sensirion_common_ns.class_("SensirionI2CDevice", i2c.I2CDevice)
|
||||
128
esphome/components/sensirion_common/i2c_sensirion.cpp
Normal file
128
esphome/components/sensirion_common/i2c_sensirion.cpp
Normal file
@@ -0,0 +1,128 @@
|
||||
#include "i2c_sensirion.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
namespace sensirion_common {
|
||||
|
||||
static const char *const TAG = "sensirion_i2c";
|
||||
// To avoid memory allocations for small writes a stack buffer is used
|
||||
static const size_t BUFFER_STACK_SIZE = 16;
|
||||
|
||||
bool SensirionI2CDevice::read_data(uint16_t *data, uint8_t len) {
|
||||
const uint8_t num_bytes = len * 3;
|
||||
std::vector<uint8_t> buf(num_bytes);
|
||||
|
||||
last_error_ = this->read(buf.data(), num_bytes);
|
||||
if (last_error_ != i2c::ERROR_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
const uint8_t j = 3 * i;
|
||||
uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
|
||||
if (crc != buf[j + 2]) {
|
||||
ESP_LOGE(TAG, "CRC8 Checksum invalid at pos %d! 0x%02X != 0x%02X", i, buf[j + 2], crc);
|
||||
last_error_ = i2c::ERROR_CRC;
|
||||
return false;
|
||||
}
|
||||
data[i] = encode_uint16(buf[j], buf[j + 1]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/***
|
||||
* write command with parameters and insert crc
|
||||
* use stack array for less than 4 paramaters. 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) {
|
||||
uint8_t temp_stack[BUFFER_STACK_SIZE];
|
||||
std::unique_ptr<uint8_t[]> temp_heap;
|
||||
uint8_t *temp;
|
||||
size_t required_buffer_len = data_len * 3 + 2;
|
||||
|
||||
// Is a dynamic allocation required ?
|
||||
if (required_buffer_len >= BUFFER_STACK_SIZE) {
|
||||
temp_heap = std::unique_ptr<uint8_t[]>(new uint8_t[required_buffer_len]);
|
||||
temp = temp_heap.get();
|
||||
} else {
|
||||
temp = temp_stack;
|
||||
}
|
||||
// First byte or word is the command
|
||||
uint8_t raw_idx = 0;
|
||||
if (command_len == 1) {
|
||||
temp[raw_idx++] = command & 0xFF;
|
||||
} else {
|
||||
// command is 2 bytes
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
temp[raw_idx++] = command >> 8;
|
||||
temp[raw_idx++] = command & 0xFF;
|
||||
#else
|
||||
temp[raw_idx++] = command & 0xFF;
|
||||
temp[raw_idx++] = command >> 8;
|
||||
#endif
|
||||
}
|
||||
// add parameters folllowed by crc
|
||||
// skipped if len == 0
|
||||
for (size_t i = 0; i < data_len; i++) {
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
temp[raw_idx++] = data[i] >> 8;
|
||||
temp[raw_idx++] = data[i] & 0xFF;
|
||||
#else
|
||||
temp[raw_idx++] = data[i] & 0xFF;
|
||||
temp[raw_idx++] = data[i] >> 8;
|
||||
#endif
|
||||
temp[raw_idx++] = sht_crc_(data[i]);
|
||||
}
|
||||
last_error_ = this->write(temp, raw_idx);
|
||||
return last_error_ == i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
bool SensirionI2CDevice::get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len,
|
||||
uint8_t delay_ms) {
|
||||
if (!this->write_command_(reg, command_len, nullptr, 0)) {
|
||||
ESP_LOGE(TAG, "Failed to write i2c register=0x%X (%d) err=%d,", reg, command_len, this->last_error_);
|
||||
return false;
|
||||
}
|
||||
delay(delay_ms);
|
||||
bool result = this->read_data(data, len);
|
||||
if (!result) {
|
||||
ESP_LOGE(TAG, "Failed to read data from register=0x%X err=%d,", reg, this->last_error_);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// The 8-bit CRC checksum is transmitted after each data word
|
||||
uint8_t SensirionI2CDevice::sht_crc_(uint16_t data) {
|
||||
uint8_t bit;
|
||||
uint8_t crc = 0xFF;
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
crc ^= data >> 8;
|
||||
#else
|
||||
crc ^= data & 0xFF;
|
||||
#endif
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ crc_polynomial_;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
crc ^= data & 0xFF;
|
||||
#else
|
||||
crc ^= data >> 8;
|
||||
#endif
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ crc_polynomial_;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
} // namespace sensirion_common
|
||||
} // namespace esphome
|
||||
155
esphome/components/sensirion_common/i2c_sensirion.h
Normal file
155
esphome/components/sensirion_common/i2c_sensirion.h
Normal file
@@ -0,0 +1,155 @@
|
||||
#pragma once
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sensirion_common {
|
||||
|
||||
/**
|
||||
* Implementation of a i2c functions for Sensirion sensors
|
||||
* Sensirion data requires crc checking.
|
||||
* Each 16 bit word is/must be followed 8 bit CRC code
|
||||
* (Applies to read and write - note the i2c command code doesn't need a CRC)
|
||||
* Format:
|
||||
* | 16 Bit Command Code | 16 bit Data word 1 | CRC of DW 1 | 16 bit Data word 1 | CRC of DW 2 | ..
|
||||
*/
|
||||
class SensirionI2CDevice : public i2c::I2CDevice {
|
||||
public:
|
||||
enum CommandLen : uint8_t { ADDR_8_BIT = 1, ADDR_16_BIT = 2 };
|
||||
|
||||
/** Read data words from i2c device.
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
bool read_data(uint16_t &data) { return this->read_data(&data, 1); }
|
||||
|
||||
/** get data words from i2c register.
|
||||
* handles crc check used by Sensirion sensors
|
||||
* @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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
/** 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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/** get data words from i2c register.
|
||||
* handles crc check used by Sensirion sensors
|
||||
* @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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/** 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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/** Write a command to the i2c device.
|
||||
* @param command i2c command to send
|
||||
* @return true if reading succeded
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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());
|
||||
}
|
||||
|
||||
/** Write a command with arguments as words
|
||||
* @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
|
||||
*/
|
||||
template<class T> bool write_command(T i2c_register, const uint16_t *data, uint8_t len) {
|
||||
// limit to 8 or 16 bit only
|
||||
static_assert(sizeof(i2c_register) == 1 || sizeof(i2c_register) == 2,
|
||||
"only 8 or 16 bit command types are supported.");
|
||||
return write_command_(i2c_register, CommandLen(sizeof(T)), data, len);
|
||||
}
|
||||
|
||||
protected:
|
||||
uint8_t crc_polynomial_{0x31u}; // default for sensirion
|
||||
/** Write a command with arguments as words
|
||||
* @param command i2c command to send can be uint8_t or uint16_t
|
||||
* @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
|
||||
*/
|
||||
bool write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, uint8_t data_len);
|
||||
|
||||
/** get data words from i2c register.
|
||||
* handles crc check used by Sensirion sensors
|
||||
* @param i2c register
|
||||
* @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
|
||||
*/
|
||||
bool get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, uint8_t delay);
|
||||
|
||||
/** 8-bit CRC checksum that is transmitted after each data word for read and write operation
|
||||
* @param command i2c command to send
|
||||
* @param data data word for which the crc8 checksum is calculated
|
||||
* @param len number of arguments (words)
|
||||
* @return 8 Bit CRC
|
||||
*/
|
||||
uint8_t sht_crc_(uint16_t data);
|
||||
|
||||
/** 8-bit CRC checksum that is transmitted after each data word for read and write operation
|
||||
* @param command i2c command to send
|
||||
* @param data1 high byte of data word
|
||||
* @param data2 low byte of data word
|
||||
* @return 8 Bit CRC
|
||||
*/
|
||||
uint8_t sht_crc_(uint8_t data1, uint8_t data2) { return sht_crc_(encode_uint16(data1, data2)); }
|
||||
|
||||
/** last error code from i2c operation
|
||||
*/
|
||||
i2c::ErrorCode last_error_;
|
||||
};
|
||||
|
||||
} // namespace sensirion_common
|
||||
} // namespace esphome
|
||||
@@ -29,6 +29,7 @@ from esphome.const import (
|
||||
CONF_WINDOW_SIZE,
|
||||
CONF_MQTT_ID,
|
||||
CONF_FORCE_UPDATE,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
@@ -70,6 +71,7 @@ DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_CARBON_MONOXIDE,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_GAS,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
@@ -212,8 +214,8 @@ SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
|
||||
cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger),
|
||||
cv.Optional(CONF_ABOVE): cv.float_,
|
||||
cv.Optional(CONF_BELOW): cv.float_,
|
||||
cv.Optional(CONF_ABOVE): cv.templatable(cv.float_),
|
||||
cv.Optional(CONF_BELOW): cv.templatable(cv.float_),
|
||||
},
|
||||
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
|
||||
),
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components import i2c, sensor, sensirion_common
|
||||
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_BASELINE,
|
||||
CONF_ECO2,
|
||||
CONF_STORE_BASELINE,
|
||||
CONF_TEMPERATURE_SOURCE,
|
||||
CONF_TVOC,
|
||||
ICON_RADIATOR,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
@@ -13,20 +16,23 @@ from esphome.const import (
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PARTS_PER_BILLION,
|
||||
ICON_MOLECULE_CO2,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
sgp30_ns = cg.esphome_ns.namespace("sgp30")
|
||||
SGP30Component = sgp30_ns.class_("SGP30Component", cg.PollingComponent, i2c.I2CDevice)
|
||||
SGP30Component = sgp30_ns.class_(
|
||||
"SGP30Component", cg.PollingComponent, sensirion_common.SensirionI2CDevice
|
||||
)
|
||||
|
||||
CONF_ECO2_BASELINE = "eco2_baseline"
|
||||
CONF_TVOC_BASELINE = "tvoc_baseline"
|
||||
CONF_STORE_BASELINE = "store_baseline"
|
||||
CONF_UPTIME = "uptime"
|
||||
CONF_COMPENSATION = "compensation"
|
||||
CONF_HUMIDITY_SOURCE = "humidity_source"
|
||||
CONF_TEMPERATURE_SOURCE = "temperature_source"
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
@@ -49,10 +55,12 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema(
|
||||
icon=ICON_MOLECULE_CO2,
|
||||
accuracy_decimals=0,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_TVOC_BASELINE): sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
|
||||
cv.Optional(CONF_BASELINE): cv.Schema(
|
||||
|
||||
@@ -36,14 +36,8 @@ void SGP30Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up SGP30...");
|
||||
|
||||
// Serial Number identification
|
||||
if (!this->write_command_(SGP30_CMD_GET_SERIAL_ID)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint16_t raw_serial_number[3];
|
||||
|
||||
if (!this->read_data_(raw_serial_number, 3)) {
|
||||
if (!this->get_register(SGP30_CMD_GET_SERIAL_ID, raw_serial_number, 3)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -52,16 +46,12 @@ void SGP30Component::setup() {
|
||||
ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_);
|
||||
|
||||
// Featureset identification for future use
|
||||
if (!this->write_command_(SGP30_CMD_GET_FEATURESET)) {
|
||||
uint16_t raw_featureset;
|
||||
if (!this->get_register(SGP30_CMD_GET_FEATURESET, raw_featureset)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint16_t raw_featureset[1];
|
||||
if (!this->read_data_(raw_featureset, 1)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->featureset_ = raw_featureset[0];
|
||||
this->featureset_ = raw_featureset;
|
||||
if (uint16_t(this->featureset_ >> 12) != 0x0) {
|
||||
if (uint16_t(this->featureset_ >> 12) == 0x1) {
|
||||
// ID matching a different sensor: SGPC3
|
||||
@@ -76,7 +66,7 @@ void SGP30Component::setup() {
|
||||
ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF));
|
||||
|
||||
// Sensor initialization
|
||||
if (!this->write_command_(SGP30_CMD_IAQ_INIT)) {
|
||||
if (!this->write_command(SGP30_CMD_IAQ_INIT)) {
|
||||
ESP_LOGE(TAG, "Sensor sgp30_iaq_init failed.");
|
||||
this->error_code_ = MEASUREMENT_INIT_FAILED;
|
||||
this->mark_failed();
|
||||
@@ -119,14 +109,14 @@ bool SGP30Component::is_sensor_baseline_reliable_() {
|
||||
|
||||
void SGP30Component::read_iaq_baseline_() {
|
||||
if (this->is_sensor_baseline_reliable_()) {
|
||||
if (!this->write_command_(SGP30_CMD_GET_IAQ_BASELINE)) {
|
||||
if (!this->write_command(SGP30_CMD_GET_IAQ_BASELINE)) {
|
||||
ESP_LOGD(TAG, "Error getting baseline");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
this->set_timeout(50, [this]() {
|
||||
uint16_t raw_data[2];
|
||||
if (!this->read_data_(raw_data, 2)) {
|
||||
if (!this->read_data(raw_data, 2)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
@@ -274,14 +264,14 @@ void SGP30Component::dump_config() {
|
||||
}
|
||||
|
||||
void SGP30Component::update() {
|
||||
if (!this->write_command_(SGP30_CMD_MEASURE_IAQ)) {
|
||||
if (!this->write_command(SGP30_CMD_MEASURE_IAQ)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
this->seconds_since_last_store_ += this->update_interval_ / 1000;
|
||||
this->set_timeout(50, [this]() {
|
||||
uint16_t raw_data[2];
|
||||
if (!this->read_data_(raw_data, 2)) {
|
||||
if (!this->read_data(raw_data, 2)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
@@ -305,56 +295,5 @@ void SGP30Component::update() {
|
||||
});
|
||||
}
|
||||
|
||||
bool SGP30Component::write_command_(uint16_t command) {
|
||||
// Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
|
||||
return this->write_byte(command >> 8, command & 0xFF);
|
||||
}
|
||||
|
||||
uint8_t SGP30Component::sht_crc_(uint8_t data1, uint8_t data2) {
|
||||
uint8_t bit;
|
||||
uint8_t crc = 0xFF;
|
||||
|
||||
crc ^= data1;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
crc ^= data2;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool SGP30Component::read_data_(uint16_t *data, uint8_t len) {
|
||||
const uint8_t num_bytes = len * 3;
|
||||
std::vector<uint8_t> buf(num_bytes);
|
||||
|
||||
if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
const uint8_t j = 3 * i;
|
||||
uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
|
||||
if (crc != buf[j + 2]) {
|
||||
ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
|
||||
return false;
|
||||
}
|
||||
data[i] = (buf[j] << 8) | buf[j + 1];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace sgp30
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include <cmath>
|
||||
|
||||
@@ -15,7 +15,7 @@ struct SGP30Baselines {
|
||||
} PACKED;
|
||||
|
||||
/// This class implements support for the Sensirion SGP30 i2c GAS (VOC and CO2eq) sensors.
|
||||
class SGP30Component : public PollingComponent, public i2c::I2CDevice {
|
||||
class SGP30Component : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
public:
|
||||
void set_eco2_sensor(sensor::Sensor *eco2) { eco2_sensor_ = eco2; }
|
||||
void set_tvoc_sensor(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
|
||||
@@ -33,13 +33,10 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice {
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
protected:
|
||||
bool write_command_(uint16_t command);
|
||||
bool read_data_(uint16_t *data, uint8_t len);
|
||||
void send_env_data_();
|
||||
void read_iaq_baseline_();
|
||||
bool is_sensor_baseline_reliable_();
|
||||
void write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline);
|
||||
uint8_t sht_crc_(uint8_t data1, uint8_t data2);
|
||||
uint64_t serial_number_;
|
||||
uint16_t featureset_;
|
||||
uint32_t required_warm_up_time_;
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components import i2c, sensor, sensirion_common
|
||||
|
||||
from esphome.const import (
|
||||
CONF_STORE_BASELINE,
|
||||
CONF_TEMPERATURE_SOURCE,
|
||||
ICON_RADIATOR,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
CODEOWNERS = ["@SenexCrenshaw"]
|
||||
|
||||
sgp40_ns = cg.esphome_ns.namespace("sgp40")
|
||||
SGP40Component = sgp40_ns.class_(
|
||||
"SGP40Component", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
|
||||
"SGP40Component",
|
||||
sensor.Sensor,
|
||||
cg.PollingComponent,
|
||||
sensirion_common.SensirionI2CDevice,
|
||||
)
|
||||
|
||||
CONF_COMPENSATION = "compensation"
|
||||
CONF_HUMIDITY_SOURCE = "humidity_source"
|
||||
CONF_TEMPERATURE_SOURCE = "temperature_source"
|
||||
CONF_STORE_BASELINE = "store_baseline"
|
||||
CONF_VOC_BASELINE = "voc_baseline"
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
@@ -12,14 +12,14 @@ void SGP40Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up SGP40...");
|
||||
|
||||
// Serial Number identification
|
||||
if (!this->write_command_(SGP40_CMD_GET_SERIAL_ID)) {
|
||||
if (!this->write_command(SGP40_CMD_GET_SERIAL_ID)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint16_t raw_serial_number[3];
|
||||
|
||||
if (!this->read_data_(raw_serial_number, 3)) {
|
||||
if (!this->read_data(raw_serial_number, 3)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -28,19 +28,19 @@ void SGP40Component::setup() {
|
||||
ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_);
|
||||
|
||||
// Featureset identification for future use
|
||||
if (!this->write_command_(SGP40_CMD_GET_FEATURESET)) {
|
||||
if (!this->write_command(SGP40_CMD_GET_FEATURESET)) {
|
||||
ESP_LOGD(TAG, "raw_featureset write_command_ failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint16_t raw_featureset[1];
|
||||
if (!this->read_data_(raw_featureset, 1)) {
|
||||
uint16_t raw_featureset;
|
||||
if (!this->read_data(raw_featureset)) {
|
||||
ESP_LOGD(TAG, "raw_featureset read_data_ failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->featureset_ = raw_featureset[0];
|
||||
this->featureset_ = raw_featureset;
|
||||
if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) {
|
||||
ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF),
|
||||
SGP40_FEATURESET);
|
||||
@@ -95,21 +95,21 @@ void SGP40Component::setup() {
|
||||
|
||||
void SGP40Component::self_test_() {
|
||||
ESP_LOGD(TAG, "Self-test started");
|
||||
if (!this->write_command_(SGP40_CMD_SELF_TEST)) {
|
||||
if (!this->write_command(SGP40_CMD_SELF_TEST)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
ESP_LOGD(TAG, "Self-test communication failed");
|
||||
this->mark_failed();
|
||||
}
|
||||
|
||||
this->set_timeout(250, [this]() {
|
||||
uint16_t reply[1];
|
||||
if (!this->read_data_(reply, 1)) {
|
||||
uint16_t reply;
|
||||
if (!this->read_data(reply)) {
|
||||
ESP_LOGD(TAG, "Self-test read_data_ failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply[0] == 0xD400) {
|
||||
if (reply == 0xD400) {
|
||||
this->self_test_complete_ = true;
|
||||
ESP_LOGD(TAG, "Self-test completed");
|
||||
return;
|
||||
@@ -192,51 +192,28 @@ uint16_t SGP40Component::measure_raw_() {
|
||||
temperature = 25;
|
||||
}
|
||||
|
||||
uint8_t command[8];
|
||||
|
||||
command[0] = 0x26;
|
||||
command[1] = 0x0F;
|
||||
|
||||
uint16_t data[2];
|
||||
uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100));
|
||||
command[2] = rhticks >> 8;
|
||||
command[3] = rhticks & 0xFF;
|
||||
command[4] = generate_crc_(command + 2, 2);
|
||||
uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175);
|
||||
command[5] = tempticks >> 8;
|
||||
command[6] = tempticks & 0xFF;
|
||||
command[7] = generate_crc_(command + 5, 2);
|
||||
// first paramater is the relative humidity ticks
|
||||
data[0] = rhticks;
|
||||
// second paramater is the temperature ticks
|
||||
data[1] = tempticks;
|
||||
|
||||
if (this->write(command, 8) != i2c::ERROR_OK) {
|
||||
if (!this->write_command(SGP40_CMD_MEASURE_RAW, data, 2)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "write error");
|
||||
return UINT16_MAX;
|
||||
ESP_LOGD(TAG, "write error (%d)", this->last_error_);
|
||||
return false;
|
||||
}
|
||||
delay(30);
|
||||
uint16_t raw_data[1];
|
||||
|
||||
if (!this->read_data_(raw_data, 1)) {
|
||||
uint16_t raw_data;
|
||||
if (!this->read_data(raw_data)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "read_data_ error");
|
||||
return UINT16_MAX;
|
||||
}
|
||||
return raw_data[0];
|
||||
}
|
||||
|
||||
uint8_t SGP40Component::generate_crc_(const uint8_t *data, uint8_t datalen) {
|
||||
// calculates 8-Bit checksum with given polynomial
|
||||
uint8_t crc = SGP40_CRC8_INIT;
|
||||
|
||||
for (uint8_t i = 0; i < datalen; i++) {
|
||||
crc ^= data[i];
|
||||
for (uint8_t b = 0; b < 8; b++) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ SGP40_CRC8_POLYNOMIAL;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
return raw_data;
|
||||
}
|
||||
|
||||
void SGP40Component::update_voc_index() {
|
||||
@@ -293,56 +270,5 @@ void SGP40Component::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
bool SGP40Component::write_command_(uint16_t command) {
|
||||
// Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
|
||||
return this->write_byte(command >> 8, command & 0xFF);
|
||||
}
|
||||
|
||||
uint8_t SGP40Component::sht_crc_(uint8_t data1, uint8_t data2) {
|
||||
uint8_t bit;
|
||||
uint8_t crc = 0xFF;
|
||||
|
||||
crc ^= data1;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
crc ^= data2;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool SGP40Component::read_data_(uint16_t *data, uint8_t len) {
|
||||
const uint8_t num_bytes = len * 3;
|
||||
std::vector<uint8_t> buf(num_bytes);
|
||||
|
||||
if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
const uint8_t j = 3 * i;
|
||||
uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
|
||||
if (crc != buf[j + 2]) {
|
||||
ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
|
||||
return false;
|
||||
}
|
||||
data[i] = (buf[j] << 8) | buf[j + 1];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace sgp40
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "sensirion_voc_algorithm.h"
|
||||
@@ -28,6 +28,7 @@ static const uint8_t SGP40_WORD_LEN = 2; ///< 2 bytes per word
|
||||
static const uint16_t SGP40_CMD_GET_SERIAL_ID = 0x3682;
|
||||
static const uint16_t SGP40_CMD_GET_FEATURESET = 0x202f;
|
||||
static const uint16_t SGP40_CMD_SELF_TEST = 0x280e;
|
||||
static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F;
|
||||
|
||||
// Shortest time interval of 3H for storing baseline values.
|
||||
// Prevents wear of the flash because of too many write operations
|
||||
@@ -39,7 +40,7 @@ const uint32_t MAXIMUM_STORAGE_DIFF = 50;
|
||||
class SGP40Component;
|
||||
|
||||
/// This class implements support for the Sensirion sgp40 i2c GAS (VOC) sensors.
|
||||
class SGP40Component : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice {
|
||||
class SGP40Component : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice {
|
||||
public:
|
||||
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
|
||||
void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
|
||||
@@ -55,11 +56,8 @@ class SGP40Component : public PollingComponent, public sensor::Sensor, public i2
|
||||
/// Input sensor for humidity and temperature compensation.
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
bool write_command_(uint16_t command);
|
||||
bool read_data_(uint16_t *data, uint8_t len);
|
||||
int16_t sensirion_init_sensors_();
|
||||
int16_t sgp40_probe_();
|
||||
uint8_t sht_crc_(uint8_t data1, uint8_t data2);
|
||||
uint64_t serial_number_;
|
||||
uint16_t featureset_;
|
||||
int32_t measure_voc_index_();
|
||||
|
||||
1
esphome/components/shelly_dimmer/__init__.py
Normal file
1
esphome/components/shelly_dimmer/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@rnauber", "@edge90"]
|
||||
158
esphome/components/shelly_dimmer/dev_table.h
Normal file
158
esphome/components/shelly_dimmer/dev_table.h
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
stm32flash - Open Source ST STM32 flash program for Arduino
|
||||
Copyright (C) 2010 Geoffrey McRae <geoff@spacevs.com>
|
||||
Copyright (C) 2014-2015 Antonio Borneo <borneo.antonio@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
#include "stm32flash.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace shelly_dimmer {
|
||||
|
||||
constexpr uint32_t SZ_128 = 0x00000080;
|
||||
constexpr uint32_t SZ_256 = 0x00000100;
|
||||
constexpr uint32_t SZ_1K = 0x00000400;
|
||||
constexpr uint32_t SZ_2K = 0x00000800;
|
||||
constexpr uint32_t SZ_16K = 0x00004000;
|
||||
constexpr uint32_t SZ_32K = 0x00008000;
|
||||
constexpr uint32_t SZ_64K = 0x00010000;
|
||||
constexpr uint32_t SZ_128K = 0x00020000;
|
||||
constexpr uint32_t SZ_256K = 0x00040000;
|
||||
|
||||
/*
|
||||
* Page-size for page-by-page flash erase.
|
||||
* Arrays are zero terminated; last non-zero value is automatically repeated
|
||||
*/
|
||||
|
||||
/* fixed size pages */
|
||||
constexpr uint32_t p_128[] = {SZ_128, 0}; // NOLINT
|
||||
constexpr uint32_t p_256[] = {SZ_256, 0}; // NOLINT
|
||||
constexpr uint32_t p_1k[] = {SZ_1K, 0}; // NOLINT
|
||||
constexpr uint32_t p_2k[] = {SZ_2K, 0}; // NOLINT
|
||||
/* F2 and F4 page size */
|
||||
constexpr uint32_t f2f4[] = {SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, 0}; // NOLINT
|
||||
/* F4 dual bank page size */
|
||||
constexpr uint32_t f4db[] = {SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, SZ_128K, // NOLINT
|
||||
SZ_128K, SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, 0};
|
||||
/* F7 page size */
|
||||
constexpr uint32_t f7[] = {SZ_32K, SZ_32K, SZ_32K, SZ_32K, SZ_128K, SZ_256K, 0}; // NOLINT
|
||||
|
||||
/*
|
||||
* Device table, corresponds to the "Bootloader device-dependant parameters"
|
||||
* table in ST document AN2606.
|
||||
* Note that the option bytes upper range is inclusive!
|
||||
*/
|
||||
constexpr stm32_dev_t DEVICES[] = {
|
||||
/* ID "name" SRAM-address-range FLASH-address-range PPS PSize
|
||||
Option-byte-addr-range System-mem-addr-range Flags */
|
||||
/* F0 */
|
||||
{0x440, "STM32F030x8/F05xxx", 0x20000800, 0x20002000, 0x08000000, 0x08010000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFEC00, 0x1FFFF800, 0},
|
||||
{0x442, "STM32F030xC/F09xxx", 0x20001800, 0x20008000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFC800, 0x1FFFF800, F_OBLL},
|
||||
{0x444, "STM32F03xx4/6", 0x20000800, 0x20001000, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFEC00, 0x1FFFF800, 0},
|
||||
{0x445, "STM32F04xxx/F070x6", 0x20001800, 0x20001800, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFC400, 0x1FFFF800, 0},
|
||||
{0x448, "STM32F070xB/F071xx/F72xx", 0x20001800, 0x20004000, 0x08000000, 0x08020000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFC800, 0x1FFFF800, 0},
|
||||
/* F1 */
|
||||
{0x412, "STM32F10xxx Low-density", 0x20000200, 0x20002800, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFF000, 0x1FFFF800, 0},
|
||||
{0x410, "STM32F10xxx Medium-density", 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0},
|
||||
{0x414, "STM32F10xxx High-density", 0x20000200, 0x20010000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFF000, 0x1FFFF800, 0},
|
||||
{0x420, "STM32F10xxx Medium-density VL", 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0},
|
||||
{0x428, "STM32F10xxx High-density VL", 0x20000200, 0x20008000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0},
|
||||
{0x418, "STM32F105xx/F107xx", 0x20001000, 0x20010000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFB000, 0x1FFFF800, 0},
|
||||
{0x430, "STM32F10xxx XL-density", 0x20000800, 0x20018000, 0x08000000, 0x08100000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFE000, 0x1FFFF800, 0},
|
||||
/* F2 */
|
||||
{0x411, "STM32F2xxxx", 0x20002000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000,
|
||||
0x1FFF7800, 0},
|
||||
/* F3 */
|
||||
{0x432, "STM32F373xx/F378xx", 0x20001400, 0x20008000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFD800, 0x1FFFF800, 0},
|
||||
{0x422, "STM32F302xB(C)/F303xB(C)/F358xx", 0x20001400, 0x2000A000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0},
|
||||
{0x439, "STM32F301xx/F302x4(6/8)/F318xx", 0x20001800, 0x20004000, 0x08000000, 0x08010000, 2, p_2k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0},
|
||||
{0x438, "STM32F303x4(6/8)/F334xx/F328xx", 0x20001800, 0x20003000, 0x08000000, 0x08010000, 2, p_2k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0},
|
||||
{0x446, "STM32F302xD(E)/F303xD(E)/F398xx", 0x20001800, 0x20010000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800,
|
||||
0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0},
|
||||
/* F4 */
|
||||
{0x413, "STM32F40xxx/41xxx", 0x20003000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F,
|
||||
0x1FFF0000, 0x1FFF7800, 0},
|
||||
{0x419, "STM32F42xxx/43xxx", 0x20003000, 0x20030000, 0x08000000, 0x08200000, 1, f4db, 0x1FFEC000, 0x1FFFC00F,
|
||||
0x1FFF0000, 0x1FFF7800, 0},
|
||||
{0x423, "STM32F401xB(C)", 0x20003000, 0x20010000, 0x08000000, 0x08040000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F,
|
||||
0x1FFF0000, 0x1FFF7800, 0},
|
||||
{0x433, "STM32F401xD(E)", 0x20003000, 0x20018000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F,
|
||||
0x1FFF0000, 0x1FFF7800, 0},
|
||||
{0x458, "STM32F410xx", 0x20003000, 0x20008000, 0x08000000, 0x08020000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000,
|
||||
0x1FFF7800, 0},
|
||||
{0x431, "STM32F411xx", 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000,
|
||||
0x1FFF7800, 0},
|
||||
{0x421, "STM32F446xx", 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000,
|
||||
0x1FFF7800, 0},
|
||||
{0x434, "STM32F469xx", 0x20003000, 0x20060000, 0x08000000, 0x08200000, 1, f4db, 0x1FFEC000, 0x1FFFC00F, 0x1FFF0000,
|
||||
0x1FFF7800, 0},
|
||||
/* F7 */
|
||||
{0x449, "STM32F74xxx/75xxx", 0x20004000, 0x20050000, 0x08000000, 0x08100000, 1, f7, 0x1FFF0000, 0x1FFF001F,
|
||||
0x1FF00000, 0x1FF0EDC0, 0},
|
||||
/* L0 */
|
||||
{0x425, "STM32L031xx/041xx", 0x20001000, 0x20002000, 0x08000000, 0x08008000, 32, p_128, 0x1FF80000, 0x1FF8001F,
|
||||
0x1FF00000, 0x1FF01000, 0},
|
||||
{0x417, "STM32L05xxx/06xxx", 0x20001000, 0x20002000, 0x08000000, 0x08010000, 32, p_128, 0x1FF80000, 0x1FF8001F,
|
||||
0x1FF00000, 0x1FF01000, 0},
|
||||
{0x447, "STM32L07xxx/08xxx", 0x20002000, 0x20005000, 0x08000000, 0x08030000, 32, p_128, 0x1FF80000, 0x1FF8001F,
|
||||
0x1FF00000, 0x1FF02000, 0},
|
||||
/* L1 */
|
||||
{0x416, "STM32L1xxx6(8/B)", 0x20000800, 0x20004000, 0x08000000, 0x08020000, 16, p_256, 0x1FF80000, 0x1FF8001F,
|
||||
0x1FF00000, 0x1FF01000, F_NO_ME},
|
||||
{0x429, "STM32L1xxx6(8/B)A", 0x20001000, 0x20008000, 0x08000000, 0x08020000, 16, p_256, 0x1FF80000, 0x1FF8001F,
|
||||
0x1FF00000, 0x1FF01000, 0},
|
||||
{0x427, "STM32L1xxxC", 0x20001000, 0x20008000, 0x08000000, 0x08040000, 16, p_256, 0x1FF80000, 0x1FF8001F,
|
||||
0x1FF00000, 0x1FF02000, 0},
|
||||
{0x436, "STM32L1xxxD", 0x20001000, 0x2000C000, 0x08000000, 0x08060000, 16, p_256, 0x1FF80000, 0x1FF8009F,
|
||||
0x1FF00000, 0x1FF02000, 0},
|
||||
{0x437, "STM32L1xxxE", 0x20001000, 0x20014000, 0x08000000, 0x08080000, 16, p_256, 0x1FF80000, 0x1FF8009F,
|
||||
0x1FF00000, 0x1FF02000, F_NO_ME},
|
||||
/* L4 */
|
||||
{0x415, "STM32L476xx/486xx", 0x20003100, 0x20018000, 0x08000000, 0x08100000, 1, p_2k, 0x1FFF7800, 0x1FFFF80F,
|
||||
0x1FFF0000, 0x1FFF7000, 0},
|
||||
/* These are not (yet) in AN2606: */
|
||||
{0x641, "Medium_Density PL", 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F,
|
||||
0x1FFFF000, 0x1FFFF800, 0},
|
||||
{0x9a8, "STM32W-128K", 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k, 0x08040800, 0x0804080F, 0x08040000,
|
||||
0x08040800, 0},
|
||||
{0x9b0, "STM32W-256K", 0x20000200, 0x20004000, 0x08000000, 0x08040000, 4, p_2k, 0x08040800, 0x0804080F, 0x08040000,
|
||||
0x08040800, 0},
|
||||
{0x0, "", 0x0, 0x0, 0x0, 0x0, 0x0, nullptr, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
};
|
||||
|
||||
} // namespace shelly_dimmer
|
||||
} // namespace esphome
|
||||
#endif
|
||||
219
esphome/components/shelly_dimmer/light.py
Normal file
219
esphome/components/shelly_dimmer/light.py
Normal file
@@ -0,0 +1,219 @@
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import re
|
||||
import requests
|
||||
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import pins
|
||||
from esphome.components import light, sensor, uart
|
||||
from esphome.const import (
|
||||
CONF_OUTPUT_ID,
|
||||
CONF_GAMMA_CORRECT,
|
||||
CONF_POWER,
|
||||
CONF_VOLTAGE,
|
||||
CONF_CURRENT,
|
||||
CONF_VERSION,
|
||||
CONF_URL,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
UNIT_VOLT,
|
||||
UNIT_AMPERE,
|
||||
UNIT_WATT,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
)
|
||||
from esphome.core import HexInt, CORE
|
||||
|
||||
DOMAIN = "shelly_dimmer"
|
||||
DEPENDENCIES = ["sensor", "uart"]
|
||||
|
||||
shelly_dimmer_ns = cg.esphome_ns.namespace("shelly_dimmer")
|
||||
ShellyDimmer = shelly_dimmer_ns.class_(
|
||||
"ShellyDimmer", light.LightOutput, cg.PollingComponent, uart.UARTDevice
|
||||
)
|
||||
|
||||
CONF_FIRMWARE = "firmware"
|
||||
CONF_SHA256 = "sha256"
|
||||
CONF_UPDATE = "update"
|
||||
|
||||
CONF_LEADING_EDGE = "leading_edge"
|
||||
CONF_WARMUP_BRIGHTNESS = "warmup_brightness"
|
||||
# CONF_WARMUP_TIME = "warmup_time"
|
||||
CONF_MIN_BRIGHTNESS = "min_brightness"
|
||||
CONF_MAX_BRIGHTNESS = "max_brightness"
|
||||
|
||||
CONF_NRST_PIN = "nrst_pin"
|
||||
CONF_BOOT0_PIN = "boot0_pin"
|
||||
|
||||
KNOWN_FIRMWARE = {
|
||||
"51.5": (
|
||||
"https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.5/shelly-dimmer-stm32_v51.5.bin",
|
||||
"553fc1d78ed113227af7683eaa9c26189a961c4ea9a48000fb5aa8f8ac5d7b60",
|
||||
),
|
||||
"51.6": (
|
||||
"https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.6/shelly-dimmer-stm32_v51.6.bin",
|
||||
"eda483e111c914723a33f5088f1397d5c0b19333db4a88dc965636b976c16c36",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def parse_firmware_version(value):
|
||||
match = re.match(r"(\d+).(\d+)", value)
|
||||
if match is None:
|
||||
raise ValueError(f"Not a valid version number {value}")
|
||||
major = int(match[1])
|
||||
minor = int(match[2])
|
||||
return major, minor
|
||||
|
||||
|
||||
def get_firmware(value):
|
||||
if not value[CONF_UPDATE]:
|
||||
return None
|
||||
|
||||
def dl(url):
|
||||
try:
|
||||
req = requests.get(url)
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise cv.Invalid(f"Could not download firmware file ({url}): {e}")
|
||||
|
||||
h = hashlib.new("sha256")
|
||||
h.update(req.content)
|
||||
return req.content, h.hexdigest()
|
||||
|
||||
url = value[CONF_URL]
|
||||
|
||||
if CONF_SHA256 in value: # we have a hash, enable caching
|
||||
path = (
|
||||
Path(CORE.config_dir)
|
||||
/ ".esphome"
|
||||
/ DOMAIN
|
||||
/ (value[CONF_SHA256] + "_fw_stm.bin")
|
||||
)
|
||||
|
||||
if not path.is_file():
|
||||
firmware_data, dl_hash = dl(url)
|
||||
|
||||
if dl_hash != value[CONF_SHA256]:
|
||||
raise cv.Invalid(
|
||||
f"Hash mismatch for {url}: {dl_hash} != {value[CONF_SHA256]}"
|
||||
)
|
||||
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
path.write_bytes(firmware_data)
|
||||
|
||||
else:
|
||||
firmware_data = path.read_bytes()
|
||||
else: # no caching, download every time
|
||||
firmware_data, dl_hash = dl(url)
|
||||
|
||||
return [HexInt(x) for x in firmware_data]
|
||||
|
||||
|
||||
def validate_firmware(value):
|
||||
config = value.copy()
|
||||
if CONF_URL not in config:
|
||||
try:
|
||||
config[CONF_URL], config[CONF_SHA256] = KNOWN_FIRMWARE[config[CONF_VERSION]]
|
||||
except KeyError as e:
|
||||
raise cv.Invalid(
|
||||
f"Firmware {config[CONF_VERSION]} is unknown, please specify an '{CONF_URL}' ..."
|
||||
) from e
|
||||
get_firmware(config)
|
||||
return config
|
||||
|
||||
|
||||
def validate_sha256(value):
|
||||
value = cv.string(value)
|
||||
if not value.isalnum() or not len(value) == 64:
|
||||
raise ValueError(f"Not a valid SHA256 hex string: {value}")
|
||||
return value
|
||||
|
||||
|
||||
def validate_version(value):
|
||||
parse_firmware_version(value)
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ShellyDimmer),
|
||||
cv.Optional(CONF_FIRMWARE, default="51.6"): cv.maybe_simple_value(
|
||||
{
|
||||
cv.Optional(CONF_URL): cv.url,
|
||||
cv.Optional(CONF_SHA256): validate_sha256,
|
||||
cv.Required(CONF_VERSION): validate_version,
|
||||
cv.Optional(CONF_UPDATE, default=False): cv.boolean,
|
||||
},
|
||||
validate_firmware, # converts a simple version key to generate the full url
|
||||
key=CONF_VERSION,
|
||||
),
|
||||
cv.Optional(CONF_NRST_PIN, default="GPIO5"): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_BOOT0_PIN, default="GPIO4"): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_LEADING_EDGE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_WARMUP_BRIGHTNESS, default=100): cv.uint16_t,
|
||||
# cv.Optional(CONF_WARMUP_TIME, default=20): cv.uint16_t,
|
||||
cv.Optional(CONF_MIN_BRIGHTNESS, default=0): cv.uint16_t,
|
||||
cv.Optional(CONF_MAX_BRIGHTNESS, default=1000): cv.uint16_t,
|
||||
cv.Optional(CONF_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
# Change the default gamma_correct setting.
|
||||
cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float,
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("10s"))
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
fw_hex = get_firmware(config[CONF_FIRMWARE])
|
||||
fw_major, fw_minor = parse_firmware_version(config[CONF_FIRMWARE][CONF_VERSION])
|
||||
|
||||
if fw_hex is not None:
|
||||
cg.add_define("USE_SHD_FIRMWARE_DATA", fw_hex)
|
||||
cg.add_define("USE_SHD_FIRMWARE_MAJOR_VERSION", fw_major)
|
||||
cg.add_define("USE_SHD_FIRMWARE_MINOR_VERSION", fw_minor)
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||
yield cg.register_component(var, config)
|
||||
config.pop(
|
||||
CONF_UPDATE_INTERVAL
|
||||
) # drop UPDATE_INTERVAL as it does not apply to the light component
|
||||
|
||||
yield light.register_light(var, config)
|
||||
yield uart.register_uart_device(var, config)
|
||||
|
||||
nrst_pin = yield cg.gpio_pin_expression(config[CONF_NRST_PIN])
|
||||
cg.add(var.set_nrst_pin(nrst_pin))
|
||||
boot0_pin = yield cg.gpio_pin_expression(config[CONF_BOOT0_PIN])
|
||||
cg.add(var.set_boot0_pin(boot0_pin))
|
||||
|
||||
cg.add(var.set_leading_edge(config[CONF_LEADING_EDGE]))
|
||||
cg.add(var.set_warmup_brightness(config[CONF_WARMUP_BRIGHTNESS]))
|
||||
# cg.add(var.set_warmup_time(config[CONF_WARMUP_TIME]))
|
||||
cg.add(var.set_min_brightness(config[CONF_MIN_BRIGHTNESS]))
|
||||
cg.add(var.set_max_brightness(config[CONF_MAX_BRIGHTNESS]))
|
||||
|
||||
for key in [CONF_POWER, CONF_VOLTAGE, CONF_CURRENT]:
|
||||
if key not in config:
|
||||
continue
|
||||
|
||||
conf = config[key]
|
||||
sens = yield sensor.new_sensor(conf)
|
||||
cg.add(getattr(var, f"set_{key}_sensor")(sens))
|
||||
523
esphome/components/shelly_dimmer/shelly_dimmer.cpp
Normal file
523
esphome/components/shelly_dimmer/shelly_dimmer.cpp
Normal file
@@ -0,0 +1,523 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "shelly_dimmer.h"
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
#include "stm32flash.h"
|
||||
#endif
|
||||
|
||||
#ifndef USE_ESP_IDF
|
||||
#include <HardwareSerial.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char TAG[] = "shelly_dimmer";
|
||||
|
||||
constexpr uint8_t SHELLY_DIMMER_ACK_TIMEOUT = 200; // ms
|
||||
constexpr uint8_t SHELLY_DIMMER_MAX_RETRIES = 3;
|
||||
constexpr uint16_t SHELLY_DIMMER_MAX_BRIGHTNESS = 1000; // 100%
|
||||
|
||||
// Protocol framing.
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_START_BYTE = 0x01;
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_END_BYTE = 0x04;
|
||||
|
||||
// Supported commands.
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH = 0x01;
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_POLL = 0x10;
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_VERSION = 0x11;
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS = 0x20;
|
||||
|
||||
// Command payload sizes.
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE = 2;
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE = 10;
|
||||
constexpr uint8_t SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE = 4 + 72 + 3;
|
||||
|
||||
// STM Firmware
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
constexpr uint8_t STM_FIRMWARE[] PROGMEM = USE_SHD_FIRMWARE_DATA;
|
||||
constexpr uint32_t STM_FIRMWARE_SIZE_IN_BYTES = sizeof(STM_FIRMWARE);
|
||||
#endif
|
||||
|
||||
// Scaling Constants
|
||||
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
|
||||
template<typename T, size_t N> constexpr size_t size(const T (&/*unused*/)[N]) noexcept { return N; }
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
namespace esphome {
|
||||
namespace shelly_dimmer {
|
||||
|
||||
/// Computes a crappy checksum as defined by the Shelly Dimmer protocol.
|
||||
uint16_t shelly_dimmer_checksum(const uint8_t *buf, int len) {
|
||||
return std::accumulate<decltype(buf), uint16_t>(buf, buf + len, 0);
|
||||
}
|
||||
|
||||
void ShellyDimmer::setup() {
|
||||
this->pin_nrst_->setup();
|
||||
this->pin_boot0_->setup();
|
||||
|
||||
ESP_LOGI(TAG, "Initializing Shelly Dimmer...");
|
||||
|
||||
// Reset the STM32 and check the firmware version.
|
||||
for (int i = 0; i < 2; i++) {
|
||||
this->reset_normal_boot_();
|
||||
this->send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION, nullptr, 0);
|
||||
ESP_LOGI(TAG, "STM32 current firmware version: %d.%d, desired version: %d.%d", this->version_major_,
|
||||
this->version_minor_, USE_SHD_FIRMWARE_MAJOR_VERSION, USE_SHD_FIRMWARE_MINOR_VERSION);
|
||||
if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION ||
|
||||
this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) {
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
// Update firmware if needed.
|
||||
ESP_LOGW(TAG, "Unsupported STM32 firmware version, flashing");
|
||||
if (i > 0) {
|
||||
// Upgrade was already performed but the reported version is still not right.
|
||||
ESP_LOGE(TAG, "STM32 firmware upgrade already performed, but version is still incorrect");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->upgrade_firmware_()) {
|
||||
ESP_LOGW(TAG, "Failed to upgrade firmware");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Firmware upgrade completed, do the checks again.
|
||||
continue;
|
||||
#else
|
||||
ESP_LOGW(TAG, "Firmware version mismatch, put 'update: true' in the yaml to flash an update.");
|
||||
this->mark_failed();
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this->send_settings_();
|
||||
// Do an immediate poll to refresh current state.
|
||||
this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0);
|
||||
|
||||
this->ready_ = true;
|
||||
}
|
||||
|
||||
void ShellyDimmer::update() { this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0); }
|
||||
|
||||
void ShellyDimmer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ShellyDimmer:");
|
||||
LOG_PIN(" NRST Pin: ", this->pin_nrst_);
|
||||
LOG_PIN(" BOOT0 Pin: ", this->pin_boot0_);
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Leading Edge: %s", YESNO(this->leading_edge_));
|
||||
ESP_LOGCONFIG(TAG, " Warmup Brightness: %d", this->warmup_brightness_);
|
||||
// ESP_LOGCONFIG(TAG, " Warmup Time: %d", this->warmup_time_);
|
||||
// ESP_LOGCONFIG(TAG, " Fade Rate: %d", this->fade_rate_);
|
||||
ESP_LOGCONFIG(TAG, " Minimum Brightness: %d", this->min_brightness_);
|
||||
ESP_LOGCONFIG(TAG, " Maximum Brightness: %d", this->max_brightness_);
|
||||
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
ESP_LOGCONFIG(TAG, " STM32 current firmware version: %d.%d ", this->version_major_, this->version_minor_);
|
||||
ESP_LOGCONFIG(TAG, " STM32 required firmware version: %d.%d", USE_SHD_FIRMWARE_MAJOR_VERSION,
|
||||
USE_SHD_FIRMWARE_MINOR_VERSION);
|
||||
|
||||
if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION ||
|
||||
this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) {
|
||||
ESP_LOGE(TAG, " Firmware version mismatch, put 'update: true' in the yaml to flash an update.");
|
||||
}
|
||||
}
|
||||
|
||||
void ShellyDimmer::write_state(light::LightState *state) {
|
||||
if (!this->ready_) {
|
||||
return;
|
||||
}
|
||||
|
||||
float brightness;
|
||||
state->current_values_as_brightness(&brightness);
|
||||
|
||||
const uint16_t brightness_int = this->convert_brightness_(brightness);
|
||||
if (brightness_int == this->brightness_) {
|
||||
ESP_LOGV(TAG, "Not sending unchanged value");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness);
|
||||
|
||||
this->send_brightness_(brightness_int);
|
||||
}
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
bool ShellyDimmer::upgrade_firmware_() {
|
||||
ESP_LOGW(TAG, "Starting STM32 firmware upgrade");
|
||||
this->reset_dfu_boot_();
|
||||
|
||||
// Cleanup with RAII
|
||||
auto stm32 = stm32_init(this, STREAM_SERIAL, 1);
|
||||
|
||||
if (!stm32) {
|
||||
ESP_LOGW(TAG, "Failed to initialize STM32");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Erase STM32 flash.
|
||||
if (stm32_erase_memory(stm32, 0, STM32_MASS_ERASE) != STM32_ERR_OK) {
|
||||
ESP_LOGW(TAG, "Failed to erase STM32 flash memory");
|
||||
return false;
|
||||
}
|
||||
|
||||
static constexpr uint32_t BUFFER_SIZE = 256;
|
||||
|
||||
// Copy the STM32 firmware over in 256-byte chunks. Note that the firmware is stored
|
||||
// in flash memory so all accesses need to be 4-byte aligned.
|
||||
uint8_t buffer[BUFFER_SIZE];
|
||||
const uint8_t *p = STM_FIRMWARE;
|
||||
uint32_t offset = 0;
|
||||
uint32_t addr = stm32->dev->fl_start;
|
||||
const uint32_t end = addr + STM_FIRMWARE_SIZE_IN_BYTES;
|
||||
|
||||
while (addr < end && offset < STM_FIRMWARE_SIZE_IN_BYTES) {
|
||||
const uint32_t left_of_buffer = std::min(end - addr, BUFFER_SIZE);
|
||||
const uint32_t len = std::min(left_of_buffer, STM_FIRMWARE_SIZE_IN_BYTES - offset);
|
||||
|
||||
if (len == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
std::memcpy(buffer, p, BUFFER_SIZE);
|
||||
p += BUFFER_SIZE;
|
||||
|
||||
if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) {
|
||||
ESP_LOGW(TAG, "Failed to write to STM32 flash memory");
|
||||
return false;
|
||||
}
|
||||
|
||||
addr += len;
|
||||
offset += len;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "STM32 firmware upgrade successful");
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint16_t ShellyDimmer::convert_brightness_(float brightness) {
|
||||
// Special case for zero as only zero means turn off completely.
|
||||
if (brightness == 0.0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return remap<uint16_t, float>(brightness, 0.0f, 1.0f, this->min_brightness_, this->max_brightness_);
|
||||
}
|
||||
|
||||
void ShellyDimmer::send_brightness_(uint16_t brightness) {
|
||||
const uint8_t payload[] = {
|
||||
// Brightness (%) * 10.
|
||||
static_cast<uint8_t>(brightness & 0xff),
|
||||
static_cast<uint8_t>(brightness >> 8),
|
||||
};
|
||||
static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE, "Invalid payload size");
|
||||
|
||||
this->send_command_(SHELLY_DIMMER_PROTO_CMD_SWITCH, payload, SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE);
|
||||
|
||||
this->brightness_ = brightness;
|
||||
}
|
||||
|
||||
void ShellyDimmer::send_settings_() {
|
||||
const uint16_t fade_rate = std::min(uint16_t{100}, this->fade_rate_);
|
||||
|
||||
float brightness = 0.0;
|
||||
if (this->state_ != nullptr) {
|
||||
this->state_->current_values_as_brightness(&brightness);
|
||||
}
|
||||
const uint16_t brightness_int = this->convert_brightness_(brightness);
|
||||
ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness);
|
||||
|
||||
const uint8_t payload[] = {
|
||||
// Brightness (%) * 10.
|
||||
static_cast<uint8_t>(brightness_int & 0xff),
|
||||
static_cast<uint8_t>(brightness_int >> 8),
|
||||
// Leading / trailing edge [0x01 = leading, 0x02 = trailing].
|
||||
this->leading_edge_ ? uint8_t{0x01} : uint8_t{0x02},
|
||||
0x00,
|
||||
// Fade rate.
|
||||
static_cast<uint8_t>(fade_rate & 0xff),
|
||||
static_cast<uint8_t>(fade_rate >> 8),
|
||||
// Warmup brightness.
|
||||
static_cast<uint8_t>(this->warmup_brightness_ & 0xff),
|
||||
static_cast<uint8_t>(this->warmup_brightness_ >> 8),
|
||||
// Warmup time.
|
||||
static_cast<uint8_t>(this->warmup_time_ & 0xff),
|
||||
static_cast<uint8_t>(this->warmup_time_ >> 8),
|
||||
};
|
||||
static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE, "Invalid payload size");
|
||||
|
||||
this->send_command_(SHELLY_DIMMER_PROTO_CMD_SETTINGS, payload, SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE);
|
||||
|
||||
// Also send brightness separately as it is ignored above.
|
||||
this->send_brightness_(brightness_int);
|
||||
}
|
||||
|
||||
bool ShellyDimmer::send_command_(uint8_t cmd, const uint8_t *const payload, uint8_t len) {
|
||||
ESP_LOGD(TAG, "Sending command: 0x%02x (%d bytes) payload 0x%s", cmd, len, format_hex(payload, len).c_str());
|
||||
|
||||
// Prepare a command frame.
|
||||
uint8_t frame[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE];
|
||||
const size_t frame_len = this->frame_command_(frame, cmd, payload, len);
|
||||
|
||||
// Write the frame and wait for acknowledgement.
|
||||
int retries = SHELLY_DIMMER_MAX_RETRIES;
|
||||
while (retries--) {
|
||||
this->write_array(frame, frame_len);
|
||||
this->flush();
|
||||
|
||||
ESP_LOGD(TAG, "Command sent, waiting for reply");
|
||||
const uint32_t tx_time = millis();
|
||||
while (millis() - tx_time < SHELLY_DIMMER_ACK_TIMEOUT) {
|
||||
if (this->read_frame_()) {
|
||||
return true;
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
ESP_LOGW(TAG, "Timeout while waiting for reply");
|
||||
}
|
||||
ESP_LOGW(TAG, "Failed to send command");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t ShellyDimmer::frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *const payload, size_t len) {
|
||||
size_t pos = 0;
|
||||
|
||||
// Generate a frame.
|
||||
data[0] = SHELLY_DIMMER_PROTO_START_BYTE;
|
||||
data[1] = ++this->seq_;
|
||||
data[2] = cmd;
|
||||
data[3] = len;
|
||||
pos += 4;
|
||||
|
||||
if (payload != nullptr) {
|
||||
std::memcpy(data + 4, payload, len);
|
||||
pos += len;
|
||||
}
|
||||
|
||||
// Calculate checksum for the payload.
|
||||
const uint16_t csum = shelly_dimmer_checksum(data + 1, 3 + len);
|
||||
data[pos++] = static_cast<uint8_t>(csum >> 8);
|
||||
data[pos++] = static_cast<uint8_t>(csum & 0xff);
|
||||
data[pos++] = SHELLY_DIMMER_PROTO_END_BYTE;
|
||||
return pos;
|
||||
}
|
||||
|
||||
int ShellyDimmer::handle_byte_(uint8_t c) {
|
||||
const uint8_t pos = this->buffer_pos_;
|
||||
|
||||
if (pos == 0) {
|
||||
// Must be start byte.
|
||||
return c == SHELLY_DIMMER_PROTO_START_BYTE ? 1 : -1;
|
||||
} else if (pos < 4) {
|
||||
// Header.
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Decode payload length from header.
|
||||
const uint8_t payload_len = this->buffer_[3];
|
||||
if ((4 + payload_len + 3) > SHELLY_DIMMER_BUFFER_SIZE) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pos < 4 + payload_len + 1) {
|
||||
// Payload.
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (pos == 4 + payload_len + 1) {
|
||||
// Verify checksum.
|
||||
const uint16_t csum = (this->buffer_[pos - 1] << 8 | c);
|
||||
const uint16_t csum_verify = shelly_dimmer_checksum(&this->buffer_[1], 3 + payload_len);
|
||||
if (csum != csum_verify) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (pos == 4 + payload_len + 2) {
|
||||
// Must be end byte.
|
||||
return c == SHELLY_DIMMER_PROTO_END_BYTE ? 0 : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool ShellyDimmer::read_frame_() {
|
||||
while (this->available()) {
|
||||
const uint8_t c = this->read();
|
||||
this->buffer_[this->buffer_pos_] = c;
|
||||
|
||||
ESP_LOGV(TAG, "Read byte: 0x%02x (pos %d)", c, this->buffer_pos_);
|
||||
|
||||
switch (this->handle_byte_(c)) {
|
||||
case 0: {
|
||||
// Frame successfully received.
|
||||
this->handle_frame_();
|
||||
this->buffer_pos_ = 0;
|
||||
return true;
|
||||
}
|
||||
case -1: {
|
||||
// Failure.
|
||||
this->buffer_pos_ = 0;
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
// Need more data.
|
||||
this->buffer_pos_++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ShellyDimmer::handle_frame_() {
|
||||
const uint8_t seq = this->buffer_[1];
|
||||
const uint8_t cmd = this->buffer_[2];
|
||||
const uint8_t payload_len = this->buffer_[3];
|
||||
|
||||
ESP_LOGD(TAG, "Got frame: 0x%02x", cmd);
|
||||
|
||||
// Compare with expected identifier as the frame is always a response to
|
||||
// our previously sent command.
|
||||
if (seq != this->seq_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t *payload = &this->buffer_[4];
|
||||
|
||||
// Handle response.
|
||||
switch (cmd) {
|
||||
case SHELLY_DIMMER_PROTO_CMD_POLL: {
|
||||
if (payload_len < 16) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t hw_version = payload[0];
|
||||
// payload[1] is unused.
|
||||
const uint16_t brightness = encode_uint16(payload[3], payload[2]);
|
||||
|
||||
const uint32_t power_raw = encode_uint32(payload[7], payload[6], payload[5], payload[4]);
|
||||
|
||||
const uint32_t voltage_raw = encode_uint32(payload[11], payload[10], payload[9], payload[8]);
|
||||
|
||||
const uint32_t current_raw = encode_uint32(payload[15], payload[14], payload[13], payload[12]);
|
||||
|
||||
const uint16_t fade_rate = payload[16];
|
||||
|
||||
float power = 0;
|
||||
if (power_raw > 0) {
|
||||
power = POWER_SCALING_FACTOR / static_cast<float>(power_raw);
|
||||
}
|
||||
|
||||
float voltage = 0;
|
||||
if (voltage_raw > 0) {
|
||||
voltage = VOLTAGE_SCALING_FACTOR / static_cast<float>(voltage_raw);
|
||||
}
|
||||
|
||||
float current = 0;
|
||||
if (current_raw > 0) {
|
||||
current = CURRENT_SCALING_FACTOR / static_cast<float>(current_raw);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Got dimmer data:");
|
||||
ESP_LOGI(TAG, " HW version: %d", hw_version);
|
||||
ESP_LOGI(TAG, " Brightness: %d", brightness);
|
||||
ESP_LOGI(TAG, " Fade rate: %d", fade_rate);
|
||||
ESP_LOGI(TAG, " Power: %f W", power);
|
||||
ESP_LOGI(TAG, " Voltage: %f V", voltage);
|
||||
ESP_LOGI(TAG, " Current: %f A", current);
|
||||
|
||||
// Update sensors.
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(power);
|
||||
}
|
||||
if (this->voltage_sensor_ != nullptr) {
|
||||
this->voltage_sensor_->publish_state(voltage);
|
||||
}
|
||||
if (this->current_sensor_ != nullptr) {
|
||||
this->current_sensor_->publish_state(current);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
case SHELLY_DIMMER_PROTO_CMD_VERSION: {
|
||||
if (payload_len < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->version_minor_ = payload[0];
|
||||
this->version_major_ = payload[1];
|
||||
return true;
|
||||
}
|
||||
case SHELLY_DIMMER_PROTO_CMD_SWITCH:
|
||||
case SHELLY_DIMMER_PROTO_CMD_SETTINGS: {
|
||||
return !(payload_len < 1 || payload[0] != 0x01);
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ShellyDimmer::reset_(bool boot0) {
|
||||
ESP_LOGD(TAG, "Reset STM32, boot0=%d", boot0);
|
||||
|
||||
this->pin_boot0_->digital_write(boot0);
|
||||
this->pin_nrst_->digital_write(false);
|
||||
|
||||
// Wait 50ms for the STM32 to reset.
|
||||
delay(50); // NOLINT
|
||||
|
||||
// Clear receive buffer.
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
|
||||
this->pin_nrst_->digital_write(true);
|
||||
// Wait 50ms for the STM32 to boot.
|
||||
delay(50); // NOLINT
|
||||
|
||||
ESP_LOGD(TAG, "Reset STM32 done");
|
||||
}
|
||||
|
||||
void ShellyDimmer::reset_normal_boot_() {
|
||||
// set NONE parity in normal mode
|
||||
|
||||
#ifndef USE_ESP_IDF // workaround for reconfiguring the uart
|
||||
Serial.end();
|
||||
Serial.begin(115200, SERIAL_8N1);
|
||||
Serial.flush();
|
||||
#endif
|
||||
|
||||
this->flush();
|
||||
this->reset_(false);
|
||||
}
|
||||
|
||||
void ShellyDimmer::reset_dfu_boot_() {
|
||||
// set EVEN parity in bootloader mode
|
||||
|
||||
#ifndef USE_ESP_IDF // workaround for reconfiguring the uart
|
||||
Serial.end();
|
||||
Serial.begin(115200, SERIAL_8E1);
|
||||
Serial.flush();
|
||||
#endif
|
||||
|
||||
this->flush();
|
||||
this->reset_(true);
|
||||
}
|
||||
|
||||
} // namespace shelly_dimmer
|
||||
} // namespace esphome
|
||||
117
esphome/components/shelly_dimmer/shelly_dimmer.h
Normal file
117
esphome/components/shelly_dimmer/shelly_dimmer.h
Normal file
@@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/light/light_output.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace esphome {
|
||||
namespace shelly_dimmer {
|
||||
|
||||
class ShellyDimmer : public PollingComponent, public light::LightOutput, public uart::UARTDevice {
|
||||
private:
|
||||
static constexpr uint16_t SHELLY_DIMMER_BUFFER_SIZE = 256;
|
||||
|
||||
public:
|
||||
float get_setup_priority() const override { return setup_priority::LATE; }
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
light::LightTraits get_traits() override {
|
||||
auto traits = light::LightTraits();
|
||||
traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS});
|
||||
return traits;
|
||||
}
|
||||
|
||||
void setup_state(light::LightState *state) override { this->state_ = state; }
|
||||
void write_state(light::LightState *state) override;
|
||||
|
||||
void set_nrst_pin(GPIOPin *nrst_pin) { this->pin_nrst_ = nrst_pin; }
|
||||
void set_boot0_pin(GPIOPin *boot0_pin) { this->pin_boot0_ = boot0_pin; }
|
||||
|
||||
void set_leading_edge(bool leading_edge) { this->leading_edge_ = leading_edge; }
|
||||
void set_warmup_brightness(uint16_t warmup_brightness) { this->warmup_brightness_ = warmup_brightness; }
|
||||
void set_warmup_time(uint16_t warmup_time) { this->warmup_time_ = warmup_time; }
|
||||
void set_fade_rate(uint16_t fade_rate) { this->fade_rate_ = fade_rate; }
|
||||
void set_min_brightness(uint16_t min_brightness) { this->min_brightness_ = min_brightness; }
|
||||
void set_max_brightness(uint16_t max_brightness) { this->max_brightness_ = max_brightness; }
|
||||
|
||||
void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; }
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
|
||||
void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; }
|
||||
|
||||
protected:
|
||||
GPIOPin *pin_nrst_;
|
||||
GPIOPin *pin_boot0_;
|
||||
|
||||
// Frame parser state.
|
||||
uint8_t seq_{0};
|
||||
std::array<uint8_t, SHELLY_DIMMER_BUFFER_SIZE> buffer_;
|
||||
uint8_t buffer_pos_{0};
|
||||
|
||||
// Firmware version.
|
||||
uint8_t version_major_;
|
||||
uint8_t version_minor_;
|
||||
|
||||
// Configuration.
|
||||
bool leading_edge_{false};
|
||||
uint16_t warmup_brightness_{100};
|
||||
uint16_t warmup_time_{20};
|
||||
uint16_t fade_rate_{0};
|
||||
uint16_t min_brightness_{0};
|
||||
uint16_t max_brightness_{1000};
|
||||
|
||||
light::LightState *state_{nullptr};
|
||||
sensor::Sensor *power_sensor_{nullptr};
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
|
||||
bool ready_{false};
|
||||
uint16_t brightness_;
|
||||
|
||||
/// Convert relative brightness into a dimmer brightness value.
|
||||
uint16_t convert_brightness_(float brightness);
|
||||
|
||||
/// Sends the given brightness value.
|
||||
void send_brightness_(uint16_t brightness);
|
||||
|
||||
/// Sends dimmer configuration.
|
||||
void send_settings_();
|
||||
|
||||
/// Performs a firmware upgrade.
|
||||
bool upgrade_firmware_();
|
||||
|
||||
/// Sends a command and waits for an acknowledgement.
|
||||
bool send_command_(uint8_t cmd, const uint8_t *payload, uint8_t len);
|
||||
|
||||
/// Frames a given command payload.
|
||||
size_t frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *payload, size_t len);
|
||||
|
||||
/// Handles a single byte as part of a protocol frame.
|
||||
///
|
||||
/// Returns -1 on failure, 0 when finished and 1 when more bytes needed.
|
||||
int handle_byte_(uint8_t c);
|
||||
|
||||
/// Reads a response frame.
|
||||
bool read_frame_();
|
||||
|
||||
/// Handles a complete frame.
|
||||
bool handle_frame_();
|
||||
|
||||
/// Reset STM32 with the BOOT0 pin set to the given value.
|
||||
void reset_(bool boot0);
|
||||
|
||||
/// Reset STM32 to boot the regular firmware.
|
||||
void reset_normal_boot_();
|
||||
|
||||
/// Reset STM32 to boot into DFU mode to enable firmware upgrades.
|
||||
void reset_dfu_boot_();
|
||||
};
|
||||
|
||||
} // namespace shelly_dimmer
|
||||
} // namespace esphome
|
||||
1064
esphome/components/shelly_dimmer/stm32flash.cpp
Normal file
1064
esphome/components/shelly_dimmer/stm32flash.cpp
Normal file
@@ -0,0 +1,1064 @@
|
||||
/*
|
||||
stm32flash - Open Source ST STM32 flash program for Arduino
|
||||
Copyright 2010 Geoffrey McRae <geoff@spacevs.com>
|
||||
Copyright 2012-2014 Tormod Volden <debian.tormod@gmail.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "stm32flash.h"
|
||||
#include "debug.h"
|
||||
|
||||
#include "dev_table.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t STM32_ACK = 0x79;
|
||||
constexpr uint8_t STM32_NACK = 0x1F;
|
||||
constexpr uint8_t STM32_BUSY = 0x76;
|
||||
|
||||
constexpr uint8_t STM32_CMD_INIT = 0x7F;
|
||||
constexpr uint8_t STM32_CMD_GET = 0x00; /* get the version and command supported */
|
||||
constexpr uint8_t STM32_CMD_GVR = 0x01; /* get version and read protection status */
|
||||
constexpr uint8_t STM32_CMD_GID = 0x02; /* get ID */
|
||||
constexpr uint8_t STM32_CMD_RM = 0x11; /* read memory */
|
||||
constexpr uint8_t STM32_CMD_GO = 0x21; /* go */
|
||||
constexpr uint8_t STM32_CMD_WM = 0x31; /* write memory */
|
||||
constexpr uint8_t STM32_CMD_WM_NS = 0x32; /* no-stretch write memory */
|
||||
constexpr uint8_t STM32_CMD_ER = 0x43; /* erase */
|
||||
constexpr uint8_t STM32_CMD_EE = 0x44; /* extended erase */
|
||||
constexpr uint8_t STM32_CMD_EE_NS = 0x45; /* extended erase no-stretch */
|
||||
constexpr uint8_t STM32_CMD_WP = 0x63; /* write protect */
|
||||
constexpr uint8_t STM32_CMD_WP_NS = 0x64; /* write protect no-stretch */
|
||||
constexpr uint8_t STM32_CMD_UW = 0x73; /* write unprotect */
|
||||
constexpr uint8_t STM32_CMD_UW_NS = 0x74; /* write unprotect no-stretch */
|
||||
constexpr uint8_t STM32_CMD_RP = 0x82; /* readout protect */
|
||||
constexpr uint8_t STM32_CMD_RP_NS = 0x83; /* readout protect no-stretch */
|
||||
constexpr uint8_t STM32_CMD_UR = 0x92; /* readout unprotect */
|
||||
constexpr uint8_t STM32_CMD_UR_NS = 0x93; /* readout unprotect no-stretch */
|
||||
constexpr uint8_t STM32_CMD_CRC = 0xA1; /* compute CRC */
|
||||
constexpr uint8_t STM32_CMD_ERR = 0xFF; /* not a valid command */
|
||||
|
||||
constexpr uint32_t STM32_RESYNC_TIMEOUT = 35 * 1000; /* milliseconds */
|
||||
constexpr uint32_t STM32_MASSERASE_TIMEOUT = 35 * 1000; /* milliseconds */
|
||||
constexpr uint32_t STM32_PAGEERASE_TIMEOUT = 5 * 1000; /* milliseconds */
|
||||
constexpr uint32_t STM32_BLKWRITE_TIMEOUT = 1 * 1000; /* milliseconds */
|
||||
constexpr uint32_t STM32_WUNPROT_TIMEOUT = 1 * 1000; /* milliseconds */
|
||||
constexpr uint32_t STM32_WPROT_TIMEOUT = 1 * 1000; /* milliseconds */
|
||||
constexpr uint32_t STM32_RPROT_TIMEOUT = 1 * 1000; /* milliseconds */
|
||||
constexpr uint32_t DEFAULT_TIMEOUT = 5 * 1000; /* milliseconds */
|
||||
|
||||
constexpr uint8_t STM32_CMD_GET_LENGTH = 17; /* bytes in the reply */
|
||||
|
||||
/* Reset code for ARMv7-M (Cortex-M3) and ARMv6-M (Cortex-M0)
|
||||
* see ARMv7-M or ARMv6-M Architecture Reference Manual (table B3-8)
|
||||
* or "The definitive guide to the ARM Cortex-M3", section 14.4.
|
||||
*/
|
||||
constexpr uint8_t STM_RESET_CODE[] = {
|
||||
0x01, 0x49, // ldr r1, [pc, #4] ; (<AIRCR_OFFSET>)
|
||||
0x02, 0x4A, // ldr r2, [pc, #8] ; (<AIRCR_RESET_VALUE>)
|
||||
0x0A, 0x60, // str r2, [r1, #0]
|
||||
0xfe, 0xe7, // endless: b endless
|
||||
0x0c, 0xed, 0x00, 0xe0, // .word 0xe000ed0c <AIRCR_OFFSET> = NVIC AIRCR register address
|
||||
0x04, 0x00, 0xfa, 0x05 // .word 0x05fa0004 <AIRCR_RESET_VALUE> = VECTKEY | SYSRESETREQ
|
||||
};
|
||||
|
||||
constexpr uint32_t STM_RESET_CODE_SIZE = sizeof(STM_RESET_CODE);
|
||||
|
||||
/* RM0360, Empty check
|
||||
* On STM32F070x6 and STM32F030xC devices only, internal empty check flag is
|
||||
* implemented to allow easy programming of the virgin devices by the boot loader. This flag is
|
||||
* used when BOOT0 pin is defining Main Flash memory as the target boot space. When the
|
||||
* flag is set, the device is considered as empty and System memory (boot loader) is selected
|
||||
* instead of the Main Flash as a boot space to allow user to program the Flash memory.
|
||||
* This flag is updated only during Option bytes loading: it is set when the content of the
|
||||
* address 0x08000 0000 is read as 0xFFFF FFFF, otherwise it is cleared. It means a power
|
||||
* on or setting of OBL_LAUNCH bit in FLASH_CR register is needed to clear this flag after
|
||||
* programming of a virgin device to execute user code after System reset.
|
||||
*/
|
||||
constexpr uint8_t STM_OBL_LAUNCH_CODE[] = {
|
||||
0x01, 0x49, // ldr r1, [pc, #4] ; (<FLASH_CR>)
|
||||
0x02, 0x4A, // ldr r2, [pc, #8] ; (<OBL_LAUNCH>)
|
||||
0x0A, 0x60, // str r2, [r1, #0]
|
||||
0xfe, 0xe7, // endless: b endless
|
||||
0x10, 0x20, 0x02, 0x40, // address: FLASH_CR = 40022010
|
||||
0x00, 0x20, 0x00, 0x00 // value: OBL_LAUNCH = 00002000
|
||||
};
|
||||
|
||||
constexpr uint32_t STM_OBL_LAUNCH_CODE_SIZE = sizeof(STM_OBL_LAUNCH_CODE);
|
||||
|
||||
constexpr char TAG[] = "stm32flash";
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
namespace esphome {
|
||||
namespace shelly_dimmer {
|
||||
|
||||
namespace {
|
||||
|
||||
int flash_addr_to_page_ceil(const stm32_unique_ptr &stm, uint32_t addr) {
|
||||
if (!(addr >= stm->dev->fl_start && addr <= stm->dev->fl_end))
|
||||
return 0;
|
||||
|
||||
int page = 0;
|
||||
addr -= stm->dev->fl_start;
|
||||
const auto *psize = stm->dev->fl_ps;
|
||||
|
||||
while (addr >= psize[0]) {
|
||||
addr -= psize[0];
|
||||
page++;
|
||||
if (psize[1])
|
||||
psize++;
|
||||
}
|
||||
|
||||
return addr ? page + 1 : page;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_get_ack_timeout(const stm32_unique_ptr &stm, uint32_t timeout) {
|
||||
auto *stream = stm->stream;
|
||||
uint8_t rxbyte;
|
||||
|
||||
if (!(stm->flags & STREAM_OPT_RETRY))
|
||||
timeout = 0;
|
||||
|
||||
if (timeout == 0)
|
||||
timeout = DEFAULT_TIMEOUT;
|
||||
|
||||
const uint32_t start_time = millis();
|
||||
do {
|
||||
yield();
|
||||
if (!stream->available()) {
|
||||
if (millis() < start_time + timeout)
|
||||
continue;
|
||||
ESP_LOGD(TAG, "Failed to read ACK timeout=%i", timeout);
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
stream->read_byte(&rxbyte);
|
||||
|
||||
if (rxbyte == STM32_ACK)
|
||||
return STM32_ERR_OK;
|
||||
if (rxbyte == STM32_NACK)
|
||||
return STM32_ERR_NACK;
|
||||
if (rxbyte != STM32_BUSY) {
|
||||
ESP_LOGD(TAG, "Got byte 0x%02x instead of ACK", rxbyte);
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
stm32_err_t stm32_get_ack(const stm32_unique_ptr &stm) { return stm32_get_ack_timeout(stm, 0); }
|
||||
|
||||
stm32_err_t stm32_send_command_timeout(const stm32_unique_ptr &stm, const uint8_t cmd, const uint32_t timeout) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
static constexpr auto BUFFER_SIZE = 2;
|
||||
const uint8_t buf[] = {
|
||||
cmd,
|
||||
static_cast<uint8_t>(cmd ^ 0xFF),
|
||||
};
|
||||
static_assert(sizeof(buf) == BUFFER_SIZE, "Buf expected to be 2 bytes");
|
||||
|
||||
stream->write_array(buf, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
|
||||
stm32_err_t s_err = stm32_get_ack_timeout(stm, timeout);
|
||||
if (s_err == STM32_ERR_OK)
|
||||
return STM32_ERR_OK;
|
||||
if (s_err == STM32_ERR_NACK) {
|
||||
ESP_LOGD(TAG, "Got NACK from device on command 0x%02x", cmd);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Unexpected reply from device on command 0x%02x", cmd);
|
||||
}
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_send_command(const stm32_unique_ptr &stm, const uint8_t cmd) {
|
||||
return stm32_send_command_timeout(stm, cmd, 0);
|
||||
}
|
||||
|
||||
/* if we have lost sync, send a wrong command and expect a NACK */
|
||||
stm32_err_t stm32_resync(const stm32_unique_ptr &stm) {
|
||||
auto *const stream = stm->stream;
|
||||
uint32_t t0 = millis();
|
||||
auto t1 = t0;
|
||||
|
||||
static constexpr auto BUFFER_SIZE = 2;
|
||||
const uint8_t buf[] = {
|
||||
STM32_CMD_ERR,
|
||||
static_cast<uint8_t>(STM32_CMD_ERR ^ 0xFF),
|
||||
};
|
||||
static_assert(sizeof(buf) == BUFFER_SIZE, "Buf expected to be 2 bytes");
|
||||
|
||||
uint8_t ack;
|
||||
while (t1 < t0 + STM32_RESYNC_TIMEOUT) {
|
||||
stream->write_array(buf, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
if (!stream->read_array(&ack, 1)) {
|
||||
t1 = millis();
|
||||
continue;
|
||||
}
|
||||
if (ack == STM32_NACK)
|
||||
return STM32_ERR_OK;
|
||||
t1 = millis();
|
||||
}
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
/*
|
||||
* some command receive reply frame with variable length, and length is
|
||||
* embedded in reply frame itself.
|
||||
* We can guess the length, but if we guess wrong the protocol gets out
|
||||
* of sync.
|
||||
* Use resync for frame oriented interfaces (e.g. I2C) and byte-by-byte
|
||||
* read for byte oriented interfaces (e.g. UART).
|
||||
*
|
||||
* to run safely, data buffer should be allocated for 256+1 bytes
|
||||
*
|
||||
* len is value of the first byte in the frame.
|
||||
*/
|
||||
stm32_err_t stm32_guess_len_cmd(const stm32_unique_ptr &stm, const uint8_t cmd, uint8_t *const data, unsigned int len) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
if (stm32_send_command(stm, cmd) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
if (stm->flags & STREAM_OPT_BYTE) {
|
||||
/* interface is UART-like */
|
||||
if (!stream->read_array(data, 1))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
len = data[0];
|
||||
if (!stream->read_array(data + 1, len + 1))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
const auto ret = stream->read_array(data, len + 2);
|
||||
if (ret && len == data[0])
|
||||
return STM32_ERR_OK;
|
||||
if (!ret) {
|
||||
/* restart with only one byte */
|
||||
if (stm32_resync(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
if (stm32_send_command(stm, cmd) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
if (!stream->read_array(data, 1))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Re sync (len = %d)", data[0]);
|
||||
if (stm32_resync(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
len = data[0];
|
||||
if (stm32_send_command(stm, cmd) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
if (!stream->read_array(data, len + 2))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* Some interface, e.g. UART, requires a specific init sequence to let STM32
|
||||
* autodetect the interface speed.
|
||||
* The sequence is only required one time after reset.
|
||||
* This function sends the init sequence and, in case of timeout, recovers
|
||||
* the interface.
|
||||
*/
|
||||
stm32_err_t stm32_send_init_seq(const stm32_unique_ptr &stm) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
stream->write_array(&STM32_CMD_INIT, 1);
|
||||
stream->flush();
|
||||
|
||||
uint8_t byte;
|
||||
bool ret = stream->read_array(&byte, 1);
|
||||
if (ret && byte == STM32_ACK)
|
||||
return STM32_ERR_OK;
|
||||
if (ret && byte == STM32_NACK) {
|
||||
/* We could get error later, but let's continue, for now. */
|
||||
ESP_LOGD(TAG, "Warning: the interface was not closed properly.");
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
if (!ret) {
|
||||
ESP_LOGD(TAG, "Failed to init device.");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if previous STM32_CMD_INIT was taken as first byte
|
||||
* of a command. Send a new byte, we should get back a NACK.
|
||||
*/
|
||||
stream->write_array(&STM32_CMD_INIT, 1);
|
||||
stream->flush();
|
||||
|
||||
ret = stream->read_array(&byte, 1);
|
||||
if (ret && byte == STM32_NACK)
|
||||
return STM32_ERR_OK;
|
||||
ESP_LOGD(TAG, "Failed to init device.");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_mass_erase(const stm32_unique_ptr &stm) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) {
|
||||
ESP_LOGD(TAG, "Can't initiate chip mass erase!");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
/* regular erase (0x43) */
|
||||
if (stm->cmd->er == STM32_CMD_ER) {
|
||||
const auto s_err = stm32_send_command_timeout(stm, 0xFF, STM32_MASSERASE_TIMEOUT);
|
||||
if (s_err != STM32_ERR_OK) {
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
/* extended erase */
|
||||
static constexpr auto BUFFER_SIZE = 3;
|
||||
const uint8_t buf[] = {
|
||||
0xFF, /* 0xFFFF the magic number for mass erase */
|
||||
0xFF, 0x00, /* checksum */
|
||||
};
|
||||
static_assert(sizeof(buf) == BUFFER_SIZE, "Expected the buffer to be 3 bytes");
|
||||
stream->write_array(buf, 3);
|
||||
stream->flush();
|
||||
|
||||
const auto s_err = stm32_get_ack_timeout(stm, STM32_MASSERASE_TIMEOUT);
|
||||
if (s_err != STM32_ERR_OK) {
|
||||
ESP_LOGD(TAG, "Mass erase failed. Try specifying the number of pages to be erased.");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
template<typename T> std::unique_ptr<T[], void (*)(T *memory)> malloc_array_raii(size_t size) {
|
||||
// Could be constexpr in c++17
|
||||
static const auto DELETOR = [](T *memory) {
|
||||
free(memory); // NOLINT
|
||||
};
|
||||
return std::unique_ptr<T[], decltype(DELETOR)>{static_cast<T *>(malloc(size)), // NOLINT
|
||||
DELETOR};
|
||||
}
|
||||
|
||||
stm32_err_t stm32_pages_erase(const stm32_unique_ptr &stm, const uint32_t spage, const uint32_t pages) {
|
||||
auto *const stream = stm->stream;
|
||||
uint8_t cs = 0;
|
||||
int i = 0;
|
||||
|
||||
/* The erase command reported by the bootloader is either 0x43, 0x44 or 0x45 */
|
||||
/* 0x44 is Extended Erase, a 2 byte based protocol and needs to be handled differently. */
|
||||
/* 0x45 is clock no-stretching version of Extended Erase for I2C port. */
|
||||
if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) {
|
||||
ESP_LOGD(TAG, "Can't initiate chip mass erase!");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
/* regular erase (0x43) */
|
||||
if (stm->cmd->er == STM32_CMD_ER) {
|
||||
// Free memory with RAII
|
||||
auto buf = malloc_array_raii<uint8_t>(1 + pages + 1);
|
||||
|
||||
if (!buf)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
buf[i++] = pages - 1;
|
||||
cs ^= (pages - 1);
|
||||
for (auto pg_num = spage; pg_num < (pages + spage); pg_num++) {
|
||||
buf[i++] = pg_num;
|
||||
cs ^= pg_num;
|
||||
}
|
||||
buf[i++] = cs;
|
||||
stream->write_array(&buf[0], i);
|
||||
stream->flush();
|
||||
|
||||
const auto s_err = stm32_get_ack_timeout(stm, pages * STM32_PAGEERASE_TIMEOUT);
|
||||
if (s_err != STM32_ERR_OK) {
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
/* extended erase */
|
||||
|
||||
// Free memory with RAII
|
||||
auto buf = malloc_array_raii<uint8_t>(2 + 2 * pages + 1);
|
||||
|
||||
if (!buf)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
/* Number of pages to be erased - 1, two bytes, MSB first */
|
||||
uint8_t pg_byte = (pages - 1) >> 8;
|
||||
buf[i++] = pg_byte;
|
||||
cs ^= pg_byte;
|
||||
pg_byte = (pages - 1) & 0xFF;
|
||||
buf[i++] = pg_byte;
|
||||
cs ^= pg_byte;
|
||||
|
||||
for (auto pg_num = spage; pg_num < spage + pages; pg_num++) {
|
||||
pg_byte = pg_num >> 8;
|
||||
cs ^= pg_byte;
|
||||
buf[i++] = pg_byte;
|
||||
pg_byte = pg_num & 0xFF;
|
||||
cs ^= pg_byte;
|
||||
buf[i++] = pg_byte;
|
||||
}
|
||||
buf[i++] = cs;
|
||||
stream->write_array(&buf[0], i);
|
||||
stream->flush();
|
||||
|
||||
const auto s_err = stm32_get_ack_timeout(stm, pages * STM32_PAGEERASE_TIMEOUT);
|
||||
if (s_err != STM32_ERR_OK) {
|
||||
ESP_LOGD(TAG, "Page-by-page erase failed. Check the maximum pages your device supports.");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
template<typename T> stm32_err_t stm32_check_ack_timeout(const stm32_err_t s_err, const T &&log) {
|
||||
switch (s_err) {
|
||||
case STM32_ERR_OK:
|
||||
return STM32_ERR_OK;
|
||||
case STM32_ERR_NACK:
|
||||
log();
|
||||
// TODO: c++17 [[fallthrough]]
|
||||
/* fallthrough */
|
||||
default:
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
/* detect CPU endian */
|
||||
bool cpu_le() {
|
||||
static constexpr int N = 1;
|
||||
|
||||
// returns true if little endian
|
||||
return *reinterpret_cast<const char *>(&N) == 1;
|
||||
}
|
||||
|
||||
uint32_t le_u32(const uint32_t v) {
|
||||
if (!cpu_le())
|
||||
return ((v & 0xFF000000) >> 24) | ((v & 0x00FF0000) >> 8) | ((v & 0x0000FF00) << 8) | ((v & 0x000000FF) << 24);
|
||||
return v;
|
||||
}
|
||||
|
||||
template<size_t N> void populate_buffer_with_address(uint8_t (&buffer)[N], uint32_t address) {
|
||||
buffer[0] = static_cast<uint8_t>(address >> 24);
|
||||
buffer[1] = static_cast<uint8_t>((address >> 16) & 0xFF);
|
||||
buffer[2] = static_cast<uint8_t>((address >> 8) & 0xFF);
|
||||
buffer[3] = static_cast<uint8_t>(address & 0xFF);
|
||||
buffer[4] = static_cast<uint8_t>(buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3]);
|
||||
}
|
||||
|
||||
template<typename T> stm32_unique_ptr make_stm32_with_deletor(T ptr) {
|
||||
static const auto CLOSE = [](stm32_t *stm32) {
|
||||
if (stm32) {
|
||||
free(stm32->cmd); // NOLINT
|
||||
}
|
||||
free(stm32); // NOLINT
|
||||
};
|
||||
|
||||
// Cleanup with RAII
|
||||
return std::unique_ptr<stm32_t, decltype(CLOSE)>{ptr, CLOSE};
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
} // namespace shelly_dimmer
|
||||
} // namespace esphome
|
||||
|
||||
namespace esphome {
|
||||
namespace shelly_dimmer {
|
||||
|
||||
/* find newer command by higher code */
|
||||
#define newer(prev, a) (((prev) == STM32_CMD_ERR) ? (a) : (((prev) > (a)) ? (prev) : (a)))
|
||||
|
||||
stm32_unique_ptr stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) {
|
||||
uint8_t buf[257];
|
||||
|
||||
auto stm = make_stm32_with_deletor(static_cast<stm32_t *>(calloc(sizeof(stm32_t), 1))); // NOLINT
|
||||
|
||||
if (!stm) {
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
}
|
||||
stm->stream = stream;
|
||||
stm->flags = flags;
|
||||
|
||||
stm->cmd = static_cast<stm32_cmd_t *>(malloc(sizeof(stm32_cmd_t))); // NOLINT
|
||||
if (!stm->cmd) {
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
}
|
||||
memset(stm->cmd, STM32_CMD_ERR, sizeof(stm32_cmd_t));
|
||||
|
||||
if ((stm->flags & STREAM_OPT_CMD_INIT) && init) {
|
||||
if (stm32_send_init_seq(stm) != STM32_ERR_OK)
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
}
|
||||
|
||||
/* get the version and read protection status */
|
||||
if (stm32_send_command(stm, STM32_CMD_GVR) != STM32_ERR_OK) {
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
}
|
||||
|
||||
/* From AN, only UART bootloader returns 3 bytes */
|
||||
{
|
||||
const auto len = (stm->flags & STREAM_OPT_GVR_ETX) ? 3 : 1;
|
||||
if (!stream->read_array(buf, len))
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
|
||||
stm->version = buf[0];
|
||||
stm->option1 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[1] : 0;
|
||||
stm->option2 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[2] : 0;
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK) {
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const auto len = ([&]() {
|
||||
/* get the bootloader information */
|
||||
if (stm->cmd_get_reply) {
|
||||
for (auto i = 0; stm->cmd_get_reply[i].length; ++i) {
|
||||
if (stm->version == stm->cmd_get_reply[i].version) {
|
||||
return stm->cmd_get_reply[i].length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return STM32_CMD_GET_LENGTH;
|
||||
})();
|
||||
|
||||
if (stm32_guess_len_cmd(stm, STM32_CMD_GET, buf, len) != STM32_ERR_OK)
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
}
|
||||
|
||||
const auto stop = buf[0] + 1;
|
||||
stm->bl_version = buf[1];
|
||||
int new_cmds = 0;
|
||||
for (auto i = 1; i < stop; ++i) {
|
||||
const auto val = buf[i + 1];
|
||||
switch (val) {
|
||||
case STM32_CMD_GET:
|
||||
stm->cmd->get = val;
|
||||
break;
|
||||
case STM32_CMD_GVR:
|
||||
stm->cmd->gvr = val;
|
||||
break;
|
||||
case STM32_CMD_GID:
|
||||
stm->cmd->gid = val;
|
||||
break;
|
||||
case STM32_CMD_RM:
|
||||
stm->cmd->rm = val;
|
||||
break;
|
||||
case STM32_CMD_GO:
|
||||
stm->cmd->go = val;
|
||||
break;
|
||||
case STM32_CMD_WM:
|
||||
case STM32_CMD_WM_NS:
|
||||
stm->cmd->wm = newer(stm->cmd->wm, val);
|
||||
break;
|
||||
case STM32_CMD_ER:
|
||||
case STM32_CMD_EE:
|
||||
case STM32_CMD_EE_NS:
|
||||
stm->cmd->er = newer(stm->cmd->er, val);
|
||||
break;
|
||||
case STM32_CMD_WP:
|
||||
case STM32_CMD_WP_NS:
|
||||
stm->cmd->wp = newer(stm->cmd->wp, val);
|
||||
break;
|
||||
case STM32_CMD_UW:
|
||||
case STM32_CMD_UW_NS:
|
||||
stm->cmd->uw = newer(stm->cmd->uw, val);
|
||||
break;
|
||||
case STM32_CMD_RP:
|
||||
case STM32_CMD_RP_NS:
|
||||
stm->cmd->rp = newer(stm->cmd->rp, val);
|
||||
break;
|
||||
case STM32_CMD_UR:
|
||||
case STM32_CMD_UR_NS:
|
||||
stm->cmd->ur = newer(stm->cmd->ur, val);
|
||||
break;
|
||||
case STM32_CMD_CRC:
|
||||
stm->cmd->crc = newer(stm->cmd->crc, val);
|
||||
break;
|
||||
default:
|
||||
if (new_cmds++ == 0) {
|
||||
ESP_LOGD(TAG, "GET returns unknown commands (0x%2x", val);
|
||||
} else {
|
||||
ESP_LOGD(TAG, ", 0x%2x", val);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (new_cmds)
|
||||
ESP_LOGD(TAG, ")");
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK) {
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
}
|
||||
|
||||
if (stm->cmd->get == STM32_CMD_ERR || stm->cmd->gvr == STM32_CMD_ERR || stm->cmd->gid == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: bootloader did not returned correct information from GET command");
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
}
|
||||
|
||||
/* get the device ID */
|
||||
if (stm32_guess_len_cmd(stm, stm->cmd->gid, buf, 1) != STM32_ERR_OK) {
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
}
|
||||
const auto returned = buf[0] + 1;
|
||||
if (returned < 2) {
|
||||
ESP_LOGD(TAG, "Only %d bytes sent in the PID, unknown/unsupported device", returned);
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
}
|
||||
stm->pid = (buf[1] << 8) | buf[2];
|
||||
if (returned > 2) {
|
||||
ESP_LOGD(TAG, "This bootloader returns %d extra bytes in PID:", returned);
|
||||
for (auto i = 2; i <= returned; i++)
|
||||
ESP_LOGD(TAG, " %02x", buf[i]);
|
||||
}
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK) {
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
}
|
||||
|
||||
stm->dev = DEVICES;
|
||||
while (stm->dev->id != 0x00 && stm->dev->id != stm->pid)
|
||||
++stm->dev;
|
||||
|
||||
if (!stm->dev->id) {
|
||||
ESP_LOGD(TAG, "Unknown/unsupported device (Device ID: 0x%03x)", stm->pid);
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
}
|
||||
|
||||
return stm;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, const uint32_t address, uint8_t *data,
|
||||
const unsigned int len) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
if (!len)
|
||||
return STM32_ERR_OK;
|
||||
|
||||
if (len > 256) {
|
||||
ESP_LOGD(TAG, "Error: READ length limit at 256 bytes");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
if (stm->cmd->rm == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: READ command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->rm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
static constexpr auto BUFFER_SIZE = 5;
|
||||
uint8_t buf[BUFFER_SIZE];
|
||||
populate_buffer_with_address(buf, address);
|
||||
|
||||
stream->write_array(buf, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
if (stm32_send_command(stm, len - 1) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
if (!stream->read_array(data, len))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data,
|
||||
const unsigned int len) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
if (!len)
|
||||
return STM32_ERR_OK;
|
||||
|
||||
if (len > 256) {
|
||||
ESP_LOGD(TAG, "Error: READ length limit at 256 bytes");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
/* must be 32bit aligned */
|
||||
if (address & 0x3) {
|
||||
ESP_LOGD(TAG, "Error: WRITE address must be 4 byte aligned");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
if (stm->cmd->wm == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: WRITE command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
/* send the address and checksum */
|
||||
if (stm32_send_command(stm, stm->cmd->wm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
static constexpr auto BUFFER_SIZE = 5;
|
||||
uint8_t buf1[BUFFER_SIZE];
|
||||
populate_buffer_with_address(buf1, address);
|
||||
|
||||
stream->write_array(buf1, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
const unsigned int aligned_len = (len + 3) & ~3;
|
||||
uint8_t cs = aligned_len - 1;
|
||||
uint8_t buf[256 + 2];
|
||||
|
||||
buf[0] = aligned_len - 1;
|
||||
for (auto i = 0; i < len; i++) {
|
||||
cs ^= data[i];
|
||||
buf[i + 1] = data[i];
|
||||
}
|
||||
/* padding data */
|
||||
for (auto i = len; i < aligned_len; i++) {
|
||||
cs ^= 0xFF;
|
||||
buf[i + 1] = 0xFF;
|
||||
}
|
||||
buf[aligned_len + 1] = cs;
|
||||
stream->write_array(buf, aligned_len + 2);
|
||||
stream->flush();
|
||||
|
||||
const auto s_err = stm32_get_ack_timeout(stm, STM32_BLKWRITE_TIMEOUT);
|
||||
if (s_err != STM32_ERR_OK) {
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm) {
|
||||
if (stm->cmd->uw == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: WRITE UNPROTECT command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->uw) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_WUNPROT_TIMEOUT),
|
||||
[]() { ESP_LOGD(TAG, "Error: Failed to WRITE UNPROTECT"); });
|
||||
}
|
||||
|
||||
stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm) {
|
||||
if (stm->cmd->wp == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: WRITE PROTECT command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->wp) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_WPROT_TIMEOUT),
|
||||
[]() { ESP_LOGD(TAG, "Error: Failed to WRITE PROTECT"); });
|
||||
}
|
||||
|
||||
stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm) {
|
||||
if (stm->cmd->ur == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: READOUT UNPROTECT command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->ur) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_MASSERASE_TIMEOUT),
|
||||
[]() { ESP_LOGD(TAG, "Error: Failed to READOUT UNPROTECT"); });
|
||||
}
|
||||
|
||||
stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm) {
|
||||
if (stm->cmd->rp == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: READOUT PROTECT command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->rp) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_RPROT_TIMEOUT),
|
||||
[]() { ESP_LOGD(TAG, "Error: Failed to READOUT PROTECT"); });
|
||||
}
|
||||
|
||||
stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages) {
|
||||
if (!pages || spage > STM32_MAX_PAGES || ((pages != STM32_MASS_ERASE) && ((spage + pages) > STM32_MAX_PAGES)))
|
||||
return STM32_ERR_OK;
|
||||
|
||||
if (stm->cmd->er == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: ERASE command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (pages == STM32_MASS_ERASE) {
|
||||
/*
|
||||
* Not all chips support mass erase.
|
||||
* Mass erase can be obtained executing a "readout protect"
|
||||
* followed by "readout un-protect". This method is not
|
||||
* suggested because can hang the target if a debug SWD/JTAG
|
||||
* is connected. When the target enters in "readout
|
||||
* protection" mode it will consider the debug connection as
|
||||
* a tentative of intrusion and will hang.
|
||||
* Erasing the flash page-by-page is the safer way to go.
|
||||
*/
|
||||
if (!(stm->dev->flags & F_NO_ME))
|
||||
return stm32_mass_erase(stm);
|
||||
|
||||
pages = flash_addr_to_page_ceil(stm, stm->dev->fl_end);
|
||||
}
|
||||
|
||||
/*
|
||||
* Some device, like STM32L152, cannot erase more than 512 pages in
|
||||
* one command. Split the call.
|
||||
*/
|
||||
static constexpr uint32_t MAX_PAGE_SIZE = 512;
|
||||
while (pages) {
|
||||
const auto n = std::min(pages, MAX_PAGE_SIZE);
|
||||
const auto s_err = stm32_pages_erase(stm, spage, n);
|
||||
if (s_err != STM32_ERR_OK)
|
||||
return s_err;
|
||||
spage += n;
|
||||
pages -= n;
|
||||
}
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
static stm32_err_t stm32_run_raw_code(const stm32_unique_ptr &stm, uint32_t target_address, const uint8_t *code,
|
||||
uint32_t code_size) {
|
||||
static constexpr uint32_t BUFFER_SIZE = 256;
|
||||
|
||||
const auto stack_le = le_u32(0x20002000);
|
||||
const auto code_address_le = le_u32(target_address + 8 + 1); // thumb mode address (!)
|
||||
uint32_t length = code_size + 8;
|
||||
|
||||
/* Must be 32-bit aligned */
|
||||
if (target_address & 0x3) {
|
||||
ESP_LOGD(TAG, "Error: code address must be 4 byte aligned");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
// Could be constexpr in c++17
|
||||
static const auto DELETOR = [](uint8_t *memory) {
|
||||
free(memory); // NOLINT
|
||||
};
|
||||
|
||||
// Free memory with RAII
|
||||
std::unique_ptr<uint8_t, decltype(DELETOR)> mem{static_cast<uint8_t *>(malloc(length)), // NOLINT
|
||||
DELETOR};
|
||||
|
||||
if (!mem)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
memcpy(mem.get(), &stack_le, sizeof(stack_le));
|
||||
memcpy(mem.get() + 4, &code_address_le, sizeof(code_address_le));
|
||||
memcpy(mem.get() + 8, code, code_size);
|
||||
|
||||
auto *pos = mem.get();
|
||||
auto address = target_address;
|
||||
while (length > 0) {
|
||||
const auto w = std::min(length, BUFFER_SIZE);
|
||||
if (stm32_write_memory(stm, address, pos, w) != STM32_ERR_OK) {
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
address += w;
|
||||
pos += w;
|
||||
length -= w;
|
||||
}
|
||||
|
||||
return stm32_go(stm, target_address);
|
||||
}
|
||||
|
||||
stm32_err_t stm32_go(const stm32_unique_ptr &stm, const uint32_t address) {
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
if (stm->cmd->go == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: GO command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->go) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
static constexpr auto BUFFER_SIZE = 5;
|
||||
uint8_t buf[BUFFER_SIZE];
|
||||
populate_buffer_with_address(buf, address);
|
||||
|
||||
stream->write_array(buf, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm) {
|
||||
const auto target_address = stm->dev->ram_start;
|
||||
|
||||
if (stm->dev->flags & F_OBLL) {
|
||||
/* set the OBL_LAUNCH bit to reset device (see RM0360, 2.5) */
|
||||
return stm32_run_raw_code(stm, target_address, STM_OBL_LAUNCH_CODE, STM_OBL_LAUNCH_CODE_SIZE);
|
||||
} else {
|
||||
return stm32_run_raw_code(stm, target_address, STM_RESET_CODE, STM_RESET_CODE_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, const uint32_t address, const uint32_t length,
|
||||
uint32_t *const crc) {
|
||||
static constexpr auto BUFFER_SIZE = 5;
|
||||
auto *const stream = stm->stream;
|
||||
|
||||
if (address & 0x3 || length & 0x3) {
|
||||
ESP_LOGD(TAG, "Start and end addresses must be 4 byte aligned");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
if (stm->cmd->crc == STM32_CMD_ERR) {
|
||||
ESP_LOGD(TAG, "Error: CRC command not implemented in bootloader.");
|
||||
return STM32_ERR_NO_CMD;
|
||||
}
|
||||
|
||||
if (stm32_send_command(stm, stm->cmd->crc) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
{
|
||||
static constexpr auto BUFFER_SIZE = 5;
|
||||
uint8_t buf[BUFFER_SIZE];
|
||||
populate_buffer_with_address(buf, address);
|
||||
|
||||
stream->write_array(buf, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
}
|
||||
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
{
|
||||
static constexpr auto BUFFER_SIZE = 5;
|
||||
uint8_t buf[BUFFER_SIZE];
|
||||
populate_buffer_with_address(buf, address);
|
||||
|
||||
stream->write_array(buf, BUFFER_SIZE);
|
||||
stream->flush();
|
||||
}
|
||||
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK)
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
{
|
||||
uint8_t buf[BUFFER_SIZE];
|
||||
if (!stream->read_array(buf, BUFFER_SIZE))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
if (buf[4] != (buf[0] ^ buf[1] ^ buf[2] ^ buf[3]))
|
||||
return STM32_ERR_UNKNOWN;
|
||||
|
||||
*crc = (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
|
||||
}
|
||||
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* CRC computed by STM32 is similar to the standard crc32_be()
|
||||
* implemented, for example, in Linux kernel in ./lib/crc32.c
|
||||
* But STM32 computes it on units of 32 bits word and swaps the
|
||||
* bytes of the word before the computation.
|
||||
* Due to byte swap, I cannot use any CRC available in existing
|
||||
* libraries, so here is a simple not optimized implementation.
|
||||
*/
|
||||
uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len) {
|
||||
static constexpr uint32_t CRCPOLY_BE = 0x04c11db7;
|
||||
static constexpr uint32_t CRC_MSBMASK = 0x80000000;
|
||||
|
||||
if (len & 0x3) {
|
||||
ESP_LOGD(TAG, "Buffer length must be multiple of 4 bytes");
|
||||
return 0;
|
||||
}
|
||||
|
||||
while (len) {
|
||||
uint32_t data = *buf++;
|
||||
data |= *buf++ << 8;
|
||||
data |= *buf++ << 16;
|
||||
data |= *buf++ << 24;
|
||||
len -= 4;
|
||||
|
||||
crc ^= data;
|
||||
|
||||
for (size_t i = 0; i < 32; ++i) {
|
||||
if (crc & CRC_MSBMASK) {
|
||||
crc = (crc << 1) ^ CRCPOLY_BE;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc) {
|
||||
static constexpr uint32_t CRC_INIT_VALUE = 0xFFFFFFFF;
|
||||
static constexpr uint32_t BUFFER_SIZE = 256;
|
||||
|
||||
uint8_t buf[BUFFER_SIZE];
|
||||
|
||||
if (address & 0x3 || length & 0x3) {
|
||||
ESP_LOGD(TAG, "Start and end addresses must be 4 byte aligned");
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
|
||||
if (stm->cmd->crc != STM32_CMD_ERR)
|
||||
return stm32_crc_memory(stm, address, length, crc);
|
||||
|
||||
const auto start = address;
|
||||
const auto total_len = length;
|
||||
uint32_t current_crc = CRC_INIT_VALUE;
|
||||
while (length) {
|
||||
const auto len = std::min(BUFFER_SIZE, length);
|
||||
if (stm32_read_memory(stm, address, buf, len) != STM32_ERR_OK) {
|
||||
ESP_LOGD(TAG, "Failed to read memory at address 0x%08x, target write-protected?", address);
|
||||
return STM32_ERR_UNKNOWN;
|
||||
}
|
||||
current_crc = stm32_sw_crc(current_crc, buf, len);
|
||||
length -= len;
|
||||
address += len;
|
||||
|
||||
ESP_LOGD(TAG, "\rCRC address 0x%08x (%.2f%%) ", address, (100.0f / (float) total_len) * (float) (address - start));
|
||||
}
|
||||
ESP_LOGD(TAG, "Done.");
|
||||
*crc = current_crc;
|
||||
return STM32_ERR_OK;
|
||||
}
|
||||
|
||||
} // namespace shelly_dimmer
|
||||
} // namespace esphome
|
||||
#endif
|
||||
131
esphome/components/shelly_dimmer/stm32flash.h
Normal file
131
esphome/components/shelly_dimmer/stm32flash.h
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
stm32flash - Open Source ST STM32 flash program for Arduino
|
||||
Copyright (C) 2010 Geoffrey McRae <geoff@spacevs.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_SHD_FIRMWARE_DATA
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace shelly_dimmer {
|
||||
|
||||
/* flags */
|
||||
constexpr auto STREAM_OPT_BYTE = (1 << 0); /* byte (not frame) oriented */
|
||||
constexpr auto STREAM_OPT_GVR_ETX = (1 << 1); /* cmd GVR returns protection status */
|
||||
constexpr auto STREAM_OPT_CMD_INIT = (1 << 2); /* use INIT cmd to autodetect speed */
|
||||
constexpr auto STREAM_OPT_RETRY = (1 << 3); /* allowed read() retry after timeout */
|
||||
constexpr auto STREAM_OPT_I2C = (1 << 4); /* i2c */
|
||||
constexpr auto STREAM_OPT_STRETCH_W = (1 << 5); /* warning for no-stretching commands */
|
||||
|
||||
constexpr auto STREAM_SERIAL = (STREAM_OPT_BYTE | STREAM_OPT_GVR_ETX | STREAM_OPT_CMD_INIT | STREAM_OPT_RETRY);
|
||||
constexpr auto STREAM_I2C = (STREAM_OPT_I2C | STREAM_OPT_STRETCH_W);
|
||||
|
||||
constexpr auto STM32_MAX_RX_FRAME = 256; /* cmd read memory */
|
||||
constexpr auto STM32_MAX_TX_FRAME = (1 + 256 + 1); /* cmd write memory */
|
||||
|
||||
constexpr auto STM32_MAX_PAGES = 0x0000ffff;
|
||||
constexpr auto STM32_MASS_ERASE = 0x00100000; /* > 2 x max_pages */
|
||||
|
||||
using stm32_err_t = enum Stm32Err {
|
||||
STM32_ERR_OK = 0,
|
||||
STM32_ERR_UNKNOWN, /* Generic error */
|
||||
STM32_ERR_NACK,
|
||||
STM32_ERR_NO_CMD, /* Command not available in bootloader */
|
||||
};
|
||||
|
||||
using flags_t = enum Flags {
|
||||
F_NO_ME = 1 << 0, /* Mass-Erase not supported */
|
||||
F_OBLL = 1 << 1, /* OBL_LAUNCH required */
|
||||
};
|
||||
|
||||
using stm32_cmd_t = struct Stm32Cmd {
|
||||
uint8_t get;
|
||||
uint8_t gvr;
|
||||
uint8_t gid;
|
||||
uint8_t rm;
|
||||
uint8_t go;
|
||||
uint8_t wm;
|
||||
uint8_t er; /* this may be extended erase */
|
||||
uint8_t wp;
|
||||
uint8_t uw;
|
||||
uint8_t rp;
|
||||
uint8_t ur;
|
||||
uint8_t crc;
|
||||
};
|
||||
|
||||
using stm32_dev_t = struct Stm32Dev { // NOLINT
|
||||
const uint16_t id;
|
||||
const char *name;
|
||||
const uint32_t ram_start, ram_end;
|
||||
const uint32_t fl_start, fl_end;
|
||||
const uint16_t fl_pps; // pages per sector
|
||||
const uint32_t *fl_ps; // page size
|
||||
const uint32_t opt_start, opt_end;
|
||||
const uint32_t mem_start, mem_end;
|
||||
const uint32_t flags;
|
||||
};
|
||||
|
||||
using stm32_t = struct Stm32 {
|
||||
uart::UARTDevice *stream;
|
||||
uint8_t flags;
|
||||
struct VarlenCmd *cmd_get_reply;
|
||||
uint8_t bl_version;
|
||||
uint8_t version;
|
||||
uint8_t option1, option2;
|
||||
uint16_t pid;
|
||||
stm32_cmd_t *cmd;
|
||||
const stm32_dev_t *dev;
|
||||
};
|
||||
|
||||
/*
|
||||
* Specify the length of reply for command GET
|
||||
* This is helpful for frame-oriented protocols, e.g. i2c, to avoid time
|
||||
* consuming try-fail-timeout-retry operation.
|
||||
* On byte-oriented protocols, i.e. UART, this information would be skipped
|
||||
* after read the first byte, so not needed.
|
||||
*/
|
||||
struct VarlenCmd {
|
||||
uint8_t version;
|
||||
uint8_t length;
|
||||
};
|
||||
|
||||
using stm32_unique_ptr = std::unique_ptr<stm32_t, void (*)(stm32_t *)>;
|
||||
|
||||
stm32_unique_ptr stm32_init(uart::UARTDevice *stream, uint8_t flags, char init);
|
||||
stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, uint32_t address, uint8_t *data, unsigned int len);
|
||||
stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data, unsigned int len);
|
||||
stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm);
|
||||
stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm);
|
||||
stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages);
|
||||
stm32_err_t stm32_go(const stm32_unique_ptr &stm, uint32_t address);
|
||||
stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm);
|
||||
stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm);
|
||||
stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm);
|
||||
stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc);
|
||||
stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc);
|
||||
uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len);
|
||||
|
||||
} // namespace shelly_dimmer
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_SHD_FIRMWARE_DATA
|
||||
@@ -1,6 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components import i2c, sensor, sensirion_common
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
@@ -13,10 +13,11 @@ from esphome.const import (
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
sht3xd_ns = cg.esphome_ns.namespace("sht3xd")
|
||||
SHT3XDComponent = sht3xd_ns.class_(
|
||||
"SHT3XDComponent", cg.PollingComponent, i2c.I2CDevice
|
||||
"SHT3XDComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
@@ -17,13 +17,8 @@ static const uint16_t SHT3XD_COMMAND_FETCH_DATA = 0xE000;
|
||||
|
||||
void SHT3XDComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up SHT3xD...");
|
||||
if (!this->write_command_(SHT3XD_COMMAND_READ_SERIAL_NUMBER)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t raw_serial_number[2];
|
||||
if (!this->read_data_(raw_serial_number, 2)) {
|
||||
if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER, raw_serial_number, 2)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -45,16 +40,16 @@ float SHT3XDComponent::get_setup_priority() const { return setup_priority::DATA;
|
||||
void SHT3XDComponent::update() {
|
||||
if (this->status_has_warning()) {
|
||||
ESP_LOGD(TAG, "Retrying to reconnect the sensor.");
|
||||
this->write_command_(SHT3XD_COMMAND_SOFT_RESET);
|
||||
this->write_command(SHT3XD_COMMAND_SOFT_RESET);
|
||||
}
|
||||
if (!this->write_command_(SHT3XD_COMMAND_POLLING_H)) {
|
||||
if (!this->write_command(SHT3XD_COMMAND_POLLING_H)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(50, [this]() {
|
||||
uint16_t raw_data[2];
|
||||
if (!this->read_data_(raw_data, 2)) {
|
||||
if (!this->read_data(raw_data, 2)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
@@ -71,56 +66,5 @@ void SHT3XDComponent::update() {
|
||||
});
|
||||
}
|
||||
|
||||
bool SHT3XDComponent::write_command_(uint16_t command) {
|
||||
// Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
|
||||
return this->write_byte(command >> 8, command & 0xFF);
|
||||
}
|
||||
|
||||
uint8_t sht_crc(uint8_t data1, uint8_t data2) {
|
||||
uint8_t bit;
|
||||
uint8_t crc = 0xFF;
|
||||
|
||||
crc ^= data1;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
crc ^= data2;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool SHT3XDComponent::read_data_(uint16_t *data, uint8_t len) {
|
||||
const uint8_t num_bytes = len * 3;
|
||||
std::vector<uint8_t> buf(num_bytes);
|
||||
|
||||
if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
const uint8_t j = 3 * i;
|
||||
uint8_t crc = sht_crc(buf[j], buf[j + 1]);
|
||||
if (crc != buf[j + 2]) {
|
||||
ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
|
||||
return false;
|
||||
}
|
||||
data[i] = (buf[j] << 8) | buf[j + 1];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace sht3xd
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sht3xd {
|
||||
|
||||
/// This class implements support for the SHT3x-DIS family of temperature+humidity i2c sensors.
|
||||
class SHT3XDComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
class SHT3XDComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
public:
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
|
||||
@@ -19,9 +19,6 @@ class SHT3XDComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
bool write_command_(uint16_t command);
|
||||
bool read_data_(uint16_t *data, uint8_t len);
|
||||
|
||||
sensor::Sensor *temperature_sensor_;
|
||||
sensor::Sensor *humidity_sensor_;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components import i2c, sensor, sensirion_common
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
@@ -16,10 +16,13 @@ from esphome.const import (
|
||||
|
||||
CODEOWNERS = ["@sjtrny"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
sht4x_ns = cg.esphome_ns.namespace("sht4x")
|
||||
|
||||
SHT4XComponent = sht4x_ns.class_("SHT4XComponent", cg.PollingComponent, i2c.I2CDevice)
|
||||
SHT4XComponent = sht4x_ns.class_(
|
||||
"SHT4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
|
||||
)
|
||||
|
||||
CONF_PRECISION = "precision"
|
||||
SHT4XPRECISION = sht4x_ns.enum("SHT4XPRECISION")
|
||||
|
||||
@@ -50,31 +50,28 @@ void SHT4XComponent::setup() {
|
||||
void SHT4XComponent::dump_config() { LOG_I2C_DEVICE(this); }
|
||||
|
||||
void SHT4XComponent::update() {
|
||||
uint8_t cmd[] = {MEASURECOMMANDS[this->precision_]};
|
||||
|
||||
// Send command
|
||||
this->write(cmd, 1);
|
||||
this->write_command(MEASURECOMMANDS[this->precision_]);
|
||||
|
||||
this->set_timeout(10, [this]() {
|
||||
const uint8_t num_bytes = 6;
|
||||
uint8_t buffer[num_bytes];
|
||||
uint16_t buffer[2];
|
||||
|
||||
// Read measurement
|
||||
bool read_status = this->read_bytes_raw(buffer, num_bytes);
|
||||
bool read_status = this->read_data(buffer, 2);
|
||||
|
||||
if (read_status) {
|
||||
// Evaluate and publish measurements
|
||||
if (this->temp_sensor_ != nullptr) {
|
||||
// Temp is contained in the first 16 bits
|
||||
float sensor_value_temp = (buffer[0] << 8) + buffer[1];
|
||||
// Temp is contained in the first result word
|
||||
float sensor_value_temp = buffer[0];
|
||||
float temp = -45 + 175 * sensor_value_temp / 65535;
|
||||
|
||||
this->temp_sensor_->publish_state(temp);
|
||||
}
|
||||
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
// Relative humidity is in the last 16 bits
|
||||
float sensor_value_rh = (buffer[3] << 8) + buffer[4];
|
||||
// Relative humidity is in the second result word
|
||||
float sensor_value_rh = buffer[1];
|
||||
float rh = -6 + 125 * sensor_value_rh / 65535;
|
||||
|
||||
this->humidity_sensor_->publish_state(rh);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sht4x {
|
||||
@@ -13,7 +13,7 @@ enum SHT4XHEATERPOWER { SHT4X_HEATERPOWER_HIGH, SHT4X_HEATERPOWER_MED, SHT4X_HEA
|
||||
|
||||
enum SHT4XHEATERTIME { SHT4X_HEATERTIME_LONG = 1100, SHT4X_HEATERTIME_SHORT = 110 };
|
||||
|
||||
class SHT4XComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
class SHT4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
public:
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void setup() override;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components import i2c, sensor, sensirion_common
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
@@ -13,9 +13,12 @@ from esphome.const import (
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
shtcx_ns = cg.esphome_ns.namespace("shtcx")
|
||||
SHTCXComponent = shtcx_ns.class_("SHTCXComponent", cg.PollingComponent, i2c.I2CDevice)
|
||||
SHTCXComponent = shtcx_ns.class_(
|
||||
"SHTCXComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
|
||||
)
|
||||
|
||||
SHTCXType = shtcx_ns.enum("SHTCXType")
|
||||
|
||||
|
||||
@@ -29,21 +29,23 @@ void SHTCXComponent::setup() {
|
||||
this->wake_up();
|
||||
this->soft_reset();
|
||||
|
||||
if (!this->write_command_(SHTCX_COMMAND_READ_ID_REGISTER)) {
|
||||
if (!this->write_command(SHTCX_COMMAND_READ_ID_REGISTER)) {
|
||||
ESP_LOGE(TAG, "Error requesting Device ID");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t device_id_register[1];
|
||||
if (!this->read_data_(device_id_register, 1)) {
|
||||
uint16_t device_id_register;
|
||||
if (!this->read_data(&device_id_register, 1)) {
|
||||
ESP_LOGE(TAG, "Error reading Device ID");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (((device_id_register[0] << 2) & 0x1C) == 0x1C) {
|
||||
if ((device_id_register[0] & 0x847) == 0x847) {
|
||||
this->sensor_id_ = device_id_register;
|
||||
|
||||
if ((device_id_register & 0x3F) == 0x07) {
|
||||
if (device_id_register & 0x800) {
|
||||
this->type_ = SHTCX_TYPE_SHTC3;
|
||||
} else {
|
||||
this->type_ = SHTCX_TYPE_SHTC1;
|
||||
@@ -51,11 +53,11 @@ void SHTCXComponent::setup() {
|
||||
} else {
|
||||
this->type_ = SHTCX_TYPE_UNKNOWN;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Device identified: %s", to_string(this->type_));
|
||||
ESP_LOGCONFIG(TAG, " Device identified: %s (%04x)", to_string(this->type_), device_id_register);
|
||||
}
|
||||
void SHTCXComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "SHTCx:");
|
||||
ESP_LOGCONFIG(TAG, " Model: %s", to_string(this->type_));
|
||||
ESP_LOGCONFIG(TAG, " Model: %s (%04x)", to_string(this->type_), this->sensor_id_);
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication with SHTCx failed!");
|
||||
@@ -74,22 +76,29 @@ void SHTCXComponent::update() {
|
||||
if (this->type_ != SHTCX_TYPE_SHTC1) {
|
||||
this->wake_up();
|
||||
}
|
||||
if (!this->write_command_(SHTCX_COMMAND_POLLING_H)) {
|
||||
if (!this->write_command(SHTCX_COMMAND_POLLING_H)) {
|
||||
ESP_LOGE(TAG, "sensor polling failed");
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(NAN);
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
this->humidity_sensor_->publish_state(NAN);
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(50, [this]() {
|
||||
float temperature = NAN;
|
||||
float humidity = NAN;
|
||||
uint16_t raw_data[2];
|
||||
if (!this->read_data_(raw_data, 2)) {
|
||||
if (!this->read_data(raw_data, 2)) {
|
||||
ESP_LOGE(TAG, "sensor read failed");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
} else {
|
||||
temperature = 175.0f * float(raw_data[0]) / 65536.0f - 45.0f;
|
||||
humidity = 100.0f * float(raw_data[1]) / 65536.0f;
|
||||
|
||||
ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity);
|
||||
}
|
||||
|
||||
float temperature = 175.0f * float(raw_data[0]) / 65536.0f - 45.0f;
|
||||
float humidity = 100.0f * float(raw_data[1]) / 65536.0f;
|
||||
|
||||
ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
@@ -101,65 +110,14 @@ void SHTCXComponent::update() {
|
||||
});
|
||||
}
|
||||
|
||||
bool SHTCXComponent::write_command_(uint16_t command) {
|
||||
// Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
|
||||
return this->write_byte(command >> 8, command & 0xFF);
|
||||
}
|
||||
|
||||
uint8_t sht_crc(uint8_t data1, uint8_t data2) {
|
||||
uint8_t bit;
|
||||
uint8_t crc = 0xFF;
|
||||
|
||||
crc ^= data1;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
crc ^= data2;
|
||||
for (bit = 8; bit > 0; --bit) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x131;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool SHTCXComponent::read_data_(uint16_t *data, uint8_t len) {
|
||||
const uint8_t num_bytes = len * 3;
|
||||
std::vector<uint8_t> buf(num_bytes);
|
||||
|
||||
if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
const uint8_t j = 3 * i;
|
||||
uint8_t crc = sht_crc(buf[j], buf[j + 1]);
|
||||
if (crc != buf[j + 2]) {
|
||||
ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
|
||||
return false;
|
||||
}
|
||||
data[i] = (buf[j] << 8) | buf[j + 1];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SHTCXComponent::soft_reset() {
|
||||
this->write_command_(SHTCX_COMMAND_SOFT_RESET);
|
||||
this->write_command(SHTCX_COMMAND_SOFT_RESET);
|
||||
delayMicroseconds(200);
|
||||
}
|
||||
void SHTCXComponent::sleep() { this->write_command_(SHTCX_COMMAND_SLEEP); }
|
||||
void SHTCXComponent::sleep() { this->write_command(SHTCX_COMMAND_SLEEP); }
|
||||
|
||||
void SHTCXComponent::wake_up() {
|
||||
this->write_command_(SHTCX_COMMAND_WAKEUP);
|
||||
this->write_command(SHTCX_COMMAND_WAKEUP);
|
||||
delayMicroseconds(200);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace shtcx {
|
||||
@@ -10,7 +10,7 @@ namespace shtcx {
|
||||
enum SHTCXType { SHTCX_TYPE_SHTC3 = 0, SHTCX_TYPE_SHTC1, SHTCX_TYPE_UNKNOWN };
|
||||
|
||||
/// This class implements support for the SHT3x-DIS family of temperature+humidity i2c sensors.
|
||||
class SHTCXComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
class SHTCXComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
public:
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
|
||||
@@ -24,9 +24,8 @@ class SHTCXComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
void wake_up();
|
||||
|
||||
protected:
|
||||
bool write_command_(uint16_t command);
|
||||
bool read_data_(uint16_t *data, uint8_t len);
|
||||
SHTCXType type_;
|
||||
uint16_t sensor_id_;
|
||||
sensor::Sensor *temperature_sensor_;
|
||||
sensor::Sensor *humidity_sensor_;
|
||||
};
|
||||
|
||||
38
esphome/components/sml/__init__.py
Normal file
38
esphome/components/sml/__init__.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import re
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import uart
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
CODEOWNERS = ["@alengwenus"]
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
sml_ns = cg.esphome_ns.namespace("sml")
|
||||
Sml = sml_ns.class_("Sml", cg.Component, uart.UARTDevice)
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_SML_ID = "sml_id"
|
||||
CONF_OBIS_CODE = "obis_code"
|
||||
CONF_SERVER_ID = "server_id"
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Sml),
|
||||
}
|
||||
).extend(uart.UART_DEVICE_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
|
||||
def obis_code(value):
|
||||
value = cv.string(value)
|
||||
match = re.match(r"^\d{1,3}-\d{1,3}:\d{1,3}\.\d{1,3}\.\d{1,3}$", value)
|
||||
if match is None:
|
||||
raise cv.Invalid(f"{value} is not a valid OBIS code")
|
||||
return value
|
||||
48
esphome/components/sml/constants.h
Normal file
48
esphome/components/sml/constants.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome {
|
||||
namespace sml {
|
||||
|
||||
enum SmlType : uint8_t {
|
||||
SML_OCTET = 0,
|
||||
SML_BOOL = 4,
|
||||
SML_INT = 5,
|
||||
SML_UINT = 6,
|
||||
SML_LIST = 7,
|
||||
SML_HEX = 10,
|
||||
SML_UNDEFINED = 255
|
||||
};
|
||||
|
||||
enum SmlMessageType : uint16_t { SML_PUBLIC_OPEN_RES = 0x0101, SML_GET_LIST_RES = 0x701 };
|
||||
|
||||
enum Crc16CheckResult : uint8_t { CHECK_CRC16_FAILED, CHECK_CRC16_X25_SUCCESS, CHECK_CRC16_KERMIT_SUCCESS };
|
||||
|
||||
// masks with two-bit mapping 0x1b -> 0b01; 0x01 -> 0b10; 0x1a -> 0b11
|
||||
const uint16_t START_MASK = 0x55aa; // 0x1b 1b 1b 1b 1b 01 01 01 01
|
||||
const uint16_t END_MASK = 0x0157; // 0x1b 1b 1b 1b 1a
|
||||
|
||||
const uint16_t CRC16_X25_TABLE[256] = {
|
||||
0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5,
|
||||
0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52,
|
||||
0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3,
|
||||
0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
|
||||
0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9,
|
||||
0x2732, 0x36bb, 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e,
|
||||
0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f,
|
||||
0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
|
||||
0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862,
|
||||
0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb,
|
||||
0x4e64, 0x5fed, 0x6d76, 0x7cff, 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948,
|
||||
0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
|
||||
0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226,
|
||||
0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497,
|
||||
0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704,
|
||||
0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
|
||||
0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb,
|
||||
0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c,
|
||||
0x3de3, 0x2c6a, 0x1ef1, 0x0f78};
|
||||
|
||||
} // namespace sml
|
||||
} // namespace esphome
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user