1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-01 15:41:52 +00:00

Compare commits

...

104 Commits

Author SHA1 Message Date
Jesse Hills
896654aaef Merge pull request #2302 from esphome/bump-2021.9.0b4
2021.9.0b4
2021-09-14 23:23:51 +12:00
Jesse Hills
5fad38f65f Bump version to 2021.9.0b4 2021-09-14 23:07:08 +12:00
Jesse Hills
89f2ea5725 Fix binary strobe (#2301) 2021-09-14 23:07:07 +12:00
Jesse Hills
a32ad33b4e Allow simple hostname for sntp servers (#2300) 2021-09-14 23:07:07 +12:00
Otto Winter
a328fff5a7 Fix api noise explicit reject (#2297) 2021-09-14 23:07:07 +12:00
Otto Winter
233783c76c API Noise logging (#2298) 2021-09-14 23:07:07 +12:00
Otto Winter
39a18fb358 Bump platformio to 5.2.0 (#2291) 2021-09-14 23:07:02 +12:00
Jas Strong
460a144ca8 t6615: tolerate sensor dropping commands (#2255)
The Amphenol T6615 has a built-in calibration system which means that
the sensor could go away for a couple of seconds to figure itself out.
While this is happening, commands are silently dropped.

This caused the previous version of this code to lock up completely,
since there was no way for the command_ state machine to tick back to
the NONE state.

Instead of just breaking the state machine, which might be harmful on
a multi-core or multi-threaded device, add a timestamp and only break
the lock if it's been more than a second since the command was issued.

The command usually doesn't take more than a few milliseconds to
complete, so this should not affect things unduly.

While we're at it, rewrite the rx side to be more robust against
bytes going missing.

Instead of reading in the data essentially inline, read into a buffer
and process it when enough has been read to make progress.

If data stops coming when we expect it to, or the data is malformed,
have a timeout that sends a new command.

Co-authored-by: jas <jas@asspa.in>
2021-09-14 23:06:13 +12:00
Oxan van Leeuwen
23ead416d5 Suppress excessive warnings about deprecated Fan interfaces (#2270) 2021-09-14 23:06:13 +12:00
Oxan van Leeuwen
1b5f11bbee Only try compat parsing after regular parsing fails (#2269) 2021-09-14 23:06:13 +12:00
Jesse Hills
0da97289e6 Merge pull request #2295 from esphome/bump-2021.9.0b3
2021.9.0b3
2021-09-14 07:47:20 +12:00
Jesse Hills
91f12a50cf Bump version to 2021.9.0b3 2021-09-14 07:13:00 +12:00
Otto Winter
e92a9d1d9e Fix API socket issues (#2288)
* Fix API socket issues

* Fix compile error against beta

* Format
2021-09-14 07:13:00 +12:00
Oxan van Leeuwen
4eb51ab4d6 Disable automatic usage of SNTP servers from DHCP (#2273) 2021-09-14 07:12:59 +12:00
Jesse Hills
f1a8d957f8 Merge pull request #2267 from esphome/bump-2021.9.0b2
2021.9.0b2
2021-09-10 22:13:54 +12:00
Jesse Hills
9821a3442b Bump version to 2021.9.0b2 2021-09-10 21:34:38 +12:00
poptix
87842e097b sm300d2: Accept (undocumented) 0x80 checksum offset. (#2263)
Co-authored-by: Matt Hallacy <github@poptix.net>
2021-09-10 21:34:38 +12:00
Jesse Hills
7dd40e2014 Fix a few ESP32-C3 compiler issues (#2265)
* Fix using Serial when using ESP32-C3 standard pins

* Force type for std::min in pn532

* Fix variable size where size_t is different on exp32-c3
2021-09-10 21:34:38 +12:00
Jesse Hills
a719998220 Merge pull request #2262 from esphome/bump-2021.9.0b1
2021.9.0b1
2021-09-09 11:40:33 +12:00
Jesse Hills
09a6fdf1c7 Bump version to 2021.9.0b1 2021-09-09 10:28:34 +12:00
Stephen Tierney
d2616cbdfc PMSA003i Update state_class and async (#2216)
* Update component (state_class and async)

* No need to specify empty device class

* Remove unused import

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2021-09-09 10:14:08 +12:00
Keith Burzinski
faf1c8bee8 SGP40 sensor start-up fix (#2178) 2021-09-09 09:42:35 +12:00
Peter van Dijk
f09aca4865 pm1006: add support for sending a measurement request (#2214) 2021-09-09 09:35:00 +12:00
Jesse Hills
cc52f37933 Revert "Dont dump legacy fields (#2241)" (#2259)
This reverts commit 97eba1eecc.
2021-09-09 09:29:08 +12:00
Otto Winter
e5051eefbc API encryption (#2254) 2021-09-09 09:22:47 +12:00
Peter van Dijk
9e5cd0da51 ccs811: publish firmware version; log bootloader and HW version; fix a bug (#2006) 2021-09-09 09:19:43 +12:00
Sergey V. DUDANOV
4e120a291e Midea support v2 (#2188) 2021-09-09 09:10:02 +12:00
Otto Winter
2790d72bff Convert API to use sockets (#2253)
* Socket component

* Lint

* Lint

* Fix esp8266 missing INADDR_ANY

* API convert to sockets and frame helper

* Fix compile error

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2021-09-08 12:52:57 +02:00
Otto Winter
e44f447d85 Fix socket not setting callbacks early enough (#2260) 2021-09-08 12:02:32 +02:00
Jesse Hills
4356581db0 Remove removesuffix 2021-09-08 19:51:20 +12:00
Jesse Hills
f87a701b28 Bump dashboard to 20210908.0 and fix card names for yml (#2258) 2021-09-08 17:14:12 +12:00
Jesse Hills
fa2eb46cd6 Allow .yml files to show up in the dashboard (#2257) 2021-09-08 16:11:09 +12:00
Otto Winter
f924e80f43 Socket component (#2250) 2021-09-08 15:41:42 +12:00
Chris Nussbaum
6180ee8065 Template sensors always publish on update interval (#2224)
Co-authored-by: Chris Nussbaum <chris.nussbaum@protolabs.com>
2021-09-08 15:36:49 +12:00
wifwucite
1be106c0b5 Fix fan speed restore issue on boot (#1867) 2021-09-08 15:30:17 +12:00
dgtal1
b0533db2eb Add new trigger to fan component on_speed_set (#2246) 2021-09-08 15:15:57 +12:00
Otto Winter
dba502c756 Logger prevent recursive logging (#2251) 2021-09-08 08:57:20 +12:00
Oxan van Leeuwen
d9cb64b893 Add device classes new in HA 2021.9 (#2248) 2021-09-07 10:12:26 +12:00
Kamil Trzciński
2d91e6b977 template: select: fix initial_value cannot be used with lambda (#2244) 2021-09-07 08:00:08 +12:00
Alex
4a6f1f150a Fix runtime exception due to dict typing (#2243) 2021-09-06 14:48:28 +12:00
Peter van Dijk
7f76f3726f LOG_UPDATE_INTERVAL: correctly report "never" (#2240) 2021-09-06 14:47:13 +12:00
Jesse Hills
e2d97b6f36 Light: include ON_OFF capability to BRIGHTNESS ColorMode (#2204) 2021-09-06 08:57:58 +12:00
Jesse Hills
2a653642f5 Fix encoding bug (#2242) 2021-09-06 08:57:37 +12:00
Jesse Hills
97eba1eecc Dont dump legacy fields (#2241) 2021-09-06 08:36:55 +12:00
Jesse Hills
ff6bed54c6 Remove last_reset_type and convert all those sensors to TOTAL_INCREASING (#2233) 2021-09-06 08:30:47 +12:00
Jesse Hills
f9b0666adf Allow using a git source for a package (#2193) 2021-09-06 08:23:06 +12:00
Jesse Hills
ca12b8aa56 Move to use zeroconf library instead of inline copy (#2192) 2021-09-06 08:22:15 +12:00
Christian Ferbar
77508f7e44 Fix UARTComponent hardware vs software UART0 conflict (#2229)
Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl>
2021-09-04 14:49:34 +12:00
Oxan van Leeuwen
54de0ca0da Reject template select/number/switches that don't handle user input (#2230) 2021-09-04 14:46:53 +12:00
Oxan van Leeuwen
f364788c03 Expose WHITE/CWWW/RGBCT color modes over MQTT (#2231) 2021-09-04 14:32:33 +12:00
DAVe3283
00aaf84c37 Fix uptime's state_class (esphome/issues#2337) (#2205) 2021-09-03 16:58:30 +12:00
Kamil Trzciński
b01bc76dc5 mqtt_sensor: properly send state_class via MQTT (#2228) 2021-09-03 16:37:18 +12:00
dependabot[bot]
910f812737 Bump pylint from 2.9.6 to 2.10.2 (#2197)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.9.6 to 2.10.2.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.9.6...v2.10.2)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-03 16:20:42 +12:00
Chris Nussbaum
a4d024f43d Add is_on and is_off conditions for the fan component (#2225)
Co-authored-by: Chris Nussbaum <chris.nussbaum@protolabs.com>
2021-09-02 12:16:11 +12:00
Alex
9937ad7fa0 Cleanup flash transitions (#2227) 2021-09-02 11:56:40 +12:00
Petko Bordjukov
edcd88123d iBeacon support for ble_presence (#1627) 2021-09-02 11:46:15 +12:00
Alex
ea1b5e19f0 Add transitions to light flash (#2201) 2021-08-31 14:18:16 +12:00
Jesse Hills
54337befc2 Fix some lint errors in pylint 2.10.2 (#2226) 2021-08-31 14:00:58 +12:00
Jérôme Laban
140ef791aa Support for the AirThings Wave Plus (#1656)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2021-08-31 14:00:30 +12:00
dependabot[bot]
58350b6c99 Bump pytest from 6.2.4 to 6.2.5 (#2223)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-31 11:10:52 +12:00
dependabot[bot]
f186ff8b46 Bump black from 21.7b0 to 21.8b0 (#2222)
Bumps [black](https://github.com/psf/black) from 21.7b0 to 21.8b0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/commits)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-31 10:40:06 +12:00
WeekendWarrior1
03190611bb Add H-Bridge fan component (#2212)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2021-08-31 10:10:22 +12:00
Guillermo Ruffino
37f322585e Glmnet schema 202105 (#2220) 2021-08-31 06:48:19 +12:00
Marcio Granzotto Rodrigues
9218e85bd6 Remove footer validation for fujitsu_general (#2196) 2021-08-31 06:03:30 +12:00
Jesse Hills
f923ba87c0 Bump dashboard to 20210826.0 (#2211) 2021-08-30 09:41:14 +12:00
Oxan van Leeuwen
fac49896df Update known boards (#2190) 2021-08-30 09:41:05 +12:00
Paul Monigatti
56225701f9 Fix Packages when using MQTT (#2210) 2021-08-30 09:27:38 +12:00
Alessandro Campolo
b5de43b225 cs_pin made optional for ili9341 (#2219) 2021-08-30 09:07:06 +12:00
Jesse Hills
b955527f6c Fix css/js file loading for webserver when esphome not executed form config directory (#2207) 2021-08-26 15:34:39 +12:00
marsjan155
94b28102f5 Add st7920 display, (#1440)
Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2021-08-26 14:33:03 +12:00
WeekendWarrior1
de871862a8 Optionally set direction on fan.turn_on action (#2171)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2021-08-26 10:25:24 +12:00
Jesse Hills
3be56fd502 Fix SDM energy units to be KILO... (#2206) 2021-08-26 09:27:00 +12:00
Jesse Hills
39cd2838df Revert "Light: include ON_OFF capability to BRIGHTNESS ColorMode (#2186)" (#2202)
This reverts commit b0fa317302.
2021-08-25 19:38:51 +12:00
Stephan Peijnik-Steinwender
565473c90c ST7789V: Make backlight_pin optional (#2180) 2021-08-24 22:57:53 +12:00
mtl010957
ed68a0e773 Internally all temperature units are Celsius so just send it directly (#1840) 2021-08-24 13:38:59 +12:00
Samuel Sieb
e2640c8368 Fix template select lambda (#2198) 2021-08-24 13:26:59 +12:00
Chris Nussbaum
eff626248f Tuya fan component uses enum datapoint type for speed instead of integer (#2182)
Co-authored-by: Chris Nussbaum <chris.nussbaum@protolabs.com>
2021-08-24 13:20:39 +12:00
puuu
ce29a3b07a mqtt_light: remove legacy API config that is not compatible with HA 2021.8 (#2183) 2021-08-24 13:18:40 +12:00
Otto Winter
1b89174558 Store source package in Component for debugging (#2070) 2021-08-24 06:49:19 +12:00
Oxan van Leeuwen
1c1ad32610 Add deprecated attribute to some deprecated types/methods (#2185) 2021-08-24 06:48:12 +12:00
Jesse Hills
71237e2f76 Fix template select log message mentioning number (#2194) 2021-08-23 21:21:30 +12:00
Oxan van Leeuwen
518c271eba Fix addressable light control without transitions & effects with transitions (#2187) 2021-08-23 20:44:24 +12:00
Oxan van Leeuwen
d71996e58d Reduce static RAM usage (#2140) 2021-08-23 20:43:54 +12:00
Oxan van Leeuwen
2f33cd2db5 Remove double scheduling from addressable lights (#1963) 2021-08-23 20:00:38 +12:00
Oxan van Leeuwen
5ec9bb0fb5 Clean-up constant definitions (#2148) 2021-08-23 19:21:30 +12:00
Oxan van Leeuwen
8cc3cbb22e Add macros header with more usable Arduino version defines (#2145) 2021-08-23 19:19:21 +12:00
puuu
b0fa317302 Light: include ON_OFF capability to BRIGHTNESS ColorMode (#2186) 2021-08-21 22:26:24 +12:00
Jesse Hills
5cb56bc677 Set SDM voltage state class to measurement (#2181) 2021-08-19 21:28:58 +12:00
Jesse Hills
21f8fd9fa5 Fix pypi download url (#2177) 2021-08-18 15:39:57 +12:00
Jesse Hills
2100ef63a9 Bump version to 2021.9.0-dev 2021-08-18 15:19:25 +12:00
Jesse Hills
29db77c9c9 Merge branch 'release' into dev 2021-08-18 15:17:33 +12:00
Jesse Hills
f0b14055b6 Add new total_increasing state-class for Home Assistant 2021.9+ (#2166) 2021-08-18 11:04:13 +12:00
Jesse Hills
fbd9e87b51 Remove specified accuracy_decimals from total_daily_energy (#2174) 2021-08-18 10:32:19 +12:00
Jesse Hills
edb3b77916 Send dirty states when screen wakes up (#2167) 2021-08-18 10:22:00 +12:00
Chris Nussbaum
ebaa84617f Total daily energy methods (#2163)
Co-authored-by: Chris Nussbaum <chris.nussbaum@protolabs.com>
2021-08-18 07:16:02 +12:00
Franck Nijhof
8eb18995cb Add device class update to binary sensor (#2170) 2021-08-17 21:44:05 +12:00
Franck Nijhof
ebabf0e7d8 Add Gas device class to DSMR component (#2169) 2021-08-17 21:43:51 +12:00
Maurice Makaay
607e1f823d Minor code cleanup in light components (#2162)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
2021-08-17 20:12:29 +12:00
Daniel Hyles
3b52a306cd Add a dummy color temp (#2161) 2021-08-17 20:05:37 +12:00
Oxan van Leeuwen
0c370d5897 Initialize color temperature to value within range if possible (#2168) 2021-08-17 14:02:38 +12:00
Otto Winter
9b48ff5775 Fix native API log level enum values (#2151) 2021-08-16 11:57:50 +12:00
Keith Burzinski
117b58ebe6 Thermostat delayed fan mode fix (#2158) 2021-08-16 09:31:48 +12:00
puuu
303b699005 let sensors announce its state_class via mqtt (#2155) 2021-08-16 07:59:29 +12:00
Oxan van Leeuwen
9173da0416 Always send all light state values in API (#2150) 2021-08-16 07:40:34 +12:00
214 changed files with 6525 additions and 7448 deletions

View File

@@ -14,6 +14,8 @@ esphome/core/* @esphome/core
esphome/components/ac_dimmer/* @glmnet
esphome/components/adc/* @esphome/core
esphome/components/addressable_light/* @justfalter
esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_plus/* @jeromelaban
esphome/components/am43/* @buxtronix
esphome/components/am43/cover/* @buxtronix
esphome/components/animation/* @syndlex
@@ -29,6 +31,7 @@ esphome/components/ble_client/* @buxtronix
esphome/components/bme680_bsec/* @trvrnrth
esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/captive_portal/* @OttoWinter
esphome/components/ccs811/* @habbie
esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet
esphome/components/color_temperature/* @jesserockz
@@ -52,6 +55,8 @@ esphome/components/globals/* @esphome/core
esphome/components/gpio/* @esphome/core
esphome/components/gps/* @coogle
esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann
esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/homeassistant/* @OttoWinter
esphome/components/hrxl_maxsonar_wr/* @netmikey
@@ -75,8 +80,7 @@ esphome/components/mcp23x17_base/* @jesserockz
esphome/components/mcp23xxx_base/* @jesserockz
esphome/components/mcp2515/* @danielschramm @mvturnho
esphome/components/mcp9808/* @k7hpn
esphome/components/midea_ac/* @dudanov
esphome/components/midea_dongle/* @dudanov
esphome/components/midea/* @dudanov
esphome/components/mitsubishi/* @RubyBailey
esphome/components/network/* @esphome/core
esphome/components/nextion/* @senexcrenshaw
@@ -90,6 +94,7 @@ esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/pid/* @OttoWinter
esphome/components/pipsolar/* @andreashergert1984
esphome/components/pm1006/* @habbie
esphome/components/pmsa003i/* @sjtrny
esphome/components/pn532/* @OttoWinter @jesserockz
esphome/components/pn532_i2c/* @OttoWinter @jesserockz
@@ -115,6 +120,7 @@ esphome/components/sht4x/* @sjtrny
esphome/components/shutdown/* @esphome/core
esphome/components/sim800l/* @glmnet
esphome/components/sm2135/* @BoukeHaarsma23
esphome/components/socket/* @esphome/core
esphome/components/spi/* @esphome/core
esphome/components/ssd1322_base/* @kbx81
esphome/components/ssd1322_spi/* @kbx81
@@ -129,6 +135,7 @@ esphome/components/ssd1351_base/* @kbx81
esphome/components/ssd1351_spi/* @kbx81
esphome/components/st7735/* @SenexCrenshaw
esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
esphome/components/switch/* @esphome/core

View File

@@ -24,7 +24,7 @@ TYPE_LINT = 'lint'
TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
BASE_VERSION = "3.6.0"
BASE_VERSION = "4.2.0"
parser = argparse.ArgumentParser()

View File

@@ -256,7 +256,7 @@ def show_logs(config, args, port):
run_miniterm(config, port)
return 0
if get_port_type(port) == "NETWORK" and "api" in config:
from esphome.api.client import run_logs
from esphome.components.api.client import run_logs
return run_logs(config, port)
if get_port_type(port) == "MQTT" and "mqtt" in config:
@@ -483,75 +483,9 @@ def parse_args(argv):
metavar=("key", "value"),
)
# Keep backward compatibility with the old command line format of
# esphome <config> <command>.
#
# Unfortunately this can't be done by adding another configuration argument to the
# main config parser, as argparse is greedy when parsing arguments, so in regular
# usage it'll eat the command as the configuration argument and error out out
# because it can't parse the configuration as a command.
#
# Instead, construct an ad-hoc parser for the old format that doesn't actually
# process the arguments, but parses them enough to let us figure out if the old
# format is used. In that case, swap the command and configuration in the arguments
# and continue on with the normal parser (after raising a deprecation warning).
#
# Disable argparse's built-in help option and add it manually to prevent this
# parser from printing the help messagefor the old format when invoked with -h.
compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False)
compat_parser.add_argument("-h", "--help")
compat_parser.add_argument("configuration", nargs="*")
compat_parser.add_argument(
"command",
choices=[
"config",
"compile",
"upload",
"logs",
"run",
"clean-mqtt",
"wizard",
"mqtt-fingerprint",
"version",
"clean",
"dashboard",
"vscode",
"update-all",
],
)
# on Python 3.9+ we can simply set exit_on_error=False in the constructor
def _raise(x):
raise argparse.ArgumentError(None, x)
compat_parser.error = _raise
deprecated_argv_suggestion = None
if ["dashboard", "config"] == argv[1:3] or ["version"] == argv[1:2]:
# this is most likely meant in new-style arg format. do not try compat parsing
pass
else:
try:
result, unparsed = compat_parser.parse_known_args(argv[1:])
last_option = len(argv) - len(unparsed) - 1 - len(result.configuration)
unparsed = [
"--device" if arg in ("--upload-port", "--serial-port") else arg
for arg in unparsed
]
argv = (
argv[0:last_option] + [result.command] + result.configuration + unparsed
)
deprecated_argv_suggestion = argv
except argparse.ArgumentError:
# This is not an old-style command line, so we don't have to do anything.
pass
# And continue on with regular parsing
parser = argparse.ArgumentParser(
description=f"ESPHome v{const.__version__}", parents=[options_parser]
)
parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion)
mqtt_options = argparse.ArgumentParser(add_help=False)
mqtt_options.add_argument("--topic", help="Manually set the MQTT topic.")
@@ -701,7 +635,83 @@ def parse_args(argv):
"configuration", help="Your YAML configuration file directories.", nargs="+"
)
return parser.parse_args(argv[1:])
# Keep backward compatibility with the old command line format of
# esphome <config> <command>.
#
# Unfortunately this can't be done by adding another configuration argument to the
# main config parser, as argparse is greedy when parsing arguments, so in regular
# usage it'll eat the command as the configuration argument and error out out
# because it can't parse the configuration as a command.
#
# Instead, if parsing using the current format fails, construct an ad-hoc parser
# that doesn't actually process the arguments, but parses them enough to let us
# figure out if the old format is used. In that case, swap the command and
# configuration in the arguments and retry with the normal parser (and raise
# a deprecation warning).
arguments = argv[1:]
# On Python 3.9+ we can simply set exit_on_error=False in the constructor
def _raise(x):
raise argparse.ArgumentError(None, x)
# First, try new-style parsing, but don't exit in case of failure
try:
# duplicate parser so that we can use the original one to raise errors later on
current_parser = argparse.ArgumentParser(add_help=False, parents=[parser])
current_parser.set_defaults(deprecated_argv_suggestion=None)
current_parser.error = _raise
return current_parser.parse_args(arguments)
except argparse.ArgumentError:
pass
# Second, try compat parsing and rearrange the command-line if it succeeds
# Disable argparse's built-in help option and add it manually to prevent this
# parser from printing the help messagefor the old format when invoked with -h.
compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False)
compat_parser.add_argument("-h", "--help", action="store_true")
compat_parser.add_argument("configuration", nargs="*")
compat_parser.add_argument(
"command",
choices=[
"config",
"compile",
"upload",
"logs",
"run",
"clean-mqtt",
"wizard",
"mqtt-fingerprint",
"version",
"clean",
"dashboard",
"vscode",
"update-all",
],
)
try:
compat_parser.error = _raise
result, unparsed = compat_parser.parse_known_args(argv[1:])
last_option = len(arguments) - len(unparsed) - 1 - len(result.configuration)
unparsed = [
"--device" if arg in ("--upload-port", "--serial-port") else arg
for arg in unparsed
]
arguments = (
arguments[0:last_option]
+ [result.command]
+ result.configuration
+ unparsed
)
deprecated_argv_suggestion = arguments
except argparse.ArgumentError:
# old-style parsing failed, don't suggest any argument
deprecated_argv_suggestion = None
# Finally, run the new-style parser again with the possibly swapped arguments,
# and let it error out if the command is unparsable.
parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion)
return parser.parse_args(arguments)
def run_esphome(argv):
@@ -715,7 +725,7 @@ def run_esphome(argv):
"and will be removed in the future. "
)
_LOGGER.warning("Please instead use:")
_LOGGER.warning(" esphome %s", " ".join(args.deprecated_argv_suggestion[1:]))
_LOGGER.warning(" esphome %s", " ".join(args.deprecated_argv_suggestion))
if sys.version_info < (3, 7, 0):
_LOGGER.error(

File diff suppressed because one or more lines are too long

View File

@@ -1,518 +0,0 @@
from datetime import datetime
import functools
import logging
import socket
import threading
import time
# pylint: disable=unused-import
from typing import Optional # noqa
from google.protobuf import message # noqa
from esphome import const
import esphome.api.api_pb2 as pb
from esphome.const import CONF_PASSWORD, CONF_PORT
from esphome.core import EsphomeError
from esphome.helpers import resolve_ip_address, indent
from esphome.log import color, Fore
from esphome.util import safe_print
_LOGGER = logging.getLogger(__name__)
class APIConnectionError(EsphomeError):
pass
MESSAGE_TYPE_TO_PROTO = {
1: pb.HelloRequest,
2: pb.HelloResponse,
3: pb.ConnectRequest,
4: pb.ConnectResponse,
5: pb.DisconnectRequest,
6: pb.DisconnectResponse,
7: pb.PingRequest,
8: pb.PingResponse,
9: pb.DeviceInfoRequest,
10: pb.DeviceInfoResponse,
11: pb.ListEntitiesRequest,
12: pb.ListEntitiesBinarySensorResponse,
13: pb.ListEntitiesCoverResponse,
14: pb.ListEntitiesFanResponse,
15: pb.ListEntitiesLightResponse,
16: pb.ListEntitiesSensorResponse,
17: pb.ListEntitiesSwitchResponse,
18: pb.ListEntitiesTextSensorResponse,
19: pb.ListEntitiesDoneResponse,
20: pb.SubscribeStatesRequest,
21: pb.BinarySensorStateResponse,
22: pb.CoverStateResponse,
23: pb.FanStateResponse,
24: pb.LightStateResponse,
25: pb.SensorStateResponse,
26: pb.SwitchStateResponse,
27: pb.TextSensorStateResponse,
28: pb.SubscribeLogsRequest,
29: pb.SubscribeLogsResponse,
30: pb.CoverCommandRequest,
31: pb.FanCommandRequest,
32: pb.LightCommandRequest,
33: pb.SwitchCommandRequest,
34: pb.SubscribeServiceCallsRequest,
35: pb.ServiceCallResponse,
36: pb.GetTimeRequest,
37: pb.GetTimeResponse,
}
def _varuint_to_bytes(value):
if value <= 0x7F:
return bytes([value])
ret = bytes()
while value:
temp = value & 0x7F
value >>= 7
if value:
ret += bytes([temp | 0x80])
else:
ret += bytes([temp])
return ret
def _bytes_to_varuint(value):
result = 0
bitpos = 0
for val in value:
result |= (val & 0x7F) << bitpos
bitpos += 7
if (val & 0x80) == 0:
return result
return None
# pylint: disable=too-many-instance-attributes,not-callable
class APIClient(threading.Thread):
def __init__(self, address, port, password):
threading.Thread.__init__(self)
self._address = address # type: str
self._port = port # type: int
self._password = password # type: Optional[str]
self._socket = None # type: Optional[socket.socket]
self._socket_open_event = threading.Event()
self._socket_write_lock = threading.Lock()
self._connected = False
self._authenticated = False
self._message_handlers = []
self._keepalive = 5
self._ping_timer = None
self.on_disconnect = None
self.on_connect = None
self.on_login = None
self.auto_reconnect = False
self._running_event = threading.Event()
self._stop_event = threading.Event()
@property
def stopped(self):
return self._stop_event.is_set()
def _refresh_ping(self):
if self._ping_timer is not None:
self._ping_timer.cancel()
self._ping_timer = None
def func():
self._ping_timer = None
if self._connected:
try:
self.ping()
except APIConnectionError as err:
self._fatal_error(err)
else:
self._refresh_ping()
self._ping_timer = threading.Timer(self._keepalive, func)
self._ping_timer.start()
def _cancel_ping(self):
if self._ping_timer is not None:
self._ping_timer.cancel()
self._ping_timer = None
def _close_socket(self):
self._cancel_ping()
if self._socket is not None:
self._socket.close()
self._socket = None
self._socket_open_event.clear()
self._connected = False
self._authenticated = False
self._message_handlers = []
def stop(self, force=False):
if self.stopped:
raise ValueError
if self._connected and not force:
try:
self.disconnect()
except APIConnectionError:
pass
self._close_socket()
self._stop_event.set()
if not force:
self.join()
def connect(self):
if not self._running_event.wait(0.1):
raise APIConnectionError("You need to call start() first!")
if self._connected:
self.disconnect(on_disconnect=False)
try:
ip = resolve_ip_address(self._address)
except EsphomeError as err:
_LOGGER.warning(
"Error resolving IP address of %s. Is it connected to WiFi?",
self._address,
)
_LOGGER.warning(
"(If this error persists, please set a static IP address: "
"https://esphome.io/components/wifi.html#manual-ips)"
)
raise APIConnectionError(err) from err
_LOGGER.info("Connecting to %s:%s (%s)", self._address, self._port, ip)
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.settimeout(10.0)
self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
try:
self._socket.connect((ip, self._port))
except OSError as err:
err = APIConnectionError(f"Error connecting to {ip}: {err}")
self._fatal_error(err)
raise err
self._socket.settimeout(0.1)
self._socket_open_event.set()
hello = pb.HelloRequest()
hello.client_info = f"ESPHome v{const.__version__}"
try:
resp = self._send_message_await_response(hello, pb.HelloResponse)
except APIConnectionError as err:
self._fatal_error(err)
raise err
_LOGGER.debug(
"Successfully connected to %s ('%s' API=%s.%s)",
self._address,
resp.server_info,
resp.api_version_major,
resp.api_version_minor,
)
self._connected = True
self._refresh_ping()
if self.on_connect is not None:
self.on_connect()
def _check_connected(self):
if not self._connected:
err = APIConnectionError("Must be connected!")
self._fatal_error(err)
raise err
def login(self):
self._check_connected()
if self._authenticated:
raise APIConnectionError("Already logged in!")
connect = pb.ConnectRequest()
if self._password is not None:
connect.password = self._password
resp = self._send_message_await_response(connect, pb.ConnectResponse)
if resp.invalid_password:
raise APIConnectionError("Invalid password!")
self._authenticated = True
if self.on_login is not None:
self.on_login()
def _fatal_error(self, err):
was_connected = self._connected
self._close_socket()
if was_connected and self.on_disconnect is not None:
self.on_disconnect(err)
def _write(self, data): # type: (bytes) -> None
if self._socket is None:
raise APIConnectionError("Socket closed")
# _LOGGER.debug("Write: %s", format_bytes(data))
with self._socket_write_lock:
try:
self._socket.sendall(data)
except OSError as err:
err = APIConnectionError(f"Error while writing data: {err}")
self._fatal_error(err)
raise err
def _send_message(self, msg):
# type: (message.Message) -> None
for message_type, klass in MESSAGE_TYPE_TO_PROTO.items():
if isinstance(msg, klass):
break
else:
raise ValueError
encoded = msg.SerializeToString()
_LOGGER.debug("Sending %s:\n%s", type(msg), indent(str(msg)))
req = bytes([0])
req += _varuint_to_bytes(len(encoded))
req += _varuint_to_bytes(message_type)
req += encoded
self._write(req)
def _send_message_await_response_complex(
self, send_msg, do_append, do_stop, timeout=5
):
event = threading.Event()
responses = []
def on_message(resp):
if do_append(resp):
responses.append(resp)
if do_stop(resp):
event.set()
self._message_handlers.append(on_message)
self._send_message(send_msg)
ret = event.wait(timeout)
try:
self._message_handlers.remove(on_message)
except ValueError:
pass
if not ret:
raise APIConnectionError("Timeout while waiting for message response!")
return responses
def _send_message_await_response(self, send_msg, response_type, timeout=5):
def is_response(msg):
return isinstance(msg, response_type)
return self._send_message_await_response_complex(
send_msg, is_response, is_response, timeout
)[0]
def device_info(self):
self._check_connected()
return self._send_message_await_response(
pb.DeviceInfoRequest(), pb.DeviceInfoResponse
)
def ping(self):
self._check_connected()
return self._send_message_await_response(pb.PingRequest(), pb.PingResponse)
def disconnect(self, on_disconnect=True):
self._check_connected()
try:
self._send_message_await_response(
pb.DisconnectRequest(), pb.DisconnectResponse
)
except APIConnectionError:
pass
self._close_socket()
if self.on_disconnect is not None and on_disconnect:
self.on_disconnect(None)
def _check_authenticated(self):
if not self._authenticated:
raise APIConnectionError("Must login first!")
def subscribe_logs(self, on_log, log_level=7, dump_config=False):
self._check_authenticated()
def on_msg(msg):
if isinstance(msg, pb.SubscribeLogsResponse):
on_log(msg)
self._message_handlers.append(on_msg)
req = pb.SubscribeLogsRequest(dump_config=dump_config)
req.level = log_level
self._send_message(req)
def _recv(self, amount):
ret = bytes()
if amount == 0:
return ret
while len(ret) < amount:
if self.stopped:
raise APIConnectionError("Stopped!")
if not self._socket_open_event.is_set():
raise APIConnectionError("No socket!")
try:
val = self._socket.recv(amount - len(ret))
except AttributeError as err:
raise APIConnectionError("Socket was closed") from err
except socket.timeout:
continue
except OSError as err:
raise APIConnectionError(f"Error while receiving data: {err}") from err
ret += val
return ret
def _recv_varint(self):
raw = bytes()
while not raw or raw[-1] & 0x80:
raw += self._recv(1)
return _bytes_to_varuint(raw)
def _run_once(self):
if not self._socket_open_event.wait(0.1):
return
# Preamble
if self._recv(1)[0] != 0x00:
raise APIConnectionError("Invalid preamble")
length = self._recv_varint()
msg_type = self._recv_varint()
raw_msg = self._recv(length)
if msg_type not in MESSAGE_TYPE_TO_PROTO:
_LOGGER.debug("Skipping message type %s", msg_type)
return
msg = MESSAGE_TYPE_TO_PROTO[msg_type]()
msg.ParseFromString(raw_msg)
_LOGGER.debug("Got message: %s:\n%s", type(msg), indent(str(msg)))
for msg_handler in self._message_handlers[:]:
msg_handler(msg)
self._handle_internal_messages(msg)
def run(self):
self._running_event.set()
while not self.stopped:
try:
self._run_once()
except APIConnectionError as err:
if self.stopped:
break
if self._connected:
_LOGGER.error("Error while reading incoming messages: %s", err)
self._fatal_error(err)
self._running_event.clear()
def _handle_internal_messages(self, msg):
if isinstance(msg, pb.DisconnectRequest):
self._send_message(pb.DisconnectResponse())
if self._socket is not None:
self._socket.close()
self._socket = None
self._connected = False
if self.on_disconnect is not None:
self.on_disconnect(None)
elif isinstance(msg, pb.PingRequest):
self._send_message(pb.PingResponse())
elif isinstance(msg, pb.GetTimeRequest):
resp = pb.GetTimeResponse()
resp.epoch_seconds = int(time.time())
self._send_message(resp)
def run_logs(config, address):
conf = config["api"]
port = conf[CONF_PORT]
password = conf[CONF_PASSWORD]
_LOGGER.info("Starting log output from %s using esphome API", address)
cli = APIClient(address, port, password)
stopping = False
retry_timer = []
has_connects = []
def try_connect(err, tries=0):
if stopping:
return
if err:
_LOGGER.warning("Disconnected from API: %s", err)
while retry_timer:
retry_timer.pop(0).cancel()
error = None
try:
cli.connect()
cli.login()
except APIConnectionError as err2: # noqa
error = err2
if error is None:
_LOGGER.info("Successfully connected to %s", address)
return
wait_time = int(min(1.5 ** min(tries, 100), 30))
if not has_connects:
_LOGGER.warning(
"Initial connection failed. The ESP might not be connected "
"to WiFi yet (%s). Re-Trying in %s seconds",
error,
wait_time,
)
else:
_LOGGER.warning(
"Couldn't connect to API (%s). Trying to reconnect in %s seconds",
error,
wait_time,
)
timer = threading.Timer(
wait_time, functools.partial(try_connect, None, tries + 1)
)
timer.start()
retry_timer.append(timer)
def on_log(msg):
time_ = datetime.now().time().strftime("[%H:%M:%S]")
text = msg.message
if msg.send_failed:
text = color(
Fore.WHITE,
"(Message skipped because it was too big to fit in "
"TCP buffer - This is only cosmetic)",
)
safe_print(time_ + text)
def on_login():
try:
cli.subscribe_logs(on_log, dump_config=not has_connects)
has_connects.append(True)
except APIConnectionError:
cli.disconnect()
cli.on_disconnect = try_connect
cli.on_login = on_login
cli.start()
try:
try_connect(None)
while True:
time.sleep(1)
except KeyboardInterrupt:
stopping = True
cli.stop(True)
while retry_timer:
retry_timer.pop(0).cancel()
return 0

View File

@@ -55,6 +55,7 @@ ESP8266_BOARD_PINS = {
"espectro": {"LED": 15, "BUTTON": 2},
"espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0},
"espinotee": {"LED": 16},
"espmxdevkit": {},
"espresso_lite_v1": {"LED": 16},
"espresso_lite_v2": {"LED": 2},
"gen4iod": {},
@@ -105,6 +106,10 @@ ESP8266_BOARD_PINS = {
},
"phoenix_v1": {"LED": 16},
"phoenix_v2": {"LED": 2},
"sonoff_basic": {},
"sonoff_s20": {},
"sonoff_sv": {},
"sonoff_th": {},
"sparkfunBlynk": "thing",
"thing": {"LED": 5, "SDA": 2, "SCL": 14},
"thingdev": "thing",
@@ -166,6 +171,7 @@ ESP8266_FLASH_SIZES = {
"espectro": FLASH_SIZE_4_MB,
"espino": FLASH_SIZE_4_MB,
"espinotee": FLASH_SIZE_4_MB,
"espmxdevkit": FLASH_SIZE_1_MB,
"espresso_lite_v1": FLASH_SIZE_4_MB,
"espresso_lite_v2": FLASH_SIZE_4_MB,
"gen4iod": FLASH_SIZE_512_KB,
@@ -178,6 +184,10 @@ ESP8266_FLASH_SIZES = {
"oak": FLASH_SIZE_4_MB,
"phoenix_v1": FLASH_SIZE_4_MB,
"phoenix_v2": FLASH_SIZE_4_MB,
"sonoff_basic": FLASH_SIZE_1_MB,
"sonoff_s20": FLASH_SIZE_1_MB,
"sonoff_sv": FLASH_SIZE_1_MB,
"sonoff_th": FLASH_SIZE_1_MB,
"sparkfunBlynk": FLASH_SIZE_4_MB,
"thing": FLASH_SIZE_512_KB,
"thingdev": FLASH_SIZE_512_KB,
@@ -291,6 +301,7 @@ ESP32_BOARD_PINS = {
"SW2": 2,
"SW3": 0,
},
"az-delivery-devkit-v4": {},
"bpi-bit": {
"BUTTON_A": 35,
"BUTTON_B": 27,
@@ -320,6 +331,8 @@ ESP32_BOARD_PINS = {
"RGB_LED": 4,
"TEMPERATURE_SENSOR": 34,
},
"briki_abc_esp32": {},
"briki_mbc-wb_esp32": {},
"d-duino-32": {
"D1": 5,
"D10": 1,
@@ -380,11 +393,58 @@ ESP32_BOARD_PINS = {
"esp32cam": {},
"esp32dev": {},
"esp32doit-devkit-v1": {"LED": 2},
"esp32doit-espduino": {"TX0": 1, "RX0": 3, "CMD": 11, "CLK": 6, "SD0": 7, "SD1": 8},
"esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2},
"esp32thing_plus": {
"SDA": 23,
"SCL": 22,
"SS": 33,
"MOSI": 18,
"MISO": 19,
"SCK": 5,
"A0": 26,
"A1": 25,
"A2": 34,
"A3": 39,
"A4": 36,
"A5": 4,
"A6": 14,
"A7": 32,
"A8": 15,
"A9": 33,
"A10": 27,
"A11": 12,
"A12": 13,
},
"esp32vn-iot-uno": {},
"espea32": {"BUTTON": 0, "LED": 5},
"espectro32": {"LED": 15, "SD_SS": 33},
"espino32": {"BUTTON": 0, "LED": 16},
"etboard": {
"LED_BUILTIN": 5,
"TX": 34,
"RX": 35,
"SS": 29,
"MOSI": 37,
"MISO": 31,
"SCK": 30,
"A0": 36,
"A1": 39,
"A2": 32,
"A3": 33,
"A4": 34,
"A5": 35,
"A6": 25,
"A7": 26,
"D2": 27,
"D3": 14,
"D4": 12,
"D5": 13,
"D6": 15,
"D7": 16,
"D8": 17,
"D9": 4,
},
"featheresp32": {
"A0": 26,
"A1": 25,
@@ -434,6 +494,18 @@ ESP32_BOARD_PINS = {
"SW4": 21,
},
"frogboard": {},
"healtypi4": {
"KEY_BUILTIN": 17,
"ADS1292_DRDY_PIN": 26,
"ADS1292_CS_PIN": 13,
"ADS1292_START_PIN": 14,
"ADS1292_PWDN_PIN": 27,
"AFE4490_CS_PIN": 21,
"AFE4490_DRDY_PIN": 39,
"AFE4490_PWDN_PIN": 4,
"PUSH_BUTTON": 17,
"SLIDE_SWITCH": 16,
},
"heltec_wifi_kit_32": {
"A1": 37,
"A2": 38,
@@ -444,6 +516,7 @@ ESP32_BOARD_PINS = {
"SDA_OLED": 4,
"Vext": 21,
},
"heltec_wifi_kit_32_v2": "heltec_wifi_kit_32",
"heltec_wifi_lora_32": {
"BUTTON": 0,
"DIO0": 26,
@@ -489,8 +562,68 @@ ESP32_BOARD_PINS = {
"SS": 18,
"Vext": 21,
},
"heltec_wireless_stick_lite": {
"LED_BUILTIN": 25,
"KEY_BUILTIN": 0,
"SS": 18,
"MOSI": 27,
"MISO": 19,
"SCK": 5,
"Vext": 21,
"LED": 25,
"RST_LoRa": 14,
"DIO0": 26,
"DIO1": 35,
"DIO2": 34,
},
"honeylemon": {
"LED_BUILTIN": 2,
"BUILTIN_KEY": 0,
},
"hornbill32dev": {"BUTTON": 0, "LED": 13},
"hornbill32minima": {"SS": 2},
"imbrios-logsens-v1p1": {
"LED_BUILTIN": 33,
"UART2_TX": 17,
"UART2_RX": 16,
"UART2_RTS": 4,
"CAN_TX": 17,
"CAN_RX": 16,
"CAN_TXDE": 4,
"SS": 15,
"MOSI": 13,
"MISO": 12,
"SCK": 14,
"SPI_SS1": 23,
"BUZZER_CTRL": 19,
"SD_CARD_DETECT": 35,
"SW2_BUILDIN": 0,
"SW3_BUILDIN": 36,
"SW4_BUILDIN": 34,
"LED1_BUILDIN": 32,
"LED2_BUILDIN": 33,
},
"inex_openkb": {
"LED_BUILTIN": 16,
"LDR_PIN": 36,
"SW1": 16,
"SW2": 14,
"BT_LED": 17,
"WIFI_LED": 2,
"NTP_LED": 15,
"IOT_LED": 12,
"BUZZER": 13,
"INPUT1": 32,
"INPUT2": 33,
"INPUT3": 34,
"INPUT4": 35,
"OUTPUT1": 26,
"OUTPUT2": 27,
"SDA0": 21,
"SCL0": 22,
"SDA1": 4,
"SCL1": 5,
},
"intorobot": {
"A1": 39,
"A2": 35,
@@ -528,6 +661,40 @@ ESP32_BOARD_PINS = {
"iotaap_magnolia": {},
"iotbusio": {},
"iotbusproteus": {},
"kits-edu": {},
"labplus_mpython": {
"SDA": 23,
"SCL": 22,
"P0": 33,
"P1": 32,
"P2": 35,
"P3": 34,
"P4": 39,
"P5": 0,
"P6": 16,
"P7": 17,
"P8": 26,
"P9": 25,
"P10": 36,
"P11": 2,
"P13": 18,
"P14": 19,
"P15": 21,
"P16": 5,
"P19": 22,
"P20": 23,
"P": 27,
"Y": 14,
"T": 12,
"H": 13,
"O": 15,
"N": 4,
"BTN_A": 0,
"BTN_B": 2,
"SOUND": 36,
"LIGHT": 39,
"BUZZER": 16,
},
"lolin32": {"LED": 5},
"lolin32_lite": {"LED": 22},
"lolin_d32": {"LED": 5, "_VBAT": 35},
@@ -554,6 +721,16 @@ ESP32_BOARD_PINS = {
"SDA": 12,
"SS": 18,
},
"m5stack-atom": {
"SDA": 26,
"SCL": 32,
"ADC1": 35,
"ADC2": 36,
"SS": 19,
"MOSI": 33,
"MISO": 23,
"SCK": 22,
},
"m5stack-core-esp32": {
"ADC1": 35,
"ADC2": 36,
@@ -580,6 +757,26 @@ ESP32_BOARD_PINS = {
"RXD2": 16,
"TXD2": 17,
},
"m5stack-core2": {
"SDA": 32,
"SCL": 33,
"SS": 5,
"MOSI": 23,
"MISO": 38,
"SCK": 18,
"ADC1": 35,
"ADC2": 36,
},
"m5stack-coreink": {
"SDA": 32,
"SCL": 33,
"SS": 9,
"MOSI": 23,
"MISO": 34,
"SCK": 18,
"ADC1": 35,
"ADC2": 36,
},
"m5stack-fire": {
"ADC1": 35,
"ADC2": 36,
@@ -630,6 +827,17 @@ ESP32_BOARD_PINS = {
"RXD2": 16,
"TXD2": 17,
},
"m5stack-timer-cam": {
"LED_BUILTIN": 2,
"SDA": 4,
"SCL": 13,
"SS": 5,
"MOSI": 23,
"MISO": 19,
"SCK": 18,
"ADC1": 35,
"ADC2": 36,
},
"m5stick-c": {
"ADC1": 35,
"ADC2": 36,
@@ -664,6 +872,17 @@ ESP32_BOARD_PINS = {
"RIGHT_PUTTON": 34,
"YELLOW_LED": 18,
},
"mgbot-iotik32a": {
"LED_BUILTIN": 4,
"TX2": 17,
"RX2": 16,
},
"mgbot-iotik32b": {
"LED_BUILTIN": 18,
"IR": 27,
"TX2": 17,
"RX2": 16,
},
"mhetesp32devkit": {"LED": 2},
"mhetesp32minikit": {"LED": 2},
"microduino-core-esp32": {
@@ -740,6 +959,7 @@ ESP32_BOARD_PINS = {
},
"node32s": {},
"nodemcu-32s": {"BUTTON": 0, "LED": 2},
"nscreen-32": {},
"odroid_esp32": {"ADC1": 35, "ADC2": 36, "LED": 2, "SCL": 4, "SDA": 15, "SS": 22},
"onehorse32dev": {"A1": 37, "A2": 38, "BUTTON": 0, "LED": 5},
"oroca_edubot": {
@@ -766,6 +986,10 @@ ESP32_BOARD_PINS = {
"VBAT": 35,
},
"pico32": {},
"piranha_esp32": {
"LED_BUILTIN": 2,
"KEY_BUILTIN": 0,
},
"pocket_32": {"LED": 16},
"pycom_gpy": {
"A1": 37,
@@ -778,7 +1002,14 @@ ESP32_BOARD_PINS = {
"SDA": 12,
"SS": 17,
},
"qchip": "heltec_wifi_kit_32",
"quantum": {},
"s_odi_ultra": {
"LED_BUILTIN": 2,
"LED_BUILTINB": 4,
},
"sensesiot_weizen": {},
"sg-o_airMon": {},
"sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16},
"tinypico": {},
"ttgo-lora32-v1": {
@@ -790,6 +1021,26 @@ ESP32_BOARD_PINS = {
"SCK": 5,
"SS": 18,
},
"ttgo-lora32-v2": {
"LED_BUILTIN": 22,
"KEY_BUILTIN": 0,
"SS": 18,
"MOSI": 27,
"MISO": 19,
"SCK": 5,
"A1": 37,
"A2": 38,
},
"ttgo-lora32-v21": {
"LED_BUILTIN": 25,
"KEY_BUILTIN": 0,
"SS": 18,
"MOSI": 27,
"MISO": 19,
"SCK": 5,
"A1": 37,
"A2": 38,
},
"ttgo-t-beam": {"BUTTON": 39, "LED": 14, "MOSI": 27, "SCK": 5, "SS": 18},
"ttgo-t-watch": {"BUTTON": 36, "MISO": 2, "MOSI": 15, "SCK": 14, "SS": 13},
"ttgo-t1": {"LED": 22, "MISO": 2, "MOSI": 15, "SCK": 14, "SCL": 23, "SS": 13},
@@ -855,6 +1106,32 @@ ESP32_BOARD_PINS = {
"T5": 5,
"T6": 4,
},
"wifiduino32": {
"LED_BUILTIN": 2,
"KEY_BUILTIN": 0,
"SDA": 5,
"SCL": 16,
"A0": 27,
"A1": 14,
"A2": 12,
"A3": 35,
"A4": 13,
"A5": 4,
"D0": 3,
"D1": 1,
"D2": 17,
"D3": 15,
"D4": 32,
"D5": 33,
"D6": 25,
"D7": 26,
"D8": 23,
"D9": 22,
"D10": 21,
"D11": 19,
"D12": 18,
"D13": 2,
},
"xinabox_cw02": {"LED": 27},
}

View File

@@ -44,6 +44,7 @@ void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) {
for (int led = it.size(); led-- > 0;) {
it[led].set(Color::BLACK);
}
it.schedule_show();
}
void AdalightLightEffect::apply(light::AddressableLight &it, const Color &current_color) {
@@ -133,6 +134,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL
it[led].set(Color(led_data[0], led_data[1], led_data[2], white));
}
it.schedule_show();
return CONSUMED;
}

View File

@@ -0,0 +1,23 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import esp32_ble_tracker
from esphome.const import CONF_ID
DEPENDENCIES = ["esp32_ble_tracker"]
CODEOWNERS = ["@jeromelaban"]
airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble")
AirthingsListener = airthings_ble_ns.class_(
"AirthingsListener", esp32_ble_tracker.ESPBTDeviceListener
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(AirthingsListener),
}
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield esp32_ble_tracker.register_ble_device(var, config)

View File

@@ -0,0 +1,33 @@
#include "airthings_listener.h"
#include "esphome/core/log.h"
#ifdef ARDUINO_ARCH_ESP32
namespace esphome {
namespace airthings_ble {
static const char *TAG = "airthings_ble";
bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
for (auto &it : device.get_manufacturer_datas()) {
if (it.uuid == esp32_ble_tracker::ESPBTUUID::from_uint32(0x0334)) {
if (it.data.size() < 4)
continue;
uint32_t sn = it.data[0];
sn |= ((uint32_t) it.data[1] << 8);
sn |= ((uint32_t) it.data[2] << 16);
sn |= ((uint32_t) it.data[3] << 24);
ESP_LOGD(TAG, "Found AirThings device Serial:%u (MAC: %s)", sn, device.address_str().c_str());
return true;
}
}
return false;
}
} // namespace airthings_ble
} // namespace esphome
#endif

View File

@@ -0,0 +1,20 @@
#pragma once
#ifdef ARDUINO_ARCH_ESP32
#include "esphome/core/component.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include <BLEDevice.h>
namespace esphome {
namespace airthings_ble {
class AirthingsListener : public esp32_ble_tracker::ESPBTDeviceListener {
public:
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
};
} // namespace airthings_ble
} // namespace esphome
#endif

View File

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

View File

@@ -0,0 +1,142 @@
#include "airthings_wave_plus.h"
#ifdef ARDUINO_ARCH_ESP32
namespace esphome {
namespace airthings_wave_plus {
void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_OPEN_EVT: {
if (param->open.status == ESP_GATT_OK) {
ESP_LOGI(TAG, "Connected successfully!");
}
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
ESP_LOGW(TAG, "Disconnected!");
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
this->handle = 0;
auto chr = this->parent()->get_characteristic(service_uuid, sensors_data_characteristic_uuid);
if (chr == nullptr) {
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid.to_string().c_str(),
sensors_data_characteristic_uuid.to_string().c_str());
break;
}
this->handle = chr->handle;
this->node_state = espbt::ClientState::Established;
request_read_values_();
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->handle) {
read_sensors_(param->read.value, param->read.value_len);
}
break;
}
default:
break;
}
}
void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
auto value = (WavePlusReadings *) raw_value;
if (sizeof(WavePlusReadings) <= value_len) {
ESP_LOGD(TAG, "version = %d", value->version);
if (value->version == 1) {
ESP_LOGD(TAG, "ambient light = %d", value->ambientLight);
this->humidity_sensor_->publish_state(value->humidity / 2.0f);
if (is_valid_radon_value_(value->radon)) {
this->radon_sensor_->publish_state(value->radon);
}
if (is_valid_radon_value_(value->radon_lt)) {
this->radon_long_term_sensor_->publish_state(value->radon_lt);
}
this->temperature_sensor_->publish_state(value->temperature / 100.0f);
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
if (is_valid_co2_value_(value->co2)) {
this->co2_sensor_->publish_state(value->co2);
}
if (is_valid_voc_value_(value->voc)) {
this->tvoc_sensor_->publish_state(value->voc);
}
// This instance must not stay connected
// so other clients can connect to it (e.g. the
// mobile app).
parent()->set_enabled(false);
} else {
ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version);
}
}
}
bool AirthingsWavePlus::is_valid_radon_value_(short radon) { return 0 <= radon && radon <= 16383; }
bool AirthingsWavePlus::is_valid_voc_value_(short voc) { return 0 <= voc && voc <= 16383; }
bool AirthingsWavePlus::is_valid_co2_value_(short co2) { return 0 <= co2 && co2 <= 16383; }
void AirthingsWavePlus::loop() {}
void AirthingsWavePlus::update() {
if (this->node_state != espbt::ClientState::Established) {
if (!parent()->enabled) {
ESP_LOGW(TAG, "Reconnecting to device");
parent()->set_enabled(true);
parent()->connect();
} else {
ESP_LOGW(TAG, "Connection in progress");
}
}
}
void AirthingsWavePlus::request_read_values_() {
auto status =
esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
}
}
void AirthingsWavePlus::dump_config() {
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
LOG_SENSOR(" ", "Radon", this->radon_sensor_);
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
}
AirthingsWavePlus::AirthingsWavePlus() : PollingComponent(10000) {
auto service_bt = *BLEUUID::fromString(std::string("b42e1c08-ade7-11e4-89d3-123b93f75cba")).getNative();
auto characteristic_bt = *BLEUUID::fromString(std::string("b42e2a68-ade7-11e4-89d3-123b93f75cba")).getNative();
service_uuid = espbt::ESPBTUUID::from_uuid(service_bt);
sensors_data_characteristic_uuid = espbt::ESPBTUUID::from_uuid(characteristic_bt);
}
void AirthingsWavePlus::setup() {}
} // namespace airthings_wave_plus
} // namespace esphome
#endif // ARDUINO_ARCH_ESP32

View File

@@ -0,0 +1,79 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <iterator>
#ifdef ARDUINO_ARCH_ESP32
#include <esp_gattc_api.h>
#include <BLEDevice.h>
using namespace esphome::ble_client;
namespace esphome {
namespace airthings_wave_plus {
static const char *TAG = "airthings_wave_plus";
class AirthingsWavePlus : public PollingComponent, public BLEClientNode {
public:
AirthingsWavePlus();
void setup() override;
void dump_config() override;
void update() override;
void loop() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; }
void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
protected:
bool is_valid_radon_value_(short radon);
bool is_valid_voc_value_(short voc);
bool is_valid_co2_value_(short co2);
void read_sensors_(uint8_t *value, uint16_t value_len);
void request_read_values_();
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *radon_sensor_{nullptr};
sensor::Sensor *radon_long_term_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
sensor::Sensor *co2_sensor_{nullptr};
sensor::Sensor *tvoc_sensor_{nullptr};
uint16_t handle;
espbt::ESPBTUUID service_uuid;
espbt::ESPBTUUID sensors_data_characteristic_uuid;
struct WavePlusReadings {
uint8_t version;
uint8_t humidity;
uint8_t ambientLight;
uint8_t unused01;
uint16_t radon;
uint16_t radon_lt;
uint16_t temperature;
uint16_t pressure;
uint16_t co2;
uint16_t voc;
};
};
} // namespace airthings_wave_plus
} // namespace esphome
#endif // ARDUINO_ARCH_ESP32

View File

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

View File

@@ -1,3 +1,5 @@
import base64
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
@@ -6,6 +8,7 @@ from esphome.const import (
CONF_DATA,
CONF_DATA_TEMPLATE,
CONF_ID,
CONF_KEY,
CONF_PASSWORD,
CONF_PORT,
CONF_REBOOT_TIMEOUT,
@@ -19,7 +22,7 @@ from esphome.const import (
from esphome.core import coroutine_with_priority
DEPENDENCIES = ["network"]
AUTO_LOAD = ["async_tcp"]
AUTO_LOAD = ["socket"]
CODEOWNERS = ["@OttoWinter"]
api_ns = cg.esphome_ns.namespace("api")
@@ -41,6 +44,22 @@ SERVICE_ARG_NATIVE_TYPES = {
"float[]": cg.std_vector.template(float),
"string[]": cg.std_vector.template(cg.std_string),
}
CONF_ENCRYPTION = "encryption"
def validate_encryption_key(value):
value = cv.string_strict(value)
try:
decoded = base64.b64decode(value, validate=True)
except ValueError as err:
raise cv.Invalid("Invalid key format, please check it's using base64") from err
if len(decoded) != 32:
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
# Return original data for roundtrip conversion
return value
CONFIG_SCHEMA = cv.Schema(
{
@@ -63,6 +82,11 @@ CONFIG_SCHEMA = cv.Schema(
),
}
),
cv.Optional(CONF_ENCRYPTION): cv.Schema(
{
cv.Required(CONF_KEY): validate_encryption_key,
}
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -92,6 +116,15 @@ async def to_code(config):
cg.add(var.register_user_service(trigger))
await automation.build_automation(trigger, func_args, conf)
if CONF_ENCRYPTION in config:
conf = config[CONF_ENCRYPTION]
decoded = base64.b64decode(conf[CONF_KEY])
cg.add(var.set_noise_psk(list(decoded)))
cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.1")
else:
cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API")
cg.add_global(api_ns.using)

View File

@@ -473,7 +473,8 @@ message ListEntitiesSensorResponse {
bool force_update = 8;
string device_class = 9;
SensorStateClass state_class = 10;
SensorLastResetType last_reset_type = 11;
// Last reset type removed in 2021.9.0
SensorLastResetType legacy_last_reset_type = 11;
bool disabled_by_default = 12;
}
message SensorStateResponse {

View File

@@ -2,6 +2,7 @@
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#include "esphome/core/version.h"
#include <cerrno>
#ifdef USE_DEEP_SLEEP
#include "esphome/components/deep_sleep/deep_sleep_component.h"
@@ -18,145 +19,146 @@ namespace api {
static const char *const TAG = "api.connection";
APIConnection::APIConnection(AsyncClient *client, APIServer *parent)
: client_(client), parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) {
this->client_->onError([](void *s, AsyncClient *c, int8_t error) { ((APIConnection *) s)->on_error_(error); }, this);
this->client_->onDisconnect([](void *s, AsyncClient *c) { ((APIConnection *) s)->on_disconnect_(); }, this);
this->client_->onTimeout([](void *s, AsyncClient *c, uint32_t time) { ((APIConnection *) s)->on_timeout_(time); },
this);
this->client_->onData([](void *s, AsyncClient *c, void *buf,
size_t len) { ((APIConnection *) s)->on_data_(reinterpret_cast<uint8_t *>(buf), len); },
this);
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
: parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) {
this->proto_write_buffer_.reserve(64);
this->send_buffer_.reserve(64);
this->recv_buffer_.reserve(32);
this->client_info_ = this->client_->remoteIP().toString().c_str();
#if defined(USE_API_PLAINTEXT)
helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
#elif defined(USE_API_NOISE)
helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
#else
#error "No frame helper defined"
#endif
}
void APIConnection::start() {
this->last_traffic_ = millis();
}
APIConnection::~APIConnection() { delete this->client_; }
void APIConnection::on_error_(int8_t error) { this->remove_ = true; }
void APIConnection::on_disconnect_() { this->remove_ = true; }
void APIConnection::on_timeout_(uint32_t time) { this->on_fatal_error(); }
void APIConnection::on_data_(uint8_t *buf, size_t len) {
if (len == 0 || buf == nullptr)
APIError err = helper_->init();
if (err != APIError::OK) {
on_fatal_error();
ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
return;
this->recv_buffer_.insert(this->recv_buffer_.end(), buf, buf + len);
}
void APIConnection::parse_recv_buffer_() {
if (this->recv_buffer_.empty() || this->remove_)
return;
while (!this->recv_buffer_.empty()) {
if (this->recv_buffer_[0] != 0x00) {
ESP_LOGW(TAG, "Invalid preamble from %s", this->client_info_.c_str());
this->on_fatal_error();
return;
}
uint32_t i = 1;
const uint32_t size = this->recv_buffer_.size();
uint32_t consumed;
auto msg_size_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed);
if (!msg_size_varint.has_value())
// not enough data there yet
return;
i += consumed;
uint32_t msg_size = msg_size_varint->as_uint32();
auto msg_type_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed);
if (!msg_type_varint.has_value())
// not enough data there yet
return;
i += consumed;
uint32_t msg_type = msg_type_varint->as_uint32();
if (size - i < msg_size)
// message body not fully received
return;
uint8_t *msg = &this->recv_buffer_[i];
this->read_message(msg_size, msg_type, msg);
if (this->remove_)
return;
// pop front
uint32_t total = i + msg_size;
this->recv_buffer_.erase(this->recv_buffer_.begin(), this->recv_buffer_.begin() + total);
this->last_traffic_ = millis();
}
}
void APIConnection::disconnect_client() {
this->client_->close();
this->remove_ = true;
client_info_ = helper_->getpeername();
helper_->set_log_info(client_info_);
}
void APIConnection::loop() {
if (this->remove_)
return;
if (this->next_close_) {
this->disconnect_client();
return;
}
if (!network_is_connected()) {
// when network is disconnected force disconnect immediately
// don't wait for timeout
this->on_fatal_error();
ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", client_info_.c_str());
return;
}
if (this->client_->disconnected()) {
// failsafe for disconnect logic
this->on_disconnect_();
if (this->next_close_) {
// requested a disconnect
this->helper_->close();
this->remove_ = true;
return;
}
this->parse_recv_buffer_();
APIError err = helper_->loop();
if (err != APIError::OK) {
on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
return;
}
ReadPacketBuffer buffer;
err = helper_->read_packet(&buffer);
if (err == APIError::WOULD_BLOCK) {
// pass
} else if (err != APIError::OK) {
on_fatal_error();
if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) {
ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str());
} else {
ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
}
return;
} else {
this->last_traffic_ = millis();
// read a packet
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
if (this->remove_)
return;
}
this->list_entities_iterator_.advance();
this->initial_state_iterator_.advance();
const uint32_t keepalive = 60000;
const uint32_t now = millis();
if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive
if (millis() - this->last_traffic_ > (keepalive * 5) / 2) {
ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str());
this->disconnect_client();
if (now - this->last_traffic_ > (keepalive * 5) / 2) {
on_fatal_error();
ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str());
}
} else if (millis() - this->last_traffic_ > keepalive) {
} else if (now - this->last_traffic_ > keepalive) {
this->sent_ping_ = true;
this->send_ping_request(PingRequest());
}
#ifdef USE_ESP32_CAMERA
if (this->image_reader_.available()) {
uint32_t space = this->client_->space();
// reserve 15 bytes for metadata, and at least 64 bytes of data
if (space >= 15 + 64) {
uint32_t to_send = std::min(space - 15, this->image_reader_.available());
auto buffer = this->create_buffer();
// fixed32 key = 1;
buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
// bytes data = 2;
buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
// bool done = 3;
bool done = this->image_reader_.available() == to_send;
buffer.encode_bool(3, done);
bool success = this->send_buffer(buffer, 44);
if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) {
uint32_t to_send = std::min((size_t) 1024, this->image_reader_.available());
auto buffer = this->create_buffer();
// fixed32 key = 1;
buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
// bytes data = 2;
buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
// bool done = 3;
bool done = this->image_reader_.available() == to_send;
buffer.encode_bool(3, done);
bool success = this->send_buffer(buffer, 44);
if (success) {
this->image_reader_.consume_data(to_send);
}
if (success && done) {
this->image_reader_.return_image();
}
if (success) {
this->image_reader_.consume_data(to_send);
}
if (success && done) {
this->image_reader_.return_image();
}
}
#endif
if (state_subs_at_ != -1) {
const auto &subs = this->parent_->get_state_subs();
if (state_subs_at_ >= subs.size()) {
state_subs_at_ = -1;
} else {
auto &it = subs[state_subs_at_];
SubscribeHomeAssistantStateResponse resp;
resp.entity_id = it.entity_id;
resp.attribute = it.attribute.value();
if (this->send_subscribe_home_assistant_state_response(resp)) {
state_subs_at_++;
}
}
}
}
std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) {
return App.get_name() + component_type + nameable->get_object_id();
}
DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) {
// remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response
// close will happen on next loop
ESP_LOGD(TAG, "%s requested disconnected", client_info_.c_str());
this->next_close_ = true;
DisconnectResponse resp;
return resp;
}
void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
// pass
}
#ifdef USE_BINARY_SENSOR
bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) {
if (!this->state_subscription_)
@@ -241,6 +243,9 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
#endif
#ifdef USE_FAN
// Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
bool APIConnection::send_fan_state(fan::FanState *fan) {
if (!this->state_subscription_)
return false;
@@ -295,6 +300,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
call.perform();
}
#pragma GCC diagnostic pop
#endif
#ifdef USE_LIGHT
@@ -417,7 +423,6 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) {
msg.force_update = sensor->get_force_update();
msg.device_class = sensor->get_device_class();
msg.state_class = static_cast<enums::SensorStateClass>(sensor->state_class);
msg.last_reset_type = static_cast<enums::SensorLastResetType>(sensor->last_reset_type);
msg.disabled_by_default = sensor->is_disabled_by_default();
return this->send_list_entities_sensor_response(msg);
@@ -709,8 +714,8 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin
}
HelloResponse APIConnection::hello(const HelloRequest &msg) {
this->client_info_ = msg.client_info + " (" + this->client_->remoteIP().toString().c_str();
this->client_info_ += ")";
this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")";
this->helper_->set_log_info(client_info_);
ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str());
HelloResponse resp;
@@ -727,7 +732,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
// bool invalid_password = 1;
resp.invalid_password = !correct;
if (correct) {
ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str());
ESP_LOGD(TAG, "%s: Connected successfully", this->client_info_.c_str());
this->connection_state_ = ConnectionState::AUTHENTICATED;
#ifdef USE_HOMEASSISTANT_TIME
@@ -745,9 +750,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
resp.mac_address = get_mac_address_pretty();
resp.esphome_version = ESPHOME_VERSION;
resp.compilation_time = App.get_compilation_time();
#ifdef ARDUINO_BOARD
resp.model = ARDUINO_BOARD;
#endif
resp.model = ESPHOME_BOARD;
#ifdef USE_DEEP_SLEEP
resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
#endif
@@ -775,57 +778,39 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
}
}
void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) {
for (auto &it : this->parent_->get_state_subs()) {
SubscribeHomeAssistantStateResponse resp;
resp.entity_id = it.entity_id;
resp.attribute = it.attribute.value();
if (!this->send_subscribe_home_assistant_state_response(resp)) {
this->on_fatal_error();
return;
}
}
state_subs_at_ = 0;
}
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) {
if (this->remove_)
return false;
if (!this->helper_->can_write_without_blocking())
return false;
std::vector<uint8_t> header;
header.push_back(0x00);
ProtoVarInt(buffer.get_buffer()->size()).encode(header);
ProtoVarInt(message_type).encode(header);
size_t needed_space = buffer.get_buffer()->size() + header.size();
if (needed_space > this->client_->space()) {
delay(0);
if (needed_space > this->client_->space()) {
// SubscribeLogsResponse
if (message_type != 29) {
ESP_LOGV(TAG, "Cannot send message because of TCP buffer space");
}
delay(0);
return false;
APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size());
if (err == APIError::WOULD_BLOCK)
return false;
if (err != APIError::OK) {
on_fatal_error();
if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) {
ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str());
} else {
ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
}
return false;
}
this->client_->add(reinterpret_cast<char *>(header.data()), header.size(),
ASYNC_WRITE_FLAG_COPY | ASYNC_WRITE_FLAG_MORE);
this->client_->add(reinterpret_cast<char *>(buffer.get_buffer()->data()), buffer.get_buffer()->size(),
ASYNC_WRITE_FLAG_COPY);
bool ret = this->client_->send();
return ret;
this->last_traffic_ = millis();
return true;
}
void APIConnection::on_unauthenticated_access() {
ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str());
this->on_fatal_error();
ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_info_.c_str());
}
void APIConnection::on_no_setup_connection() {
ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str());
this->on_fatal_error();
ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_info_.c_str());
}
void APIConnection::on_fatal_error() {
ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str());
this->client_->close();
this->helper_->close();
this->remove_ = true;
}

View File

@@ -5,16 +5,17 @@
#include "api_pb2.h"
#include "api_pb2_service.h"
#include "api_server.h"
#include "api_frame_helper.h"
namespace esphome {
namespace api {
class APIConnection : public APIServerConnection {
public:
APIConnection(AsyncClient *client, APIServer *parent);
virtual ~APIConnection();
APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
virtual ~APIConnection() = default;
void disconnect_client();
void start();
void loop();
bool send_list_info_done() {
@@ -86,10 +87,7 @@ class APIConnection : public APIServerConnection {
}
#endif
void on_disconnect_response(const DisconnectResponse &value) override {
// we initiated disconnect_client
this->next_close_ = true;
}
void on_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override {
// we initiated ping
this->sent_ping_ = false;
@@ -100,12 +98,7 @@ class APIConnection : public APIServerConnection {
#endif
HelloResponse hello(const HelloRequest &msg) override;
ConnectResponse connect(const ConnectRequest &msg) override;
DisconnectResponse disconnect(const DisconnectRequest &msg) override {
// remote initiated disconnect_client
this->next_close_ = true;
DisconnectResponse resp;
return resp;
}
DisconnectResponse disconnect(const DisconnectRequest &msg) override;
PingResponse ping(const PingRequest &msg) override { return {}; }
DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override;
void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
@@ -135,19 +128,16 @@ class APIConnection : public APIServerConnection {
void on_unauthenticated_access() override;
void on_no_setup_connection() override;
ProtoWriteBuffer create_buffer() override {
this->send_buffer_.clear();
return {&this->send_buffer_};
// FIXME: ensure no recursive writes can happen
this->proto_write_buffer_.clear();
return {&this->proto_write_buffer_};
}
bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
protected:
friend APIServer;
void on_error_(int8_t error);
void on_disconnect_();
void on_timeout_(uint32_t time);
void on_data_(uint8_t *buf, size_t len);
void parse_recv_buffer_();
bool send_(const void *buf, size_t len, bool force);
enum class ConnectionState {
WAITING_FOR_HELLO,
@@ -157,8 +147,10 @@ class APIConnection : public APIServerConnection {
bool remove_{false};
std::vector<uint8_t> send_buffer_;
std::vector<uint8_t> recv_buffer_;
// Buffer used to encode proto messages
// Re-use to prevent allocations
std::vector<uint8_t> proto_write_buffer_;
std::unique_ptr<APIFrameHelper> helper_;
std::string client_info_;
#ifdef USE_ESP32_CAMERA
@@ -170,12 +162,11 @@ class APIConnection : public APIServerConnection {
uint32_t last_traffic_;
bool sent_ping_{false};
bool service_call_subscription_{false};
bool current_nodelay_{false};
bool next_close_{false};
AsyncClient *client_;
bool next_close_ = false;
APIServer *parent_;
InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_;
int state_subs_at_ = -1;
};
} // namespace api

View File

@@ -0,0 +1,961 @@
#include "api_frame_helper.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "proto.h"
namespace esphome {
namespace api {
static const char *const TAG = "api.socket";
/// Is the given return value (from read/write syscalls) a wouldblock error?
bool is_would_block(ssize_t ret) {
if (ret == -1) {
return errno == EWOULDBLOCK || errno == EAGAIN;
}
return ret == 0;
}
const char *api_error_to_str(APIError err) {
// not using switch to ensure compiler doesn't try to build a big table out of it
if (err == APIError::OK) {
return "OK";
} else if (err == APIError::WOULD_BLOCK) {
return "WOULD_BLOCK";
} else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) {
return "BAD_HANDSHAKE_PACKET_LEN";
} else if (err == APIError::BAD_INDICATOR) {
return "BAD_INDICATOR";
} else if (err == APIError::BAD_DATA_PACKET) {
return "BAD_DATA_PACKET";
} else if (err == APIError::TCP_NODELAY_FAILED) {
return "TCP_NODELAY_FAILED";
} else if (err == APIError::TCP_NONBLOCKING_FAILED) {
return "TCP_NONBLOCKING_FAILED";
} else if (err == APIError::CLOSE_FAILED) {
return "CLOSE_FAILED";
} else if (err == APIError::SHUTDOWN_FAILED) {
return "SHUTDOWN_FAILED";
} else if (err == APIError::BAD_STATE) {
return "BAD_STATE";
} else if (err == APIError::BAD_ARG) {
return "BAD_ARG";
} else if (err == APIError::SOCKET_READ_FAILED) {
return "SOCKET_READ_FAILED";
} else if (err == APIError::SOCKET_WRITE_FAILED) {
return "SOCKET_WRITE_FAILED";
} else if (err == APIError::HANDSHAKESTATE_READ_FAILED) {
return "HANDSHAKESTATE_READ_FAILED";
} else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) {
return "HANDSHAKESTATE_WRITE_FAILED";
} else if (err == APIError::HANDSHAKESTATE_BAD_STATE) {
return "HANDSHAKESTATE_BAD_STATE";
} else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) {
return "CIPHERSTATE_DECRYPT_FAILED";
} else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) {
return "CIPHERSTATE_ENCRYPT_FAILED";
} else if (err == APIError::OUT_OF_MEMORY) {
return "OUT_OF_MEMORY";
} else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) {
return "HANDSHAKESTATE_SETUP_FAILED";
} else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) {
return "HANDSHAKESTATE_SPLIT_FAILED";
} else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) {
return "BAD_HANDSHAKE_ERROR_BYTE";
}
return "UNKNOWN";
}
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
// uncomment to log raw packets
//#define HELPER_LOG_PACKETS
#ifdef USE_API_NOISE
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
/// Convert a noise error code to a readable error
std::string noise_err_to_str(int err) {
if (err == NOISE_ERROR_NO_MEMORY)
return "NO_MEMORY";
if (err == NOISE_ERROR_UNKNOWN_ID)
return "UNKNOWN_ID";
if (err == NOISE_ERROR_UNKNOWN_NAME)
return "UNKNOWN_NAME";
if (err == NOISE_ERROR_MAC_FAILURE)
return "MAC_FAILURE";
if (err == NOISE_ERROR_NOT_APPLICABLE)
return "NOT_APPLICABLE";
if (err == NOISE_ERROR_SYSTEM)
return "SYSTEM";
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
return "REMOTE_KEY_REQUIRED";
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
return "LOCAL_KEY_REQUIRED";
if (err == NOISE_ERROR_PSK_REQUIRED)
return "PSK_REQUIRED";
if (err == NOISE_ERROR_INVALID_LENGTH)
return "INVALID_LENGTH";
if (err == NOISE_ERROR_INVALID_PARAM)
return "INVALID_PARAM";
if (err == NOISE_ERROR_INVALID_STATE)
return "INVALID_STATE";
if (err == NOISE_ERROR_INVALID_NONCE)
return "INVALID_NONCE";
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
return "INVALID_PRIVATE_KEY";
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
return "INVALID_PUBLIC_KEY";
if (err == NOISE_ERROR_INVALID_FORMAT)
return "INVALID_FORMAT";
if (err == NOISE_ERROR_INVALID_SIGNATURE)
return "INVALID_SIGNATURE";
return to_string(err);
}
/// Initialize the frame helper, returns OK if successful.
APIError APINoiseFrameHelper::init() {
if (state_ != State::INITIALIZE || socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
int err = socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nonblocking failed with errno %d", errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nodelay failed with errno %d", errno);
return APIError::TCP_NODELAY_FAILED;
}
// init prologue
prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT));
state_ = State::CLIENT_HELLO;
return APIError::OK;
}
/// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() {
APIError err = state_action_();
if (err == APIError::WOULD_BLOCK)
return APIError::OK;
if (err != APIError::OK)
return err;
if (!tx_buf_.empty()) {
err = try_send_tx_buf_();
if (err != APIError::OK) {
return err;
}
}
return APIError::OK;
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
*
* @param frame: The struct to hold the frame information in.
* msg_start: points to the start of the payload - this pointer is only valid until the next
* try_receive_raw_ call
*
* @return 0 if a full packet is in rx_buf_
* @return -1 if error, check errno.
*
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
* errno ENOMEM: Not enough memory for reading packet.
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
*/
APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
int err;
APIError aerr;
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
// read header
if (rx_header_buf_len_ < 3) {
// no header information yet
size_t to_read = 3 - rx_header_buf_len_;
ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
if (is_would_block(received)) {
return APIError::WOULD_BLOCK;
} else if (received == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
}
rx_header_buf_len_ += received;
if (received != to_read) {
// not a full read
return APIError::WOULD_BLOCK;
}
// header reading done
}
// read body
uint8_t indicator = rx_header_buf_[0];
if (indicator != 0x01) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", indicator);
return APIError::BAD_INDICATOR;
}
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
if (state_ != State::DATA && msg_size > 128) {
// for handshake message only permit up to 128 bytes
state_ = State::FAILED;
HELPER_LOG("Bad packet len for handshake: %d", msg_size);
return APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// reserve space for body
if (rx_buf_.size() != msg_size) {
rx_buf_.resize(msg_size);
}
if (rx_buf_len_ < msg_size) {
// more data to read
size_t to_read = msg_size - rx_buf_len_;
ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (is_would_block(received)) {
return APIError::WOULD_BLOCK;
} else if (received == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
}
rx_buf_len_ += received;
if (received != to_read) {
// not all read
return APIError::WOULD_BLOCK;
}
}
// uncomment for even more debugging
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str());
#endif
frame->msg = std::move(rx_buf_);
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
rx_header_buf_len_ = 0;
return APIError::OK;
}
/** To be called from read/write methods.
*
* This method runs through the internal handshake methods, if in that state.
*
* If the handshake is still active when this method returns and a read/write can't take place at
* the moment, returns WOULD_BLOCK.
* If an error occured, returns that error. Only returns OK if the transport is ready for data
* traffic.
*/
APIError APINoiseFrameHelper::state_action_() {
int err;
APIError aerr;
if (state_ == State::INITIALIZE) {
HELPER_LOG("Bad state for method: %d", (int) state_);
return APIError::BAD_STATE;
}
if (state_ == State::CLIENT_HELLO) {
// waiting for client hello
ParsedFrame frame;
aerr = try_read_frame_(&frame);
if (aerr == APIError::BAD_INDICATOR) {
send_explicit_handshake_reject_("Bad indicator byte");
return aerr;
}
if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
send_explicit_handshake_reject_("Bad handshake packet len");
return aerr;
}
if (aerr != APIError::OK)
return aerr;
// ignore contents, may be used in future for flags
prologue_.push_back((uint8_t)(frame.msg.size() >> 8));
prologue_.push_back((uint8_t) frame.msg.size());
prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end());
state_ = State::SERVER_HELLO;
}
if (state_ == State::SERVER_HELLO) {
// send server hello
uint8_t msg[1];
msg[0] = 0x01; // chosen proto
aerr = write_frame_(msg, 1);
if (aerr != APIError::OK)
return aerr;
// start handshake
aerr = init_handshake_();
if (aerr != APIError::OK)
return aerr;
state_ = State::HANDSHAKE;
}
if (state_ == State::HANDSHAKE) {
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) {
// waiting for handshake msg
ParsedFrame frame;
aerr = try_read_frame_(&frame);
if (aerr == APIError::BAD_INDICATOR) {
send_explicit_handshake_reject_("Bad indicator byte");
return aerr;
}
if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
send_explicit_handshake_reject_("Bad handshake packet len");
return aerr;
}
if (aerr != APIError::OK)
return aerr;
if (frame.msg.empty()) {
send_explicit_handshake_reject_("Empty handshake message");
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (frame.msg[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]);
send_explicit_handshake_reject_("Bad handshake error byte");
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
}
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1);
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str());
if (err == NOISE_ERROR_MAC_FAILURE) {
send_explicit_handshake_reject_("Handshake MAC failure");
} else {
send_explicit_handshake_reject_("Handshake error");
}
return APIError::HANDSHAKESTATE_READ_FAILED;
}
aerr = check_handshake_finished_();
if (aerr != APIError::OK)
return aerr;
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
uint8_t buffer[65];
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_WRITE_FAILED;
}
buffer[0] = 0x00; // success
aerr = write_frame_(buffer, mbuf.size + 1);
if (aerr != APIError::OK)
return aerr;
aerr = check_handshake_finished_();
if (aerr != APIError::OK)
return aerr;
} else {
// bad state for action
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
}
if (state_ == State::CLOSED || state_ == State::FAILED) {
return APIError::BAD_STATE;
}
return APIError::OK;
}
void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) {
std::vector<uint8_t> data;
data.resize(reason.length() + 1);
data[0] = 0x01; // failure
for (size_t i = 0; i < reason.length(); i++) {
data[i + 1] = (uint8_t) reason[i];
}
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
write_frame_(data.data(), data.size());
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
int err;
APIError aerr;
aerr = state_action_();
if (aerr != APIError::OK) {
return aerr;
}
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
ParsedFrame frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK)
return aerr;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size());
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str());
return APIError::CIPHERSTATE_DECRYPT_FAILED;
}
size_t msg_size = mbuf.size;
uint8_t *msg_data = frame.msg.data();
if (msg_size < 4) {
state_ = State::FAILED;
HELPER_LOG("Bad data packet: size %d too short", msg_size);
return APIError::BAD_DATA_PACKET;
}
// uint16_t type;
// uint16_t data_len;
// uint8_t *data;
// uint8_t *padding; zero or more bytes to fill up the rest of the packet
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
if (data_len > msg_size - 4) {
state_ = State::FAILED;
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
return APIError::BAD_DATA_PACKET;
}
buffer->container = std::move(frame.msg);
buffer->data_offset = 4;
buffer->data_len = data_len;
buffer->type = type;
return APIError::OK;
}
bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
int err;
APIError aerr;
aerr = state_action_();
if (aerr != APIError::OK) {
return aerr;
}
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
size_t padding = 0;
size_t msg_len = 4 + payload_len + padding;
size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_);
auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]};
if (tmpbuf == nullptr) {
HELPER_LOG("Could not allocate for writing packet");
return APIError::OUT_OF_MEMORY;
}
tmpbuf[0] = 0x01; // indicator
// tmpbuf[1], tmpbuf[2] to be set later
const uint8_t msg_offset = 3;
const uint8_t payload_offset = msg_offset + 4;
tmpbuf[msg_offset + 0] = (uint8_t)(type >> 8); // type
tmpbuf[msg_offset + 1] = (uint8_t) type;
tmpbuf[msg_offset + 2] = (uint8_t)(payload_len >> 8); // data_len
tmpbuf[msg_offset + 3] = (uint8_t) payload_len;
// copy data
std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]);
// fill padding with zeros
std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0);
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset);
err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str());
return APIError::CIPHERSTATE_ENCRYPT_FAILED;
}
size_t total_len = 3 + mbuf.size;
tmpbuf[1] = (uint8_t)(mbuf.size >> 8);
tmpbuf[2] = (uint8_t) mbuf.size;
// write raw to not have two packets sent if NAGLE disabled
aerr = write_raw_(&tmpbuf[0], total_len);
if (aerr != APIError::OK) {
return aerr;
}
return APIError::OK;
}
APIError APINoiseFrameHelper::try_send_tx_buf_() {
// try send from tx_buf
while (state_ != State::CLOSED && !tx_buf_.empty()) {
ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
if (sent == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN)
break;
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if (sent == 0) {
break;
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
}
return APIError::OK;
}
/** Write the data to the socket, or buffer it a write would block
*
* @param data The data to write
* @param len The length of data
*/
APIError APINoiseFrameHelper::write_raw_(const uint8_t *data, size_t len) {
if (len == 0)
return APIError::OK;
int err;
APIError aerr;
// uncomment for even more debugging
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str());
#endif
if (!tx_buf_.empty()) {
// try to empty tx_buf_ first
aerr = try_send_tx_buf_();
if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
return aerr;
}
if (!tx_buf_.empty()) {
// tx buf not empty, can't write now because then stream would be inconsistent
tx_buf_.insert(tx_buf_.end(), data, data + len);
return APIError::OK;
}
ssize_t sent = socket_->write(data, len);
if (is_would_block(sent)) {
// operation would block, add buffer to tx_buf
tx_buf_.insert(tx_buf_.end(), data, data + len);
return APIError::OK;
} else if (sent == -1) {
// an error occured
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if (sent != len) {
// partially sent, add end to tx_buf
tx_buf_.insert(tx_buf_.end(), data + sent, data + len);
return APIError::OK;
}
// fully sent
return APIError::OK;
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
APIError aerr;
uint8_t header[3];
header[0] = 0x01; // indicator
header[1] = (uint8_t)(len >> 8);
header[2] = (uint8_t) len;
aerr = write_raw_(header, 3);
if (aerr != APIError::OK)
return aerr;
aerr = write_raw_(data, len);
return aerr;
}
/** Initiate the data structures for the handshake.
*
* @return 0 on success, -1 on error (check errno)
*/
APIError APINoiseFrameHelper::init_handshake_() {
int err;
memset(&nid_, 0, sizeof(nid_));
// const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
// err = noise_protocol_name_to_id(&nid_, proto, strlen(proto));
nid_.pattern_id = NOISE_PATTERN_NN;
nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY;
nid_.dh_id = NOISE_DH_CURVE25519;
nid_.prefix_id = NOISE_PREFIX_STANDARD;
nid_.hybrid_id = NOISE_DH_NONE;
nid_.hash_id = NOISE_HASH_SHA256;
nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SETUP_FAILED;
}
const auto &psk = ctx_->get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SETUP_FAILED;
}
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SETUP_FAILED;
}
// set_prologue copies it into handshakestate, so we can get rid of it now
prologue_ = {};
err = noise_handshakestate_start(handshake_);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SETUP_FAILED;
}
return APIError::OK;
}
APIError APINoiseFrameHelper::check_handshake_finished_() {
assert(state_ == State::HANDSHAKE);
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
return APIError::OK;
if (action != NOISE_ACTION_SPLIT) {
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SPLIT_FAILED;
}
HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
state_ = State::DATA;
return APIError::OK;
}
APINoiseFrameHelper::~APINoiseFrameHelper() {
if (handshake_ != nullptr) {
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
}
if (send_cipher_ != nullptr) {
noise_cipherstate_free(send_cipher_);
send_cipher_ = nullptr;
}
if (recv_cipher_ != nullptr) {
noise_cipherstate_free(recv_cipher_);
recv_cipher_ = nullptr;
}
}
APIError APINoiseFrameHelper::close() {
state_ = State::CLOSED;
int err = socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError APINoiseFrameHelper::shutdown(int how) {
int err = socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
extern "C" {
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
void noise_rand_bytes(void *output, size_t len) { esphome::fill_random(reinterpret_cast<uint8_t *>(output), len); }
}
#endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT
/// Initialize the frame helper, returns OK if successful.
APIError APIPlaintextFrameHelper::init() {
if (state_ != State::INITIALIZE || socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
int err = socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nonblocking failed with errno %d", errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nodelay failed with errno %d", errno);
return APIError::TCP_NODELAY_FAILED;
}
state_ = State::DATA;
return APIError::OK;
}
/// Not used for plaintext
APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
// try send pending TX data
if (!tx_buf_.empty()) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK) {
return err;
}
}
return APIError::OK;
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
*
* @param frame: The struct to hold the frame information in.
* msg: store the parsed frame in that struct
*
* @return See APIError
*
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
*/
APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
int err;
APIError aerr;
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
// read header
while (!rx_header_parsed_) {
uint8_t data;
ssize_t received = socket_->read(&data, 1);
if (is_would_block(received)) {
return APIError::WOULD_BLOCK;
} else if (received == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
}
rx_header_buf_.push_back(data);
// try parse header
if (rx_header_buf_[0] != 0x00) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
}
size_t i = 1;
uint32_t consumed = 0;
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
if (!msg_size_varint.has_value()) {
// not enough data there yet
continue;
}
i += consumed;
rx_header_parsed_len_ = msg_size_varint->as_uint32();
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
if (!msg_type_varint.has_value()) {
// not enough data there yet
continue;
}
rx_header_parsed_type_ = msg_type_varint->as_uint32();
rx_header_parsed_ = true;
}
// header reading done
// reserve space for body
if (rx_buf_.size() != rx_header_parsed_len_) {
rx_buf_.resize(rx_header_parsed_len_);
}
if (rx_buf_len_ < rx_header_parsed_len_) {
// more data to read
size_t to_read = rx_header_parsed_len_ - rx_buf_len_;
ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (is_would_block(received)) {
return APIError::WOULD_BLOCK;
} else if (received == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
}
rx_buf_len_ += received;
if (received != to_read) {
// not all read
return APIError::WOULD_BLOCK;
}
}
// uncomment for even more debugging
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str());
#endif
frame->msg = std::move(rx_buf_);
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
rx_header_buf_.clear();
rx_header_parsed_ = false;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
int err;
APIError aerr;
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
ParsedFrame frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK)
return aerr;
buffer->container = std::move(frame.msg);
buffer->data_offset = 0;
buffer->data_len = rx_header_parsed_len_;
buffer->type = rx_header_parsed_type_;
return APIError::OK;
}
bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
int err;
APIError aerr;
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
std::vector<uint8_t> header;
header.push_back(0x00);
ProtoVarInt(payload_len).encode(header);
ProtoVarInt(type).encode(header);
aerr = write_raw_(&header[0], header.size());
if (aerr != APIError::OK) {
return aerr;
}
aerr = write_raw_(payload, payload_len);
if (aerr != APIError::OK) {
return aerr;
}
return APIError::OK;
}
APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
// try send from tx_buf
while (state_ != State::CLOSED && !tx_buf_.empty()) {
ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
if (is_would_block(sent)) {
break;
} else if (sent == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
}
return APIError::OK;
}
/** Write the data to the socket, or buffer it a write would block
*
* @param data The data to write
* @param len The length of data
*/
APIError APIPlaintextFrameHelper::write_raw_(const uint8_t *data, size_t len) {
if (len == 0)
return APIError::OK;
int err;
APIError aerr;
// uncomment for even more debugging
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str());
#endif
if (!tx_buf_.empty()) {
// try to empty tx_buf_ first
aerr = try_send_tx_buf_();
if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
return aerr;
}
if (!tx_buf_.empty()) {
// tx buf not empty, can't write now because then stream would be inconsistent
tx_buf_.insert(tx_buf_.end(), data, data + len);
return APIError::OK;
}
ssize_t sent = socket_->write(data, len);
if (is_would_block(sent)) {
// operation would block, add buffer to tx_buf
tx_buf_.insert(tx_buf_.end(), data, data + len);
return APIError::OK;
} else if (sent == -1) {
// an error occured
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if (sent != len) {
// partially sent, add end to tx_buf
tx_buf_.insert(tx_buf_.end(), data + sent, data + len);
return APIError::OK;
}
// fully sent
return APIError::OK;
}
APIError APIPlaintextFrameHelper::close() {
state_ = State::CLOSED;
int err = socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::shutdown(int how) {
int err = socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
#endif // USE_API_PLAINTEXT
} // namespace api
} // namespace esphome

View File

@@ -0,0 +1,182 @@
#pragma once
#include <cstdint>
#include <vector>
#include <deque>
#include "esphome/core/defines.h"
#ifdef USE_API_NOISE
#include "noise/protocol.h"
#endif
#include "esphome/components/socket/socket.h"
#include "api_noise_context.h"
namespace esphome {
namespace api {
struct ReadPacketBuffer {
std::vector<uint8_t> container;
uint16_t type;
size_t data_offset;
size_t data_len;
};
struct PacketBuffer {
const std::vector<uint8_t> container;
uint16_t type;
uint8_t data_offset;
uint8_t data_len;
};
enum class APIError : int {
OK = 0,
WOULD_BLOCK = 1001,
BAD_HANDSHAKE_PACKET_LEN = 1002,
BAD_INDICATOR = 1003,
BAD_DATA_PACKET = 1004,
TCP_NODELAY_FAILED = 1005,
TCP_NONBLOCKING_FAILED = 1006,
CLOSE_FAILED = 1007,
SHUTDOWN_FAILED = 1008,
BAD_STATE = 1009,
BAD_ARG = 1010,
SOCKET_READ_FAILED = 1011,
SOCKET_WRITE_FAILED = 1012,
HANDSHAKESTATE_READ_FAILED = 1013,
HANDSHAKESTATE_WRITE_FAILED = 1014,
HANDSHAKESTATE_BAD_STATE = 1015,
CIPHERSTATE_DECRYPT_FAILED = 1016,
CIPHERSTATE_ENCRYPT_FAILED = 1017,
OUT_OF_MEMORY = 1018,
HANDSHAKESTATE_SETUP_FAILED = 1019,
HANDSHAKESTATE_SPLIT_FAILED = 1020,
BAD_HANDSHAKE_ERROR_BYTE = 1021,
};
const char *api_error_to_str(APIError err);
class APIFrameHelper {
public:
virtual APIError init() = 0;
virtual APIError loop() = 0;
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
virtual bool can_write_without_blocking() = 0;
virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0;
virtual std::string getpeername() = 0;
virtual APIError close() = 0;
virtual APIError shutdown(int how) = 0;
// Give this helper a name for logging
virtual void set_log_info(std::string info) = 0;
};
#ifdef USE_API_NOISE
class APINoiseFrameHelper : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
: socket_(std::move(socket)), ctx_(ctx) {}
~APINoiseFrameHelper();
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
std::string getpeername() override { return socket_->getpeername(); }
APIError close() override;
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
protected:
struct ParsedFrame {
std::vector<uint8_t> msg;
};
APIError state_action_();
APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_();
APIError write_frame_(const uint8_t *data, size_t len);
APIError write_raw_(const uint8_t *data, size_t len);
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason);
std::unique_ptr<socket::Socket> socket_;
std::string info_;
uint8_t rx_header_buf_[3];
size_t rx_header_buf_len_ = 0;
std::vector<uint8_t> rx_buf_;
size_t rx_buf_len_ = 0;
std::vector<uint8_t> tx_buf_;
std::vector<uint8_t> prologue_;
std::shared_ptr<APINoiseContext> ctx_;
NoiseHandshakeState *handshake_ = nullptr;
NoiseCipherState *send_cipher_ = nullptr;
NoiseCipherState *recv_cipher_ = nullptr;
NoiseProtocolId nid_;
enum class State {
INITIALIZE = 1,
CLIENT_HELLO = 2,
SERVER_HELLO = 3,
HANDSHAKE = 4,
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8,
} state_ = State::INITIALIZE;
};
#endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT
class APIPlaintextFrameHelper : public APIFrameHelper {
public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
~APIPlaintextFrameHelper() = default;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
std::string getpeername() override { return socket_->getpeername(); }
APIError close() override;
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
protected:
struct ParsedFrame {
std::vector<uint8_t> msg;
};
APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_();
APIError write_raw_(const uint8_t *data, size_t len);
std::unique_ptr<socket::Socket> socket_;
std::string info_;
std::vector<uint8_t> rx_header_buf_;
bool rx_header_parsed_ = false;
uint32_t rx_header_parsed_type_ = 0;
uint32_t rx_header_parsed_len_ = 0;
std::vector<uint8_t> rx_buf_;
size_t rx_buf_len_ = 0;
std::vector<uint8_t> tx_buf_;
enum class State {
INITIALIZE = 1,
DATA = 2,
CLOSED = 3,
FAILED = 4,
} state_ = State::INITIALIZE;
};
#endif
} // namespace api
} // namespace esphome

View File

@@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
#include <array>
#include "esphome/core/defines.h"
namespace esphome {
namespace api {
#ifdef USE_API_NOISE
using psk_t = std::array<uint8_t, 32>;
class APINoiseContext {
public:
void set_psk(psk_t psk) { psk_ = std::move(psk); }
const psk_t &get_psk() const { return psk_; }
protected:
psk_t psk_;
};
#endif // USE_API_NOISE
} // namespace api
} // namespace esphome

View File

@@ -1817,7 +1817,7 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va
return true;
}
case 11: {
this->last_reset_type = value.as_enum<enums::SensorLastResetType>();
this->legacy_last_reset_type = value.as_enum<enums::SensorLastResetType>();
return true;
}
case 12: {
@@ -1879,7 +1879,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(8, this->force_update);
buffer.encode_string(9, this->device_class);
buffer.encode_enum<enums::SensorStateClass>(10, this->state_class);
buffer.encode_enum<enums::SensorLastResetType>(11, this->last_reset_type);
buffer.encode_enum<enums::SensorLastResetType>(11, this->legacy_last_reset_type);
buffer.encode_bool(12, this->disabled_by_default);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1928,8 +1928,8 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const {
out.append(proto_enum_to_string<enums::SensorStateClass>(this->state_class));
out.append("\n");
out.append(" last_reset_type: ");
out.append(proto_enum_to_string<enums::SensorLastResetType>(this->last_reset_type));
out.append(" legacy_last_reset_type: ");
out.append(proto_enum_to_string<enums::SensorLastResetType>(this->legacy_last_reset_type));
out.append("\n");
out.append(" disabled_by_default: ");

View File

@@ -510,7 +510,7 @@ class ListEntitiesSensorResponse : public ProtoMessage {
bool force_update{false};
std::string device_class{};
enums::SensorStateClass state_class{};
enums::SensorLastResetType last_reset_type{};
enums::SensorLastResetType legacy_last_reset_type{};
bool disabled_by_default{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP

View File

@@ -1,10 +1,11 @@
#include "api_server.h"
#include "api_connection.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/core/util.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#include "esphome/core/version.h"
#include <cerrno>
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
@@ -21,20 +22,45 @@ static const char *const TAG = "api";
void APIServer::setup() {
ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server...");
this->setup_controller();
this->server_ = AsyncServer(this->port_);
this->server_.setNoDelay(false);
this->server_.begin();
this->server_.onClient(
[](void *s, AsyncClient *client) {
if (client == nullptr)
return;
socket_ = socket::socket(AF_INET, SOCK_STREAM, 0);
if (socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket.");
this->mark_failed();
return;
}
int enable = 1;
int err = socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
// we can still continue
}
err = socket_->setblocking(false);
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
this->mark_failed();
return;
}
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = ESPHOME_INADDR_ANY;
server.sin_port = htons(this->port_);
err = socket_->bind((struct sockaddr *) &server, sizeof(server));
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
this->mark_failed();
return;
}
err = socket_->listen(4);
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
this->mark_failed();
return;
}
// can't print here because in lwIP thread
// ESP_LOGD(TAG, "New client connected from %s", client->remoteIP().toString().c_str());
auto *a_this = (APIServer *) s;
a_this->clients_.push_back(new APIConnection(client, a_this));
},
this);
#ifdef USE_LOGGER
if (logger::global_logger != nullptr) {
logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
@@ -59,12 +85,26 @@ void APIServer::setup() {
#endif
}
void APIServer::loop() {
// Accept new clients
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = socket_->accept((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str());
auto *conn = new APIConnection(std::move(sock), this);
clients_.push_back(conn);
conn->start();
}
// Partition clients into remove and active
auto new_end =
std::partition(this->clients_.begin(), this->clients_.end(), [](APIConnection *conn) { return !conn->remove_; });
// print disconnection messages
for (auto it = new_end; it != this->clients_.end(); ++it) {
ESP_LOGD(TAG, "Disconnecting %s", (*it)->client_info_.c_str());
ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str());
}
// only then delete the pointers, otherwise log routine
// would access freed memory

View File

@@ -4,19 +4,14 @@
#include "esphome/core/controller.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#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"
#ifdef ARDUINO_ARCH_ESP32
#include <AsyncTCP.h>
#endif
#ifdef ARDUINO_ARCH_ESP8266
#include <ESPAsyncTCP.h>
#endif
#include "api_noise_context.h"
namespace esphome {
namespace api {
@@ -35,6 +30,12 @@ class APIServer : public Component, public Controller {
void set_port(uint16_t port);
void set_password(const std::string &password);
void set_reboot_timeout(uint32_t reboot_timeout);
#ifdef USE_API_NOISE
void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(std::move(psk)); }
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
#endif // USE_API_NOISE
void handle_disconnect(APIConnection *conn);
#ifdef USE_BINARY_SENSOR
void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
@@ -86,7 +87,7 @@ class APIServer : public Component, public Controller {
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
protected:
AsyncServer server_{0};
std::unique_ptr<socket::Socket> socket_ = nullptr;
uint16_t port_{6053};
uint32_t reboot_timeout_{300000};
uint32_t last_connected_{0};
@@ -94,6 +95,10 @@ class APIServer : public Component, public Controller {
std::string password_;
std::vector<HomeAssistantStateSubscription> state_subs_;
std::vector<UserServiceDescriptor *> user_services_;
#ifdef USE_API_NOISE
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
#endif // USE_API_NOISE
};
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -0,0 +1,73 @@
import asyncio
import logging
from datetime import datetime
from typing import Optional
from aioesphomeapi import APIClient, ReconnectLogic, APIConnectionError, LogLevel
import zeroconf
from esphome.const import CONF_KEY, CONF_PORT, CONF_PASSWORD, __version__
from esphome.util import safe_print
from . import CONF_ENCRYPTION
_LOGGER = logging.getLogger(__name__)
async def async_run_logs(config, address):
conf = config["api"]
port: int = conf[CONF_PORT]
password: str = conf[CONF_PASSWORD]
noise_psk: Optional[str] = None
if CONF_ENCRYPTION in conf:
noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
_LOGGER.info("Starting log output from %s using esphome API", address)
zc = zeroconf.Zeroconf()
cli = APIClient(
asyncio.get_event_loop(),
address,
port,
password,
client_info=f"ESPHome Logs {__version__}",
noise_psk=noise_psk,
)
first_connect = True
def on_log(msg):
time_ = datetime.now().time().strftime("[%H:%M:%S]")
text = msg.message.decode("utf8", "backslashreplace")
safe_print(time_ + text)
async def on_connect():
nonlocal first_connect
try:
await cli.subscribe_logs(
on_log,
log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE,
dump_config=first_connect,
)
first_connect = False
except APIConnectionError:
cli.disconnect()
async def on_disconnect():
_LOGGER.warning("Disconnected from API")
zc = zeroconf.Zeroconf()
reconnect = ReconnectLogic(
client=cli,
on_connect=on_connect,
on_disconnect=on_disconnect,
zeroconf_instance=zc,
)
await reconnect.start()
try:
while True:
await asyncio.sleep(60)
except KeyboardInterrupt:
await reconnect.stop()
zc.close()
def run_logs(config, address):
asyncio.run(async_run_logs(config, address))

View File

@@ -19,8 +19,8 @@ from esphome.const import (
DEVICE_CLASS_VOLTAGE,
ICON_LIGHTBULB,
ICON_CURRENT_AC,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_HERTZ,
UNIT_VOLT,
UNIT_AMPERE,
@@ -94,15 +94,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t,
cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t,

View File

@@ -55,7 +55,10 @@ void BinaryFan::loop() {
ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable));
}
}
float BinaryFan::get_setup_priority() const { return setup_priority::DATA; }
// We need a higher priority than the FanState component to make sure that the traits are set
// when that component sets itself up.
float BinaryFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; }
} // namespace binary
} // namespace esphome

View File

@@ -48,6 +48,7 @@ from esphome.const import (
DEVICE_CLASS_SAFETY,
DEVICE_CLASS_SMOKE,
DEVICE_CLASS_SOUND,
DEVICE_CLASS_UPDATE,
DEVICE_CLASS_VIBRATION,
DEVICE_CLASS_WINDOW,
)
@@ -79,6 +80,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_SAFETY,
DEVICE_CLASS_SMOKE,
DEVICE_CLASS_SOUND,
DEVICE_CLASS_UPDATE,
DEVICE_CLASS_VIBRATION,
DEVICE_CLASS_WINDOW,
]

View File

@@ -1,7 +1,14 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor, esp32_ble_tracker
from esphome.const import CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_ID
from esphome.const import (
CONF_MAC_ADDRESS,
CONF_SERVICE_UUID,
CONF_IBEACON_MAJOR,
CONF_IBEACON_MINOR,
CONF_IBEACON_UUID,
CONF_ID,
)
DEPENDENCIES = ["esp32_ble_tracker"]
@@ -13,17 +20,30 @@ BLEPresenceDevice = ble_presence_ns.class_(
esp32_ble_tracker.ESPBTDeviceListener,
)
def _validate(config):
if CONF_IBEACON_MAJOR in config and CONF_IBEACON_UUID not in config:
raise cv.Invalid("iBeacon major identifier requires iBeacon UUID")
if CONF_IBEACON_MINOR in config and CONF_IBEACON_UUID not in config:
raise cv.Invalid("iBeacon minor identifier requires iBeacon UUID")
return config
CONFIG_SCHEMA = cv.All(
binary_sensor.BINARY_SENSOR_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(BLEPresenceDevice),
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t,
cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t,
cv.Optional(CONF_IBEACON_UUID): cv.uuid,
}
)
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID),
cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_IBEACON_UUID),
_validate,
)
@@ -50,5 +70,15 @@ async def to_code(config):
)
)
elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format):
uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID])
uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID])
cg.add(var.set_service_uuid128(uuid128))
if CONF_IBEACON_UUID in config:
ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(config[CONF_IBEACON_UUID]))
cg.add(var.set_ibeacon_uuid(ibeacon_uuid))
if CONF_IBEACON_MAJOR in config:
cg.add(var.set_ibeacon_major(config[CONF_IBEACON_MAJOR]))
if CONF_IBEACON_MINOR in config:
cg.add(var.set_ibeacon_minor(config[CONF_IBEACON_MINOR]))

View File

@@ -14,41 +14,78 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
public Component {
public:
void set_address(uint64_t address) {
this->by_address_ = true;
this->match_by_ = MATCH_BY_MAC_ADDRESS;
this->address_ = address;
}
void set_service_uuid16(uint16_t uuid) {
this->by_address_ = false;
this->match_by_ = MATCH_BY_SERVICE_UUID;
this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid);
}
void set_service_uuid32(uint32_t uuid) {
this->by_address_ = false;
this->match_by_ = MATCH_BY_SERVICE_UUID;
this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid);
}
void set_service_uuid128(uint8_t *uuid) {
this->by_address_ = false;
this->match_by_ = MATCH_BY_SERVICE_UUID;
this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid);
}
void set_ibeacon_uuid(uint8_t *uuid) {
this->match_by_ = MATCH_BY_IBEACON_UUID;
this->ibeacon_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid);
}
void set_ibeacon_major(uint16_t major) {
this->check_ibeacon_major_ = true;
this->ibeacon_major_ = major;
}
void set_ibeacon_minor(uint16_t minor) {
this->check_ibeacon_minor_ = true;
this->ibeacon_minor_ = minor;
}
void on_scan_end() override {
if (!this->found_)
this->publish_state(false);
this->found_ = false;
}
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
if (this->by_address_) {
if (device.address_uint64() == this->address_) {
this->publish_state(true);
this->found_ = true;
return true;
}
} else {
for (auto uuid : device.get_service_uuids()) {
if (this->uuid_ == uuid) {
this->publish_state(device.get_rssi());
switch (this->match_by_) {
case MATCH_BY_MAC_ADDRESS:
if (device.address_uint64() == this->address_) {
this->publish_state(true);
this->found_ = true;
return true;
}
}
break;
case MATCH_BY_SERVICE_UUID:
for (auto uuid : device.get_service_uuids()) {
if (this->uuid_ == uuid) {
this->publish_state(device.get_rssi());
this->found_ = true;
return true;
}
}
break;
case MATCH_BY_IBEACON_UUID:
if (!device.get_ibeacon().has_value()) {
return false;
}
auto ibeacon = device.get_ibeacon().value();
if (this->ibeacon_uuid_ != ibeacon.get_uuid()) {
return false;
}
if (this->check_ibeacon_major_ && this->ibeacon_major_ != ibeacon.get_major()) {
return false;
}
if (this->check_ibeacon_minor_ && this->ibeacon_minor_ != ibeacon.get_minor()) {
return false;
}
this->publish_state(device.get_rssi());
this->found_ = true;
return true;
}
return false;
}
@@ -56,10 +93,20 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
enum MATCH_TYPE { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID };
MATCH_TYPE match_by_;
bool found_{false};
bool by_address_{false};
uint64_t address_;
esp32_ble_tracker::ESPBTUUID uuid_;
esp32_ble_tracker::ESPBTUUID ibeacon_uuid_;
uint16_t ibeacon_major_;
bool check_ibeacon_major_;
uint16_t ibeacon_minor_;
bool check_ibeacon_minor_;
};
} // namespace ble_presence

View File

@@ -60,5 +60,5 @@ async def to_code(config):
)
)
elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format):
uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID])
uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID])
cg.add(var.set_service_uuid128(uuid128))

View File

@@ -10,7 +10,7 @@ static const char *const TAG = "bme680_bsec.sensor";
static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
BME680BSECComponent *BME680BSECComponent::instance;
BME680BSECComponent *BME680BSECComponent::instance; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void BME680BSECComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC...");
@@ -359,7 +359,7 @@ void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float va
sensor->publish_state(value);
}
void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value) {
void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) {
if (!sensor || (sensor->has_state() && sensor->state == value)) {
return;
}

View File

@@ -70,7 +70,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
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, std::string value);
void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value);
void load_state_();
void save_state_(uint8_t accuracy);

View File

@@ -16,7 +16,7 @@ static const char *const TAG = "ccs811";
return; \
}
#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICAITON_FAILED)
#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICATION_FAILED)
void CCS811Component::setup() {
// page 9 programming guide - hwid is always 0x81
@@ -38,12 +38,14 @@ void CCS811Component::setup() {
// set MEAS_MODE (page 5)
uint8_t meas_mode = 0;
uint32_t interval = this->get_update_interval();
if (interval <= 1000)
meas_mode = 1 << 4;
else if (interval <= 10000)
meas_mode = 2 << 4;
if (interval >= 60 * 1000)
meas_mode = 3 << 4; // sensor takes a reading every 60 seconds
else if (interval >= 10 * 1000)
meas_mode = 2 << 4; // sensor takes a reading every 10 seconds
else if (interval >= 1 * 1000)
meas_mode = 1 << 4; // sensor takes a reading every second
else
meas_mode = 3 << 4;
meas_mode = 4 << 4; // sensor takes a reading every 250ms
CHECKED_IO(this->write_byte(0x01, meas_mode))
@@ -51,6 +53,36 @@ void CCS811Component::setup() {
// baseline available, write to sensor
this->write_bytes(0x11, decode_uint16(*this->baseline_));
}
auto hardware_version_data = this->read_bytes<1>(0x21);
auto bootloader_version_data = this->read_bytes<2>(0x23);
auto application_version_data = this->read_bytes<2>(0x24);
uint8_t hardware_version = 0;
uint16_t bootloader_version = 0;
uint16_t application_version = 0;
if (hardware_version_data.has_value()) {
hardware_version = (*hardware_version_data)[0];
}
if (bootloader_version_data.has_value()) {
bootloader_version = encode_uint16((*bootloader_version_data)[0], (*bootloader_version_data)[1]);
}
if (application_version_data.has_value()) {
application_version = encode_uint16((*application_version_data)[0], (*application_version_data)[1]);
}
ESP_LOGD(TAG, "hardware_version=0x%x bootloader_version=0x%x application_version=0x%x\n", hardware_version,
bootloader_version, application_version);
if (this->version_ != nullptr) {
char version[20]; // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room
sprintf(version, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), (application_version >> 8 & 15),
(application_version >> 4 & 15), application_version);
ESP_LOGD(TAG, "publishing version state: %s", version);
this->version_->publish_state(version);
}
}
void CCS811Component::update() {
if (!this->status_has_data_())
@@ -117,6 +149,7 @@ void CCS811Component::dump_config() {
LOG_UPDATE_INTERVAL(this)
LOG_SENSOR(" ", "CO2 Sensor", this->co2_)
LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_)
LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_)
if (this->baseline_) {
ESP_LOGCONFIG(TAG, " Baseline: %04X", *this->baseline_);
} else {
@@ -124,7 +157,7 @@ void CCS811Component::dump_config() {
}
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICAITON_FAILED:
case COMMUNICATION_FAILED:
ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
break;
case INVALID_ID:

View File

@@ -3,6 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
@@ -12,6 +13,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice {
public:
void set_co2(sensor::Sensor *co2) { co2_ = co2; }
void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; }
void set_version(text_sensor::TextSensor *version) { version_ = version; }
void set_baseline(uint16_t baseline) { baseline_ = baseline; }
void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
@@ -34,7 +36,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice {
enum ErrorCode {
UNKNOWN,
COMMUNICAITON_FAILED,
COMMUNICATION_FAILED,
INVALID_ID,
SENSOR_REPORTED_ERROR,
APP_INVALID,
@@ -43,6 +45,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice {
sensor::Sensor *co2_{nullptr};
sensor::Sensor *tvoc_{nullptr};
text_sensor::TextSensor *version_{nullptr};
optional<uint16_t> baseline_{};
/// Input sensor for humidity reading.
sensor::Sensor *humidity_{nullptr};

View File

@@ -1,9 +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, text_sensor
from esphome.const import (
CONF_ICON,
CONF_ID,
ICON_RADIATOR,
ICON_RESTART,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_MILLION,
UNIT_PARTS_PER_BILLION,
@@ -12,9 +16,12 @@ from esphome.const import (
CONF_TEMPERATURE,
CONF_TVOC,
CONF_HUMIDITY,
CONF_VERSION,
ICON_MOLECULE_CO2,
)
AUTO_LOAD = ["text_sensor"]
CODEOWNERS = ["@habbie"]
DEPENDENCIES = ["i2c"]
ccs811_ns = cg.esphome_ns.namespace("ccs811")
@@ -30,14 +37,22 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_VERSION): text_sensor.TEXT_SENSOR_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
cv.Optional(CONF_ICON, default=ICON_RESTART): cv.icon,
}
),
cv.Optional(CONF_BASELINE): cv.hex_uint16_t,
cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor),
cv.Optional(CONF_HUMIDITY): cv.use_id(sensor.Sensor),
@@ -58,6 +73,11 @@ async def to_code(config):
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))
if CONF_VERSION in config:
sens = cg.new_Pvariable(config[CONF_VERSION][CONF_ID])
await text_sensor.register_text_sensor(sens, config[CONF_VERSION])
cg.add(var.set_version(sens))
if CONF_BASELINE in config:
cg.add(var.set_baseline(config[CONF_BASELINE]))

View File

@@ -63,6 +63,7 @@ validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True)
ClimatePreset = climate_ns.enum("ClimatePreset")
CLIMATE_PRESETS = {
"NONE": ClimatePreset.CLIMATE_PRESET_NONE,
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,

View File

@@ -494,5 +494,74 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
climate->publish_state();
}
template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &alt, const T1 &src) {
bool is_changed = alt.has_value();
alt.reset();
if (is_changed || dst != src) {
dst = src;
is_changed = true;
}
return is_changed;
}
bool Climate::set_fan_mode_(ClimateFanMode mode) {
return set_alternative(this->fan_mode, this->custom_fan_mode, mode);
}
bool Climate::set_custom_fan_mode_(const std::string &mode) {
return set_alternative(this->custom_fan_mode, this->fan_mode, mode);
}
bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); }
bool Climate::set_custom_preset_(const std::string &preset) {
return set_alternative(this->custom_preset, this->preset, preset);
}
void Climate::dump_traits_(const char *tag) {
auto traits = this->get_traits();
ESP_LOGCONFIG(tag, "ClimateTraits:");
ESP_LOGCONFIG(tag, " [x] Visual settings:");
ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature());
ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature());
ESP_LOGCONFIG(tag, " - Step: %.1f", traits.get_visual_temperature_step());
if (traits.get_supports_current_temperature())
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
if (traits.get_supports_two_point_target_temperature())
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
if (traits.get_supports_action())
ESP_LOGCONFIG(tag, " [x] Supports action");
if (!traits.get_supported_modes().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported modes:");
for (ClimateMode m : traits.get_supported_modes())
ESP_LOGCONFIG(tag, " - %s", climate_mode_to_string(m));
}
if (!traits.get_supported_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported fan modes:");
for (ClimateFanMode m : traits.get_supported_fan_modes())
ESP_LOGCONFIG(tag, " - %s", climate_fan_mode_to_string(m));
}
if (!traits.get_supported_custom_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported custom fan modes:");
for (const std::string &s : traits.get_supported_custom_fan_modes())
ESP_LOGCONFIG(tag, " - %s", s.c_str());
}
if (!traits.get_supported_presets().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported presets:");
for (ClimatePreset p : traits.get_supported_presets())
ESP_LOGCONFIG(tag, " - %s", climate_preset_to_string(p));
}
if (!traits.get_supported_custom_presets().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported custom presets:");
for (const std::string &s : traits.get_supported_custom_presets())
ESP_LOGCONFIG(tag, " - %s", s.c_str());
}
if (!traits.get_supported_swing_modes().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported swing modes:");
for (ClimateSwingMode m : traits.get_supported_swing_modes())
ESP_LOGCONFIG(tag, " - %s", climate_swing_mode_to_string(m));
}
}
} // namespace climate
} // namespace esphome

View File

@@ -245,6 +245,18 @@ class Climate : public Nameable {
protected:
friend ClimateCall;
/// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
bool set_fan_mode_(ClimateFanMode mode);
/// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
bool set_custom_fan_mode_(const std::string &mode);
/// Set preset. Reset custom preset. Return true if preset has been changed.
bool set_preset_(ClimatePreset preset);
/// Set custom preset. Reset primary preset. Return true if preset has been changed.
bool set_custom_preset_(const std::string &preset);
/** Get the default traits of this climate device.
*
* Traits are static data that encode the capabilities and static data for a climate device such as supported
@@ -270,6 +282,7 @@ class Climate : public Nameable {
void save_state_();
uint32_t hash_base() override;
void dump_traits_(const char *tag);
CallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;

View File

@@ -72,6 +72,7 @@ class ClimateTraits {
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { supported_fan_modes_ = std::move(modes); }
void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { supported_custom_fan_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
@@ -104,6 +105,7 @@ class ClimateTraits {
void set_supported_presets(std::set<ClimatePreset> presets) { supported_presets_ = std::move(presets); }
void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); }
void add_supported_custom_preset(const std::string &preset) { supported_custom_presets_.insert(preset); }
bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); }
bool get_supports_presets() const { return !supported_presets_.empty(); }
const std::set<climate::ClimatePreset> &get_supported_presets() const { return supported_presets_; }

View File

@@ -125,16 +125,19 @@ class Cover : public Nameable {
*
* This is a legacy method and may be removed later, please use `.make_call()` instead.
*/
ESPDEPRECATED("open() is deprecated, use make_call().set_command_open() instead.", "2021.9")
void open();
/** Close the cover.
*
* This is a legacy method and may be removed later, please use `.make_call()` instead.
*/
ESPDEPRECATED("close() is deprecated, use make_call().set_command_close() instead.", "2021.9")
void close();
/** Stop the cover.
*
* This is a legacy method and may be removed later, please use `.make_call()` instead.
*/
ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop() instead.", "2021.9")
void stop();
void add_on_state_callback(std::function<void()> &&f);

View File

@@ -19,7 +19,6 @@ from esphome.const import (
CONF_ICON,
CONF_ID,
CONF_INVERTED,
CONF_LAST_RESET_TYPE,
CONF_MAX_VALUE,
CONF_MIN_VALUE,
CONF_NAME,
@@ -40,8 +39,8 @@ from esphome.const import (
ICON_BLUR,
ICON_EMPTY,
ICON_THERMOMETER,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_CELSIUS,
UNIT_EMPTY,
UNIT_PERCENT,
@@ -336,8 +335,7 @@ CONFIG_SCHEMA = cv.Schema(
CONF_UNIT_OF_MEASUREMENT: UNIT_WATT_HOURS,
CONF_ACCURACY_DECIMALS: 0,
CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
CONF_LAST_RESET_TYPE: LAST_RESET_TYPE_AUTO,
CONF_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING,
},
],
): [

View File

@@ -11,8 +11,8 @@ class DemoSensor : public sensor::Sensor, public PollingComponent {
public:
void update() override {
float val = random_float();
bool is_auto = this->last_reset_type == sensor::LAST_RESET_TYPE_AUTO;
if (is_auto) {
bool increasing = this->state_class == sensor::STATE_CLASS_TOTAL_INCREASING;
if (increasing) {
float base = isnan(this->state) ? 0.0f : this->state;
this->publish_state(base + val * 10);
} else {

View File

@@ -9,9 +9,9 @@ from esphome.const import (
DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE,
ICON_EMPTY,
LAST_RESET_TYPE_NEVER,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_NONE,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_EMPTY,
UNIT_VOLT,
@@ -26,52 +26,22 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr),
cv.Optional("energy_delivered_lux"): sensor.sensor_schema(
"kWh",
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
"kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING
),
cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema(
"kWh",
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
"kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING
),
cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema(
"kWh",
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
"kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING
),
cv.Optional("energy_returned_lux"): sensor.sensor_schema(
"kWh",
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
"kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING
),
cv.Optional("energy_returned_tariff1"): sensor.sensor_schema(
"kWh",
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
"kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING
),
cv.Optional("energy_returned_tariff2"): sensor.sensor_schema(
"kWh",
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
"kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING
),
cv.Optional("total_imported_energy"): sensor.sensor_schema(
"kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE
@@ -176,20 +146,10 @@ CONFIG_SCHEMA = cv.Schema(
UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE
),
cv.Optional("gas_delivered"): sensor.sensor_schema(
"",
ICON_EMPTY,
3,
DEVICE_CLASS_GAS,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
"", ICON_EMPTY, 3, DEVICE_CLASS_GAS, STATE_CLASS_TOTAL_INCREASING
),
cv.Optional("gas_delivered_be"): sensor.sensor_schema(
"",
ICON_EMPTY,
3,
DEVICE_CLASS_GAS,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
"", ICON_EMPTY, 3, DEVICE_CLASS_GAS, STATE_CLASS_TOTAL_INCREASING
),
}
).extend(cv.COMPONENT_SCHEMA)

View File

@@ -84,6 +84,7 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
break;
}
it->schedule_show();
return true;
}

View File

@@ -82,11 +82,9 @@ bool BLEServer::create_device_characteristics_() {
this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ);
model->set_value(this->model_.value());
} else {
#ifdef ARDUINO_BOARD
BLECharacteristic *model =
this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ);
model->set_value(ARDUINO_BOARD);
#endif
model->set_value(ESPHOME_BOARD);
}
BLECharacteristic *version =

View File

@@ -108,6 +108,16 @@ def as_hex(value):
def as_hex_array(value):
value = value.replace("-", "")
cpp_array = [
f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)]
]
return cg.RawExpression(
"(uint8_t*)(const uint8_t[16]){{{}}}".format(",".join(cpp_array))
)
def as_reversed_hex_array(value):
value = value.replace("-", "")
cpp_array = [
f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)]
@@ -193,7 +203,7 @@ async def to_code(config):
elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid32_format):
cg.add(trigger.set_service_uuid32(as_hex(conf[CONF_SERVICE_UUID])))
elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid128_format):
uuid128 = as_hex_array(conf[CONF_SERVICE_UUID])
uuid128 = as_reversed_hex_array(conf[CONF_SERVICE_UUID])
cg.add(trigger.set_service_uuid128(uuid128))
if CONF_MAC_ADDRESS in conf:
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
@@ -205,7 +215,7 @@ async def to_code(config):
elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid32_format):
cg.add(trigger.set_manufacturer_uuid32(as_hex(conf[CONF_MANUFACTURER_ID])))
elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid128_format):
uuid128 = as_hex_array(conf[CONF_MANUFACTURER_ID])
uuid128 = as_reversed_hex_array(conf[CONF_MANUFACTURER_ID])
cg.add(trigger.set_manufacturer_uuid128(uuid128))
if CONF_MAC_ADDRESS in conf:
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))

View File

@@ -434,6 +434,14 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e
}
for (auto &data : this->manufacturer_datas_) {
ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(data.data).c_str());
if (this->get_ibeacon().has_value()) {
auto ibeacon = this->get_ibeacon().value();
ESP_LOGVV(TAG, " iBeacon data:");
ESP_LOGVV(TAG, " UUID: %s", ibeacon.get_uuid().to_string().c_str());
ESP_LOGVV(TAG, " Major: %u", ibeacon.get_major());
ESP_LOGVV(TAG, " Minor: %u", ibeacon.get_minor());
ESP_LOGVV(TAG, " TXPower: %d", ibeacon.get_signal_power());
}
}
for (auto &data : this->service_datas_) {
ESP_LOGVV(TAG, " Service data:");

View File

@@ -1,9 +1,10 @@
#include "esp8266_pwm.h"
#include "esphome/core/macros.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
#error ESP8266 PWM requires at least arduino_core_version 2.4.0
#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0)
#error ESP8266 PWM requires at least arduino_version 2.4.0
#endif
#include <core_esp8266_waveform.h>

View File

@@ -1,13 +1,12 @@
import re
import logging
from pathlib import Path
import subprocess
import hashlib
import datetime
import esphome.config_validation as cv
from esphome.const import (
CONF_COMPONENTS,
CONF_REF,
CONF_REFRESH,
CONF_SOURCE,
CONF_URL,
CONF_TYPE,
@@ -15,7 +14,7 @@ from esphome.const import (
CONF_PATH,
)
from esphome.core import CORE
from esphome import loader
from esphome import git, loader
_LOGGER = logging.getLogger(__name__)
@@ -23,19 +22,11 @@ DOMAIN = CONF_EXTERNAL_COMPONENTS
TYPE_GIT = "git"
TYPE_LOCAL = "local"
CONF_REFRESH = "refresh"
CONF_REF = "ref"
def validate_git_ref(value):
if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None:
raise cv.Invalid("Not a valid git ref")
return value
GIT_SCHEMA = {
cv.Required(CONF_URL): cv.url,
cv.Optional(CONF_REF): validate_git_ref,
cv.Optional(CONF_REF): cv.git_ref,
}
LOCAL_SCHEMA = {
cv.Required(CONF_PATH): cv.directory,
@@ -68,14 +59,6 @@ def validate_source_shorthand(value):
return SOURCE_SCHEMA(conf)
def validate_refresh(value: str):
if value.lower() == "always":
return validate_refresh("0s")
if value.lower() == "never":
return validate_refresh("1000y")
return cv.positive_time_period_seconds(value)
SOURCE_SCHEMA = cv.Any(
validate_source_shorthand,
cv.typed_schema(
@@ -90,7 +73,7 @@ SOURCE_SCHEMA = cv.Any(
CONFIG_SCHEMA = cv.ensure_list(
{
cv.Required(CONF_SOURCE): SOURCE_SCHEMA,
cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, validate_refresh),
cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh),
cv.Optional(CONF_COMPONENTS, default="all"): cv.Any(
"all", cv.ensure_list(cv.string)
),
@@ -102,65 +85,13 @@ async def to_code(config):
pass
def _compute_destination_path(key: str) -> Path:
base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN
h = hashlib.new("sha256")
h.update(key.encode())
return base_dir / h.hexdigest()[:8]
def _run_git_command(cmd, cwd=None):
try:
ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False)
except FileNotFoundError as err:
raise cv.Invalid(
"git is not installed but required for external_components.\n"
"Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git"
) from err
if ret.returncode != 0 and ret.stderr:
err_str = ret.stderr.decode("utf-8")
lines = [x.strip() for x in err_str.splitlines()]
if lines[-1].startswith("fatal:"):
raise cv.Invalid(lines[-1][len("fatal: ") :])
raise cv.Invalid(err_str)
def _process_git_config(config: dict, refresh) -> str:
key = f"{config[CONF_URL]}@{config.get(CONF_REF)}"
repo_dir = _compute_destination_path(key)
if not repo_dir.is_dir():
_LOGGER.info("Cloning %s", key)
_LOGGER.debug("Location: %s", repo_dir)
cmd = ["git", "clone", "--depth=1"]
if CONF_REF in config:
cmd += ["--branch", config[CONF_REF]]
cmd += ["--", config[CONF_URL], str(repo_dir)]
_run_git_command(cmd)
else:
# Check refresh needed
file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD")
# On first clone, FETCH_HEAD does not exists
if not file_timestamp.exists():
file_timestamp = Path(repo_dir / ".git" / "HEAD")
age = datetime.datetime.now() - datetime.datetime.fromtimestamp(
file_timestamp.stat().st_mtime
)
if age.total_seconds() > refresh.total_seconds:
_LOGGER.info("Updating %s", key)
_LOGGER.debug("Location: %s", repo_dir)
# Stash local changes (if any)
_run_git_command(
["git", "stash", "push", "--include-untracked"], str(repo_dir)
)
# Fetch remote ref
cmd = ["git", "fetch", "--", "origin"]
if CONF_REF in config:
cmd.append(config[CONF_REF])
_run_git_command(cmd, str(repo_dir))
# Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch)
_run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir))
repo_dir = git.clone_or_update(
url=config[CONF_URL],
ref=config.get(CONF_REF),
refresh=refresh,
domain=DOMAIN,
)
if (repo_dir / "esphome" / "components").is_dir():
components_dir = repo_dir / "esphome" / "components"

View File

@@ -15,9 +15,11 @@ from esphome.const import (
CONF_SPEED_COMMAND_TOPIC,
CONF_SPEED_STATE_TOPIC,
CONF_NAME,
CONF_ON_SPEED_SET,
CONF_ON_TURN_OFF,
CONF_ON_TURN_ON,
CONF_TRIGGER_ID,
CONF_DIRECTION,
)
from esphome.core import CORE, coroutine_with_priority
@@ -27,6 +29,12 @@ fan_ns = cg.esphome_ns.namespace("fan")
FanState = fan_ns.class_("FanState", cg.Nameable, cg.Component)
MakeFan = cg.Application.struct("MakeFan")
FanDirection = fan_ns.enum("FanDirection")
FAN_DIRECTION_ENUM = {
"FORWARD": FanDirection.FAN_DIRECTION_FORWARD,
"REVERSE": FanDirection.FAN_DIRECTION_REVERSE,
}
# Actions
TurnOnAction = fan_ns.class_("TurnOnAction", automation.Action)
TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action)
@@ -34,6 +42,10 @@ ToggleAction = fan_ns.class_("ToggleAction", automation.Action)
FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template())
FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template())
FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template())
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template())
FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
{
@@ -61,6 +73,11 @@ FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOffTrigger),
}
),
cv.Optional(CONF_ON_SPEED_SET): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger),
}
),
}
)
@@ -100,6 +117,9 @@ async def setup_fan_core_(var, config):
for conf in config.get(CONF_ON_TURN_OFF, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_SPEED_SET, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
async def register_fan(var, config):
@@ -143,6 +163,9 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args):
cv.Required(CONF_ID): cv.use_id(FanState),
cv.Optional(CONF_OSCILLATING): cv.templatable(cv.boolean),
cv.Optional(CONF_SPEED): cv.templatable(cv.int_range(1)),
cv.Optional(CONF_DIRECTION): cv.templatable(
cv.enum(FAN_DIRECTION_ENUM, upper=True)
),
}
),
)
@@ -155,9 +178,35 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args):
if CONF_SPEED in config:
template_ = await cg.templatable(config[CONF_SPEED], args, int)
cg.add(var.set_speed(template_))
if CONF_DIRECTION in config:
template_ = await cg.templatable(config[CONF_DIRECTION], args, FanDirection)
cg.add(var.set_direction(template_))
return var
@automation.register_condition(
"fan.is_on",
FanIsOnCondition,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(FanState),
}
),
)
@automation.register_condition(
"fan.is_off",
FanIsOffCondition,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(FanState),
}
),
)
async def fan_is_on_off_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)
@coroutine_with_priority(100.0)
async def to_code(config):
cg.add_define("USE_FAN")

View File

@@ -13,6 +13,7 @@ template<typename... Ts> class TurnOnAction : public Action<Ts...> {
TEMPLATABLE_VALUE(bool, oscillating)
TEMPLATABLE_VALUE(int, speed)
TEMPLATABLE_VALUE(FanDirection, direction)
void play(Ts... x) override {
auto call = this->state_->turn_on();
@@ -22,6 +23,9 @@ template<typename... Ts> class TurnOnAction : public Action<Ts...> {
if (this->speed_.has_value()) {
call.set_speed(this->speed_.value(x...));
}
if (this->direction_.has_value()) {
call.set_direction(this->direction_.value(x...));
}
call.perform();
}
@@ -46,6 +50,23 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> {
FanState *state_;
};
template<typename... Ts> class FanIsOnCondition : public Condition<Ts...> {
public:
explicit FanIsOnCondition(FanState *state) : state_(state) {}
bool check(Ts... x) override { return this->state_->state; }
protected:
FanState *state_;
};
template<typename... Ts> class FanIsOffCondition : public Condition<Ts...> {
public:
explicit FanIsOffCondition(FanState *state) : state_(state) {}
bool check(Ts... x) override { return !this->state_->state; }
protected:
FanState *state_;
};
class FanTurnOnTrigger : public Trigger<> {
public:
FanTurnOnTrigger(FanState *state) {
@@ -82,5 +103,23 @@ class FanTurnOffTrigger : public Trigger<> {
bool last_on_;
};
class FanSpeedSetTrigger : public Trigger<> {
public:
FanSpeedSetTrigger(FanState *state) {
state->add_on_state_callback([this, state]() {
auto speed = state->speed;
auto should_trigger = speed != !this->last_speed_;
this->last_speed_ = speed;
if (should_trigger) {
this->trigger();
}
});
this->last_speed_ = state->speed;
}
protected:
int last_speed_;
};
} // namespace fan
} // namespace esphome

View File

@@ -4,6 +4,9 @@
namespace esphome {
namespace fan {
// This whole file is deprecated, don't warn about usage of deprecated types in here.
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) {
const auto speed_ratio = static_cast<float>(speed_level) / (supported_speed_levels + 1);
const auto legacy_level = clamp<int>(static_cast<int>(ceilf(speed_ratio * 3)), 1, 3);

View File

@@ -4,8 +4,16 @@
namespace esphome {
namespace fan {
// Shut-up about usage of deprecated FanSpeed for a bit.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
ESPDEPRECATED("FanSpeed and speed_level_to_enum() are deprecated.", "2021.9")
FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels);
ESPDEPRECATED("FanSpeed and speed_enum_to_level() are deprecated.", "2021.9")
int speed_enum_to_level(FanSpeed speed, int supported_speed_levels);
#pragma GCC diagnostic pop
} // namespace fan
} // namespace esphome

View File

@@ -39,7 +39,7 @@ void FanState::setup() {
call.set_direction(recovered.direction);
call.perform();
}
float FanState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; }
uint32_t FanState::hash_base() { return 418001110UL; }
void FanStateCall::perform() const {
@@ -67,6 +67,8 @@ void FanStateCall::perform() const {
this->state_->state_callback_.call();
}
// This whole method is deprecated, don't warn about usage of deprecated methods inside of it.
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
FanStateCall &FanStateCall::set_speed(const char *legacy_speed) {
const auto supported_speed_count = this->state_->get_traits().supported_speed_count();
if (strcasecmp(legacy_speed, "low") == 0) {

View File

@@ -10,7 +10,7 @@ namespace esphome {
namespace fan {
/// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon
enum FanSpeed {
enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed {
FAN_SPEED_LOW = 0, ///< The fan is running on low speed.
FAN_SPEED_MEDIUM = 1, ///< The fan is running on medium speed.
FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed.
@@ -45,6 +45,7 @@ class FanStateCall {
this->speed_ = speed;
return *this;
}
ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9")
FanStateCall &set_speed(const char *legacy_speed);
FanStateCall &set_direction(FanDirection direction) {
this->direction_ = direction;

View File

@@ -20,13 +20,12 @@ void FastLEDLightOutput::dump_config() {
ESP_LOGCONFIG(TAG, " Num LEDs: %u", this->num_leds_);
ESP_LOGCONFIG(TAG, " Max refresh rate: %u", *this->max_refresh_rate_);
}
void FastLEDLightOutput::loop() {
if (!this->should_show_())
return;
uint32_t now = micros();
void FastLEDLightOutput::write_state(light::LightState *state) {
// protect from refreshing too often
uint32_t now = micros();
if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) {
// try again next loop iteration, so that this change won't get lost
this->schedule_show();
return;
}
this->last_refresh_ = now;

View File

@@ -213,7 +213,7 @@ class FastLEDLightOutput : public light::AddressableLight {
}
void setup() override;
void dump_config() override;
void loop() override;
void write_state(light::LightState *state) override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void clear_effect_data() override {

View File

@@ -297,12 +297,6 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) {
}
}
// Validate footer
if (!data.expect_mark(FUJITSU_GENERAL_BIT_MARK)) {
ESP_LOGV(TAG, "Footer fail");
return false;
}
for (uint8_t byte = 0; byte < recv_message_length; ++byte) {
ESP_LOGVV(TAG, "%02X", recv_message[byte]);
}

View File

@@ -13,9 +13,8 @@ from esphome.const import (
DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE,
ICON_CURRENT_AC,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_NONE,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_DEGREES,
UNIT_HERTZ,
@@ -143,25 +142,23 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_TOTAL_GENERATION_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_HOURS,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_TODAY_GENERATION_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_MINUTE,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES,

View File

@@ -0,0 +1,3 @@
import esphome.codegen as cg
hbridge_ns = cg.esphome_ns.namespace("hbridge")

View File

@@ -0,0 +1,70 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.automation import maybe_simple_id
from esphome.components import fan, output
from esphome.const import (
CONF_ID,
CONF_DECAY_MODE,
CONF_SPEED_COUNT,
CONF_PIN_A,
CONF_PIN_B,
CONF_ENABLE_PIN,
)
from .. import hbridge_ns
CODEOWNERS = ["@WeekendWarrior"]
HBridgeFan = hbridge_ns.class_("HBridgeFan", fan.FanState)
DecayMode = hbridge_ns.enum("DecayMode")
DECAY_MODE_OPTIONS = {
"SLOW": DecayMode.DECAY_MODE_SLOW,
"FAST": DecayMode.DECAY_MODE_FAST,
}
# Actions
BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action)
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan),
cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput),
cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput),
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
DECAY_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
}
).extend(cv.COMPONENT_SCHEMA)
@automation.register_action(
"fan.hbridge.brake",
BrakeAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(HBridgeFan)}),
)
async def fan_hbridge_brake_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)
async def to_code(config):
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_SPEED_COUNT],
config[CONF_DECAY_MODE],
)
await fan.register_fan(var, config)
pin_a_ = await cg.get_variable(config[CONF_PIN_A])
cg.add(var.set_pin_a(pin_a_))
pin_b_ = await cg.get_variable(config[CONF_PIN_B])
cg.add(var.set_pin_b(pin_b_))
if CONF_ENABLE_PIN in config:
enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN])
cg.add(var.set_enable_pin(enable_pin))

View File

@@ -0,0 +1,85 @@
#include "hbridge_fan.h"
#include "esphome/components/fan/fan_helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace hbridge {
static const char *const TAG = "fan.hbridge";
void HBridgeFan::set_hbridge_levels_(float a_level, float b_level) {
this->pin_a_->set_level(a_level);
this->pin_b_->set_level(b_level);
ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f", a_level, b_level);
}
// constant IN1/IN2, PWM on EN => power control, fast current decay
// constant IN1/EN, PWM on IN2 => power control, slow current decay
void HBridgeFan::set_hbridge_levels_(float a_level, float b_level, float enable) {
this->pin_a_->set_level(a_level);
this->pin_b_->set_level(b_level);
this->enable_->set_level(enable);
ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f, enable: %.2f", a_level, b_level, enable);
}
fan::FanStateCall HBridgeFan::brake() {
ESP_LOGD(TAG, "Braking");
(this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f) : this->set_hbridge_levels_(1.0f, 1.0f, 1.0f);
return this->make_call().set_state(false);
}
void HBridgeFan::dump_config() {
ESP_LOGCONFIG(TAG, "Fan '%s':", this->get_name().c_str());
if (this->get_traits().supports_oscillation()) {
ESP_LOGCONFIG(TAG, " Oscillation: YES");
}
if (this->get_traits().supports_direction()) {
ESP_LOGCONFIG(TAG, " Direction: YES");
}
if (this->decay_mode_ == DECAY_MODE_SLOW) {
ESP_LOGCONFIG(TAG, " Decay Mode: Slow");
} else {
ESP_LOGCONFIG(TAG, " Decay Mode: Fast");
}
}
void HBridgeFan::setup() {
auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
this->set_traits(traits);
this->add_on_state_callback([this]() { this->next_update_ = true; });
}
void HBridgeFan::loop() {
if (!this->next_update_) {
return;
}
this->next_update_ = false;
float speed = 0.0f;
if (this->state) {
speed = static_cast<float>(this->speed) / static_cast<float>(this->speed_count_);
}
if (speed == 0.0f) { // off means idle
(this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, speed)
: this->set_hbridge_levels_(speed, speed, speed);
return;
}
if (this->direction == fan::FAN_DIRECTION_FORWARD) {
if (this->decay_mode_ == DECAY_MODE_SLOW) {
(this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f - speed, 1.0f)
: this->set_hbridge_levels_(1.0f - speed, 1.0f, 1.0f);
} else { // DECAY_MODE_FAST
(this->enable_ == nullptr) ? this->set_hbridge_levels_(0.0f, speed)
: this->set_hbridge_levels_(0.0f, 1.0f, speed);
}
} else { // fan::FAN_DIRECTION_REVERSE
if (this->decay_mode_ == DECAY_MODE_SLOW) {
(this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f - speed)
: this->set_hbridge_levels_(1.0f, 1.0f - speed, 1.0f);
} else { // DECAY_MODE_FAST
(this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, 0.0f)
: this->set_hbridge_levels_(1.0f, 0.0f, speed);
}
}
}
} // namespace hbridge
} // namespace esphome

View File

@@ -0,0 +1,58 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/components/output/binary_output.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/fan/fan_state.h"
namespace esphome {
namespace hbridge {
enum DecayMode {
DECAY_MODE_SLOW = 0,
DECAY_MODE_FAST = 1,
};
class HBridgeFan : public fan::FanState {
public:
HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
fan::FanStateCall brake();
int get_speed_count() { return this->speed_count_; }
// update Hbridge without a triggered FanState change, eg. for acceleration/deceleration ramping
void internal_update() { this->next_update_ = true; }
protected:
output::FloatOutput *pin_a_;
output::FloatOutput *pin_b_;
output::FloatOutput *enable_{nullptr};
output::BinaryOutput *oscillating_{nullptr};
bool next_update_{true};
int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW};
void set_hbridge_levels_(float a_level, float b_level);
void set_hbridge_levels_(float a_level, float b_level, float enable);
};
template<typename... Ts> class BrakeAction : public Action<Ts...> {
public:
explicit BrakeAction(HBridgeFan *parent) : parent_(parent) {}
void play(Ts... x) override { this->parent_->brake(); }
HBridgeFan *parent_;
};
} // namespace hbridge
} // namespace esphome

View File

@@ -2,8 +2,10 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import light, output
from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B
from .. import hbridge_ns
CODEOWNERS = ["@DotNetDann"]
hbridge_ns = cg.esphome_ns.namespace("hbridge")
HBridgeLightOutput = hbridge_ns.class_(
"HBridgeLightOutput", cg.PollingComponent, light.LightOutput
)

View File

@@ -18,8 +18,8 @@ from esphome.const import (
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_VOLT,
UNIT_AMPERE,
UNIT_WATT,
@@ -78,8 +78,7 @@ CONFIG_SCHEMA = cv.Schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance,
cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float,

View File

@@ -6,6 +6,10 @@ from esphome.const import (
CONF_PM_2_5,
CONF_PM_10_0,
CONF_PM_1_0,
DEVICE_CLASS_AQI,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
DEVICE_CLASS_PM25,
STATE_CLASS_MEASUREMENT,
UNIT_MICROGRAMS_PER_CUBIC_METER,
ICON_CHEMICAL_WEAPON,
@@ -45,24 +49,28 @@ CONFIG_SCHEMA = cv.All(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM1,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM10,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AQI): sensor.sensor_schema(
unit_of_measurement=UNIT_INDEX,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{

View File

@@ -42,7 +42,7 @@ CONFIG_SCHEMA = cv.All(
}
)
.extend(cv.polling_component_schema("1s"))
.extend(spi.spi_device_schema()),
.extend(spi.spi_device_schema(False)),
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
)

View File

@@ -7,6 +7,7 @@ from esphome.const import (
CONF_DEFAULT_TRANSITION_LENGTH,
CONF_DISABLED_BY_DEFAULT,
CONF_EFFECTS,
CONF_FLASH_TRANSITION_LENGTH,
CONF_GAMMA_CORRECT,
CONF_ID,
CONF_INTERNAL,
@@ -85,6 +86,9 @@ BRIGHTNESS_ONLY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend(
cv.Optional(
CONF_DEFAULT_TRANSITION_LENGTH, default="1s"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_FLASH_TRANSITION_LENGTH, default="0s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_EFFECTS): validate_effects(MONOCHROMATIC_EFFECTS),
}
)
@@ -132,6 +136,10 @@ async def setup_light_core_(light_var, output_var, config):
config[CONF_DEFAULT_TRANSITION_LENGTH]
)
)
if CONF_FLASH_TRANSITION_LENGTH in config:
cg.add(
light_var.set_flash_transition_length(config[CONF_FLASH_TRANSITION_LENGTH])
)
if CONF_GAMMA_CORRECT in config:
cg.add(light_var.set_gamma_correct(config[CONF_GAMMA_CORRECT]))
effects = await cg.build_registry_list(

View File

@@ -12,14 +12,13 @@ void AddressableLight::call_setup() {
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
this->set_interval(5000, [this]() {
const char *name = this->state_parent_ == nullptr ? "" : this->state_parent_->get_name().c_str();
ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s next_show=%s)", name, YESNO(this->effect_active_),
YESNO(this->next_show_));
ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s)", name, YESNO(this->effect_active_));
for (int i = 0; i < this->size(); i++) {
auto color = this->get(i);
ESP_LOGVV(TAG, " [%2d] Color: R=%3u G=%3u B=%3u W=%3u", i, color.get_red_raw(), color.get_green_raw(),
color.get_blue_raw(), color.get_white_raw());
}
ESP_LOGVV(TAG, "");
ESP_LOGVV(TAG, " ");
});
#endif
}
@@ -36,7 +35,7 @@ Color esp_color_from_light_color_values(LightColorValues val) {
return Color(r, g, b, w);
}
void AddressableLight::write_state(LightState *state) {
void AddressableLight::update_state(LightState *state) {
auto val = state->current_values;
auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state());
this->correction_.set_local_brightness(max_brightness);
@@ -46,9 +45,14 @@ void AddressableLight::write_state(LightState *state) {
// don't use LightState helper, gamma correction+brightness is handled by ESPColorView
this->all() = esp_color_from_light_color_values(val);
this->schedule_show();
}
void AddressableLightTransformer::start() {
// don't try to transition over running effects.
if (this->light_.is_effect_active())
return;
auto end_values = this->target_values_;
this->target_color_ = esp_color_from_light_color_values(end_values);

View File

@@ -51,9 +51,9 @@ class AddressableLight : public LightOutput, public Component {
amnt = this->size();
this->range(amnt, this->size()) = this->range(0, -amnt);
}
// Indicates whether an effect that directly updates the output buffer is active to prevent overwriting
bool is_effect_active() const { return this->effect_active_; }
void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; }
void write_state(LightState *state) override;
std::unique_ptr<LightTransformer> create_default_transition() override;
void set_correction(float red, float green, float blue, float white = 1.0f) {
this->correction_.set_max_brightness(
@@ -63,7 +63,8 @@ class AddressableLight : public LightOutput, public Component {
this->correction_.calculate_gamma_table(state->get_gamma_correct());
this->state_parent_ = state;
}
void schedule_show() { this->next_show_ = true; }
void update_state(LightState *state) override;
void schedule_show() { this->state_parent_->next_write_ = true; }
#ifdef USE_POWER_SUPPLY
void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); }
@@ -74,9 +75,7 @@ class AddressableLight : public LightOutput, public Component {
protected:
friend class AddressableLightTransformer;
bool should_show_() const { return this->effect_active_ || this->next_show_; }
void mark_shown_() {
this->next_show_ = false;
#ifdef USE_POWER_SUPPLY
for (auto c : *this) {
if (c.get().is_on()) {
@@ -90,7 +89,6 @@ class AddressableLight : public LightOutput, public Component {
virtual ESPColorView get_view_internal(int32_t index) const = 0;
bool effect_active_{false};
bool next_show_{true};
ESPColorCorrection correction_{};
#ifdef USE_POWER_SUPPLY
power_supply::PowerSupplyRequester power_;

View File

@@ -63,6 +63,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect {
this->last_run_ = now;
this->f_(it, current_color, this->initial_run_);
this->initial_run_ = false;
it.schedule_show();
}
}
@@ -87,6 +88,7 @@ class AddressableRainbowLightEffect : public AddressableLightEffect {
var = hsv;
hue += add;
}
it.schedule_show();
}
void set_speed(uint32_t speed) { this->speed_ = speed; }
void set_width(uint16_t width) { this->width_ = width; }
@@ -134,6 +136,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
new_color.b = c.b;
}
}
it.schedule_show();
}
protected:
@@ -151,25 +154,27 @@ class AddressableScanEffect : public AddressableLightEffect {
void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; }
void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; }
void apply(AddressableLight &it, const Color &current_color) override {
it.all() = Color::BLACK;
const uint32_t now = millis();
if (now - this->last_move_ < this->move_interval_)
return;
if (direction_) {
this->at_led_++;
if (this->at_led_ == it.size() - this->scan_width_)
this->direction_ = false;
} else {
this->at_led_--;
if (this->at_led_ == 0)
this->direction_ = true;
}
this->last_move_ = now;
it.all() = Color::BLACK;
for (auto i = 0; i < this->scan_width_; i++) {
it[this->at_led_ + i] = current_color;
}
const uint32_t now = millis();
if (now - this->last_move_ > this->move_interval_) {
if (direction_) {
this->at_led_++;
if (this->at_led_ == it.size() - this->scan_width_)
this->direction_ = false;
} else {
this->at_led_--;
if (this->at_led_ == 0)
this->direction_ = true;
}
this->last_move_ = now;
}
it.schedule_show();
}
protected:
@@ -210,6 +215,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect {
continue;
addressable[pos].set_effect_data(1);
}
addressable.schedule_show();
}
void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; }
void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; }
@@ -257,6 +263,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect {
const uint8_t color = random_uint32() & 0b111;
it[pos].set_effect_data(0b1000 | color);
}
it.schedule_show();
}
void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; }
void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; }
@@ -301,6 +308,7 @@ class AddressableFireworksEffect : public AddressableLightEffect {
it[pos] = current_color;
}
}
it.schedule_show();
}
void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
void set_spark_probability(float spark_probability) { this->spark_probability_ = spark_probability; }
@@ -335,6 +343,7 @@ class AddressableFlickerEffect : public AddressableLightEffect {
// slowly fade back to "real" value
var = (var.get() * inv_intensity) + (current_color * intensity);
}
it.schedule_show();
}
void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
void set_intensity(float intensity) { this->intensity_ = to_uint8_scale(intensity); }

View File

@@ -156,7 +156,7 @@ class StrobeLightEffect : public LightEffect {
if (!color.is_on()) {
// Don't turn the light off, otherwise the light effect will be stopped
call.set_brightness_if_supported(0.0f);
call.set_brightness(0.0f);
call.set_state(true);
}
call.set_publish(false);
@@ -196,7 +196,6 @@ class FlickerLightEffect : public LightEffect {
out.set_warm_white(remote.get_warm_white() * beta + current.get_warm_white() * alpha +
(random_cubic_float() * this->intensity_));
auto traits = this->state_->get_traits();
auto call = this->state_->make_call();
call.set_publish(false);
call.set_save(false);

View File

@@ -52,7 +52,7 @@ enum class ColorMode : uint8_t {
/// Only on/off control.
ON_OFF = (uint8_t) ColorCapability::ON_OFF,
/// Dimmable light.
BRIGHTNESS = (uint8_t) ColorCapability::BRIGHTNESS,
BRIGHTNESS = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS),
/// White output only (use only if the light also has another color mode such as RGB).
WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::WHITE),
/// Controllable color temperature output.

View File

@@ -8,26 +8,23 @@ namespace light {
static const char *const TAG = "light";
static const char *color_mode_to_human(ColorMode color_mode) {
switch (color_mode) {
case ColorMode::UNKNOWN:
return "Unknown";
case ColorMode::WHITE:
return "White";
case ColorMode::COLOR_TEMPERATURE:
return "Color temperature";
case ColorMode::COLD_WARM_WHITE:
return "Cold/warm white";
case ColorMode::RGB:
return "RGB";
case ColorMode::RGB_WHITE:
return "RGBW";
case ColorMode::RGB_COLD_WARM_WHITE:
return "RGB + cold/warm white";
case ColorMode::RGB_COLOR_TEMPERATURE:
return "RGB + color temperature";
default:
return "";
}
if (color_mode == ColorMode::UNKNOWN)
return "Unknown";
if (color_mode == ColorMode::WHITE)
return "White";
if (color_mode == ColorMode::COLOR_TEMPERATURE)
return "Color temperature";
if (color_mode == ColorMode::COLD_WARM_WHITE)
return "Cold/warm white";
if (color_mode == ColorMode::RGB)
return "RGB";
if (color_mode == ColorMode::RGB_WHITE)
return "RGBW";
if (color_mode == ColorMode::RGB_COLD_WARM_WHITE)
return "RGB + cold/warm white";
if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE)
return "RGB + color temperature";
return "";
}
void LightCall::perform() {

View File

@@ -19,6 +19,13 @@ class LightOutput {
virtual void setup_state(LightState *state) {}
/// Called on every update of the current values of the associated LightState,
/// can optionally be used to do processing of this change.
virtual void update_state(LightState *state) {}
/// Called from loop() every time the light state has changed, and should
/// should write the new state to hardware. Every call to write_state() is
/// preceded by (at least) one call to update_state().
virtual void write_state(LightState *state) = 0;
};

View File

@@ -114,9 +114,11 @@ void LightState::loop() {
// Apply transformer (if any)
if (this->transformer_ != nullptr) {
auto values = this->transformer_->apply();
this->next_write_ = values.has_value(); // don't write if transformer doesn't want us to
if (values.has_value())
if (values.has_value()) {
this->current_values = *values;
this->output_->update_state(this);
this->next_write_ = true;
}
if (this->transformer_->is_finished()) {
this->transformer_->stop();
@@ -127,18 +129,15 @@ void LightState::loop() {
// Write state to the light
if (this->next_write_) {
this->output_->write_state(this);
this->next_write_ = false;
this->output_->write_state(this);
}
}
float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
uint32_t LightState::hash_base() { return 1114400283; }
void LightState::publish_state() {
this->remote_values_callback_.call();
this->next_write_ = true;
}
void LightState::publish_state() { this->remote_values_callback_.call(); }
LightOutput *LightState::get_output() const { return this->output_; }
std::string LightState::get_effect_name() {
@@ -158,6 +157,11 @@ void LightState::add_new_target_state_reached_callback(std::function<void()> &&s
void LightState::set_default_transition_length(uint32_t default_transition_length) {
this->default_transition_length_ = default_transition_length;
}
uint32_t LightState::get_default_transition_length() const { return this->default_transition_length_; }
void LightState::set_flash_transition_length(uint32_t flash_transition_length) {
this->flash_transition_length_ = flash_transition_length;
}
uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; }
void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; }
void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
bool LightState::supports_effects() { return !this->effects_.empty(); }
@@ -235,7 +239,7 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length) {
// If starting a flash if one is already happening, set end values to end values of current flash
// Hacky but works
if (this->transformer_ != nullptr)
end_colors = this->transformer_->get_target_values();
end_colors = this->transformer_->get_start_values();
this->transformer_ = make_unique<LightFlashTransformer>(*this);
this->transformer_->setup(end_colors, target, length);
@@ -248,6 +252,7 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot
if (set_remote_values) {
this->remote_values = target;
}
this->output_->update_state(this);
this->next_write_ = true;
}

View File

@@ -99,6 +99,11 @@ class LightState : public Nameable, public Component {
/// Set the default transition length, i.e. the transition length when no transition is provided.
void set_default_transition_length(uint32_t default_transition_length);
uint32_t get_default_transition_length() const;
/// Set the flash transition length
void set_flash_transition_length(uint32_t flash_transition_length);
uint32_t get_flash_transition_length() const;
/// Set the gamma correction factor
void set_gamma_correct(float gamma_correct);
@@ -188,6 +193,8 @@ class LightState : public Nameable, public Component {
/// Default transition length for all transitions in ms.
uint32_t default_transition_length_{};
/// Transition length to use for flash transitions.
uint32_t flash_transition_length_{};
/// Gamma correction factor for the light.
float gamma_correct_{};
/// Restore mode of the light.

View File

@@ -58,7 +58,43 @@ class LightFlashTransformer : public LightTransformer {
public:
LightFlashTransformer(LightState &state) : state_(state) {}
optional<LightColorValues> apply() override { return this->get_target_values(); }
void start() override {
this->transition_length_ = this->state_.get_flash_transition_length();
if (this->transition_length_ * 2 > this->length_)
this->transition_length_ = this->length_ / 2;
// do not create transition if length is 0
if (this->transition_length_ == 0)
return;
// first transition to original target
this->transformer_ = this->state_.get_output()->create_default_transition();
this->transformer_->setup(this->state_.current_values, this->target_values_, this->transition_length_);
}
optional<LightColorValues> apply() override {
// transition transformer does not handle 0 length as progress returns nan
if (this->transition_length_ == 0)
return this->target_values_;
if (this->transformer_ != nullptr) {
if (!this->transformer_->is_finished()) {
return this->transformer_->apply();
} else {
this->transformer_->stop();
this->transformer_ = nullptr;
}
}
if (millis() > this->start_time_ + this->length_ - this->transition_length_) {
// second transition back to start value
this->transformer_ = this->state_.get_output()->create_default_transition();
this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_);
}
// once transition is complete, don't change states until next transition
return optional<LightColorValues>();
}
// Restore the original values after the flash.
void stop() override {
@@ -69,6 +105,8 @@ class LightFlashTransformer : public LightTransformer {
protected:
LightState &state_;
uint32_t transition_length_;
std::unique_ptr<LightTransformer> transformer_{nullptr};
};
} // namespace light

View File

@@ -43,21 +43,24 @@ void Logger::write_header_(int level, const char *tag, int line) {
}
void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT
if (level > this->level_for(tag))
if (level > this->level_for(tag) || recursion_guard_)
return;
recursion_guard_ = true;
this->reset_buffer_();
this->write_header_(level, tag, line);
this->vprintf_to_buffer_(format, args);
this->write_footer_();
this->log_message_(level, tag);
recursion_guard_ = false;
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format,
va_list args) { // NOLINT
if (level > this->level_for(tag))
if (level > this->level_for(tag) || recursion_guard_)
return;
recursion_guard_ = true;
this->reset_buffer_();
// copy format string
const char *format_pgm_p = (PGM_P) format;
@@ -78,6 +81,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr
this->vprintf_to_buffer_(this->tx_buffer_, args);
this->write_footer_();
this->log_message_(level, tag, offset);
recursion_guard_ = false;
}
#endif

View File

@@ -113,6 +113,8 @@ class Logger : public Component {
};
std::vector<LogLevelOverride> log_levels_;
CallbackManager<void(int, const char *, const char *)> log_callback_{};
/// Prevents recursive log calls, if true a log message is already being processed.
bool recursion_guard_ = false;
};
extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -91,7 +91,7 @@ async def mcp23xxx_pin_to_code(config):
# BEGIN Removed pin schemas below to show error in configuration
# TODO remove in 1.19.0
# TODO remove in 2022.5.0
for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]:
PIN_SCHEMA = cv.Schema(
@@ -110,6 +110,7 @@ for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]:
}
)
# pylint: disable=cell-var-from-loop
@pins.PIN_SCHEMA_REGISTRY.register(id, (PIN_SCHEMA, PIN_SCHEMA))
def pin_to_code(config):
pass

View File

@@ -8,6 +8,7 @@ from esphome.const import (
CONF_ID,
CONF_TEMPERATURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_CARBON_DIOXIDE,
ICON_MOLECULE_CO2,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_MILLION,
@@ -34,6 +35,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(

View File

@@ -0,0 +1,173 @@
#include "esphome/core/log.h"
#include "adapter.h"
namespace esphome {
namespace midea {
const char *const Constants::TAG = "midea";
const std::string Constants::FREEZE_PROTECTION = "freeze protection";
const std::string Constants::SILENT = "silent";
const std::string Constants::TURBO = "turbo";
ClimateMode Converters::to_climate_mode(MideaMode mode) {
switch (mode) {
case MideaMode::MODE_AUTO:
return ClimateMode::CLIMATE_MODE_HEAT_COOL;
case MideaMode::MODE_COOL:
return ClimateMode::CLIMATE_MODE_COOL;
case MideaMode::MODE_DRY:
return ClimateMode::CLIMATE_MODE_DRY;
case MideaMode::MODE_FAN_ONLY:
return ClimateMode::CLIMATE_MODE_FAN_ONLY;
case MideaMode::MODE_HEAT:
return ClimateMode::CLIMATE_MODE_HEAT;
default:
return ClimateMode::CLIMATE_MODE_OFF;
}
}
MideaMode Converters::to_midea_mode(ClimateMode mode) {
switch (mode) {
case ClimateMode::CLIMATE_MODE_HEAT_COOL:
return MideaMode::MODE_AUTO;
case ClimateMode::CLIMATE_MODE_COOL:
return MideaMode::MODE_COOL;
case ClimateMode::CLIMATE_MODE_DRY:
return MideaMode::MODE_DRY;
case ClimateMode::CLIMATE_MODE_FAN_ONLY:
return MideaMode::MODE_FAN_ONLY;
case ClimateMode::CLIMATE_MODE_HEAT:
return MideaMode::MODE_HEAT;
default:
return MideaMode::MODE_OFF;
}
}
ClimateSwingMode Converters::to_climate_swing_mode(MideaSwingMode mode) {
switch (mode) {
case MideaSwingMode::SWING_VERTICAL:
return ClimateSwingMode::CLIMATE_SWING_VERTICAL;
case MideaSwingMode::SWING_HORIZONTAL:
return ClimateSwingMode::CLIMATE_SWING_HORIZONTAL;
case MideaSwingMode::SWING_BOTH:
return ClimateSwingMode::CLIMATE_SWING_BOTH;
default:
return ClimateSwingMode::CLIMATE_SWING_OFF;
}
}
MideaSwingMode Converters::to_midea_swing_mode(ClimateSwingMode mode) {
switch (mode) {
case ClimateSwingMode::CLIMATE_SWING_VERTICAL:
return MideaSwingMode::SWING_VERTICAL;
case ClimateSwingMode::CLIMATE_SWING_HORIZONTAL:
return MideaSwingMode::SWING_HORIZONTAL;
case ClimateSwingMode::CLIMATE_SWING_BOTH:
return MideaSwingMode::SWING_BOTH;
default:
return MideaSwingMode::SWING_OFF;
}
}
MideaFanMode Converters::to_midea_fan_mode(ClimateFanMode mode) {
switch (mode) {
case ClimateFanMode::CLIMATE_FAN_LOW:
return MideaFanMode::FAN_LOW;
case ClimateFanMode::CLIMATE_FAN_MEDIUM:
return MideaFanMode::FAN_MEDIUM;
case ClimateFanMode::CLIMATE_FAN_HIGH:
return MideaFanMode::FAN_HIGH;
default:
return MideaFanMode::FAN_AUTO;
}
}
ClimateFanMode Converters::to_climate_fan_mode(MideaFanMode mode) {
switch (mode) {
case MideaFanMode::FAN_LOW:
return ClimateFanMode::CLIMATE_FAN_LOW;
case MideaFanMode::FAN_MEDIUM:
return ClimateFanMode::CLIMATE_FAN_MEDIUM;
case MideaFanMode::FAN_HIGH:
return ClimateFanMode::CLIMATE_FAN_HIGH;
default:
return ClimateFanMode::CLIMATE_FAN_AUTO;
}
}
bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) {
switch (mode) {
case MideaFanMode::FAN_SILENT:
case MideaFanMode::FAN_TURBO:
return true;
default:
return false;
}
}
const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) {
switch (mode) {
case MideaFanMode::FAN_SILENT:
return Constants::SILENT;
default:
return Constants::TURBO;
}
}
MideaFanMode Converters::to_midea_fan_mode(const std::string &mode) {
if (mode == Constants::SILENT)
return MideaFanMode::FAN_SILENT;
return MideaFanMode::FAN_TURBO;
}
MideaPreset Converters::to_midea_preset(ClimatePreset preset) {
switch (preset) {
case ClimatePreset::CLIMATE_PRESET_SLEEP:
return MideaPreset::PRESET_SLEEP;
case ClimatePreset::CLIMATE_PRESET_ECO:
return MideaPreset::PRESET_ECO;
case ClimatePreset::CLIMATE_PRESET_BOOST:
return MideaPreset::PRESET_TURBO;
default:
return MideaPreset::PRESET_NONE;
}
}
ClimatePreset Converters::to_climate_preset(MideaPreset preset) {
switch (preset) {
case MideaPreset::PRESET_SLEEP:
return ClimatePreset::CLIMATE_PRESET_SLEEP;
case MideaPreset::PRESET_ECO:
return ClimatePreset::CLIMATE_PRESET_ECO;
case MideaPreset::PRESET_TURBO:
return ClimatePreset::CLIMATE_PRESET_BOOST;
default:
return ClimatePreset::CLIMATE_PRESET_NONE;
}
}
bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; }
const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; }
MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; }
void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) {
if (capabilities.supportAutoMode())
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT_COOL);
if (capabilities.supportCoolMode())
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_COOL);
if (capabilities.supportHeatMode())
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT);
if (capabilities.supportDryMode())
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_DRY);
if (capabilities.supportTurboPreset())
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST);
if (capabilities.supportEcoPreset())
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO);
if (capabilities.supportFrostProtectionPreset())
traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION);
}
} // namespace midea
} // namespace esphome

View File

@@ -0,0 +1,42 @@
#pragma once
#include <Appliance/AirConditioner/AirConditioner.h>
#include "esphome/components/climate/climate_traits.h"
#include "appliance_base.h"
namespace esphome {
namespace midea {
using MideaMode = dudanov::midea::ac::Mode;
using MideaSwingMode = dudanov::midea::ac::SwingMode;
using MideaFanMode = dudanov::midea::ac::FanMode;
using MideaPreset = dudanov::midea::ac::Preset;
class Constants {
public:
static const char *const TAG;
static const std::string FREEZE_PROTECTION;
static const std::string SILENT;
static const std::string TURBO;
};
class Converters {
public:
static MideaMode to_midea_mode(ClimateMode mode);
static ClimateMode to_climate_mode(MideaMode mode);
static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode);
static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode);
static MideaPreset to_midea_preset(ClimatePreset preset);
static MideaPreset to_midea_preset(const std::string &preset);
static bool is_custom_midea_preset(MideaPreset preset);
static ClimatePreset to_climate_preset(MideaPreset preset);
static const std::string &to_custom_climate_preset(MideaPreset preset);
static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode);
static MideaFanMode to_midea_fan_mode(const std::string &fan_mode);
static bool is_custom_midea_fan_mode(MideaFanMode fan_mode);
static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode);
static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode);
static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities);
};
} // namespace midea
} // namespace esphome

View File

@@ -0,0 +1,152 @@
#include "esphome/core/log.h"
#include "air_conditioner.h"
#include "adapter.h"
#ifdef USE_REMOTE_TRANSMITTER
#include "midea_ir.h"
#endif
namespace esphome {
namespace midea {
static void set_sensor(Sensor *sensor, float value) {
if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value))
sensor->publish_state(value);
}
template<typename T> void update_property(T &property, const T &value, bool &flag) {
if (property != value) {
property = value;
flag = true;
}
}
void AirConditioner::on_status_change() {
bool need_publish = false;
update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish);
update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish);
auto mode = Converters::to_climate_mode(this->base_.getMode());
update_property(this->mode, mode, need_publish);
auto swing_mode = Converters::to_climate_swing_mode(this->base_.getSwingMode());
update_property(this->swing_mode, swing_mode, need_publish);
// Preset
auto preset = this->base_.getPreset();
if (Converters::is_custom_midea_preset(preset)) {
if (this->set_custom_preset_(Converters::to_custom_climate_preset(preset)))
need_publish = true;
} else if (this->set_preset_(Converters::to_climate_preset(preset))) {
need_publish = true;
}
// Fan mode
auto fan_mode = this->base_.getFanMode();
if (Converters::is_custom_midea_fan_mode(fan_mode)) {
if (this->set_custom_fan_mode_(Converters::to_custom_climate_fan_mode(fan_mode)))
need_publish = true;
} else if (this->set_fan_mode_(Converters::to_climate_fan_mode(fan_mode))) {
need_publish = true;
}
if (need_publish)
this->publish_state();
set_sensor(this->outdoor_sensor_, this->base_.getOutdoorTemp());
set_sensor(this->power_sensor_, this->base_.getPowerUsage());
set_sensor(this->humidity_sensor_, this->base_.getIndoorHum());
}
void AirConditioner::control(const ClimateCall &call) {
dudanov::midea::ac::Control ctrl{};
if (call.get_target_temperature().has_value())
ctrl.targetTemp = call.get_target_temperature().value();
if (call.get_swing_mode().has_value())
ctrl.swingMode = Converters::to_midea_swing_mode(call.get_swing_mode().value());
if (call.get_mode().has_value())
ctrl.mode = Converters::to_midea_mode(call.get_mode().value());
if (call.get_preset().has_value())
ctrl.preset = Converters::to_midea_preset(call.get_preset().value());
else if (call.get_custom_preset().has_value())
ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value());
if (call.get_fan_mode().has_value())
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value());
else if (call.get_custom_fan_mode().has_value())
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value());
this->base_.control(ctrl);
}
ClimateTraits AirConditioner::traits() {
auto traits = ClimateTraits();
traits.set_supports_current_temperature(true);
traits.set_visual_min_temperature(17);
traits.set_visual_max_temperature(30);
traits.set_visual_temperature_step(0.5);
traits.set_supported_modes(this->supported_modes_);
traits.set_supported_swing_modes(this->supported_swing_modes_);
traits.set_supported_presets(this->supported_presets_);
traits.set_supported_custom_presets(this->supported_custom_presets_);
traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_);
/* + MINIMAL SET OF CAPABILITIES */
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF);
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH);
traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF);
traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL);
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE);
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP);
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK)
Converters::to_climate_traits(traits, this->base_.getCapabilities());
return traits;
}
void AirConditioner::dump_config() {
ESP_LOGCONFIG(Constants::TAG, "MideaDongle:");
ESP_LOGCONFIG(Constants::TAG, " [x] Period: %dms", this->base_.getPeriod());
ESP_LOGCONFIG(Constants::TAG, " [x] Response timeout: %dms", this->base_.getTimeout());
ESP_LOGCONFIG(Constants::TAG, " [x] Request attempts: %d", this->base_.getNumAttempts());
#ifdef USE_REMOTE_TRANSMITTER
ESP_LOGCONFIG(Constants::TAG, " [x] Using RemoteTransmitter");
#endif
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) {
this->base_.getCapabilities().dump();
} else if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_ERROR) {
ESP_LOGW(Constants::TAG,
"Failed to get 0xB5 capabilities report. Suggest to disable it in config and manually set your "
"appliance options.");
}
this->dump_traits_(Constants::TAG);
}
/* ACTIONS */
void AirConditioner::do_follow_me(float temperature, bool beeper) {
#ifdef USE_REMOTE_TRANSMITTER
IrFollowMeData data(static_cast<uint8_t>(lroundf(temperature)), beeper);
this->transmit_ir(data);
#else
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
#endif
}
void AirConditioner::do_swing_step() {
#ifdef USE_REMOTE_TRANSMITTER
IrSpecialData data(0x01);
this->transmit_ir(data);
#else
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
#endif
}
void AirConditioner::do_display_toggle() {
if (this->base_.getCapabilities().supportLightControl()) {
this->base_.displayToggle();
} else {
#ifdef USE_REMOTE_TRANSMITTER
IrSpecialData data(0x08);
this->transmit_ir(data);
#else
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
#endif
}
}
} // namespace midea
} // namespace esphome

View File

@@ -0,0 +1,41 @@
#pragma once
#include <Appliance/AirConditioner/AirConditioner.h>
#include "appliance_base.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace midea {
using sensor::Sensor;
using climate::ClimateCall;
class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner> {
public:
void dump_config() override;
void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; }
void set_humidity_setpoint_sensor(Sensor *sensor) { this->humidity_sensor_ = sensor; }
void set_power_sensor(Sensor *sensor) { this->power_sensor_ = sensor; }
void on_status_change() override;
/* ############### */
/* ### ACTIONS ### */
/* ############### */
void do_follow_me(float temperature, bool beeper = false);
void do_display_toggle();
void do_swing_step();
void do_beeper_on() { this->set_beeper_feedback(true); }
void do_beeper_off() { this->set_beeper_feedback(false); }
void do_power_on() { this->base_.setPowerState(true); }
void do_power_off() { this->base_.setPowerState(false); }
protected:
void control(const ClimateCall &call) override;
ClimateTraits traits() override;
Sensor *outdoor_sensor_{nullptr};
Sensor *humidity_sensor_{nullptr};
Sensor *power_sensor_{nullptr};
};
} // namespace midea
} // namespace esphome

View File

@@ -0,0 +1,76 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/climate/climate.h"
#ifdef USE_REMOTE_TRANSMITTER
#include "esphome/components/remote_base/midea_protocol.h"
#include "esphome/components/remote_transmitter/remote_transmitter.h"
#endif
#include <Appliance/ApplianceBase.h>
#include <Helpers/Logger.h>
namespace esphome {
namespace midea {
using climate::ClimatePreset;
using climate::ClimateTraits;
using climate::ClimateMode;
using climate::ClimateSwingMode;
using climate::ClimateFanMode;
template<typename T> class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate {
static_assert(std::is_base_of<dudanov::midea::ApplianceBase, T>::value,
"T must derive from dudanov::midea::ApplianceBase class");
public:
ApplianceBase() {
this->base_.setStream(this);
this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this));
dudanov::midea::ApplianceBase::setLogger([](int level, const char *tag, int line, String format, va_list args) {
esp_log_vprintf_(level, tag, line, format.c_str(), args);
});
}
bool can_proceed() override {
return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS;
}
float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; }
void setup() override { this->base_.setup(); }
void loop() override { this->base_.loop(); }
void set_period(uint32_t ms) { this->base_.setPeriod(ms); }
void set_response_timeout(uint32_t ms) { this->base_.setTimeout(ms); }
void set_request_attempts(uint32_t attempts) { this->base_.setNumAttempts(attempts); }
void set_beeper_feedback(bool state) { this->base_.setBeeper(state); }
void set_autoconf(bool value) { this->base_.setAutoconf(value); }
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
void set_custom_presets(std::set<std::string> presets) { this->supported_custom_presets_ = std::move(presets); }
void set_custom_fan_modes(std::set<std::string> modes) { this->supported_custom_fan_modes_ = std::move(modes); }
virtual void on_status_change() = 0;
#ifdef USE_REMOTE_TRANSMITTER
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
this->transmitter_ = transmitter;
}
void transmit_ir(remote_base::MideaData &data) {
data.finalize();
auto transmit = this->transmitter_->transmit();
remote_base::MideaProtocol().encode(transmit.get_data(), data);
transmit.perform();
}
#endif
protected:
T base_;
std::set<ClimateMode> supported_modes_{};
std::set<ClimateSwingMode> supported_swing_modes_{};
std::set<ClimatePreset> supported_presets_{};
std::set<std::string> supported_custom_presets_{};
std::set<std::string> supported_custom_fan_modes_{};
#ifdef USE_REMOTE_TRANSMITTER
remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr};
#endif
};
} // namespace midea
} // namespace esphome

View File

@@ -0,0 +1,56 @@
#pragma once
#include "esphome/core/automation.h"
#include "air_conditioner.h"
namespace esphome {
namespace midea {
template<typename... Ts> class MideaActionBase : public Action<Ts...> {
public:
void set_parent(AirConditioner *parent) { this->parent_ = parent; }
protected:
AirConditioner *parent_;
};
template<typename... Ts> class FollowMeAction : public MideaActionBase<Ts...> {
TEMPLATABLE_VALUE(float, temperature)
TEMPLATABLE_VALUE(bool, beeper)
void play(Ts... x) override {
this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...));
}
};
template<typename... Ts> class SwingStepAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_swing_step(); }
};
template<typename... Ts> class DisplayToggleAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_display_toggle(); }
};
template<typename... Ts> class BeeperOnAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_beeper_on(); }
};
template<typename... Ts> class BeeperOffAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_beeper_off(); }
};
template<typename... Ts> class PowerOnAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_power_on(); }
};
template<typename... Ts> class PowerOffAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_power_off(); }
};
} // namespace midea
} // namespace esphome

View File

@@ -0,0 +1,284 @@
from esphome.core import coroutine
from esphome import automation
from esphome.components import climate, sensor, uart, remote_transmitter
from esphome.components.remote_base import CONF_TRANSMITTER_ID
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_AUTOCONF,
CONF_BEEPER,
CONF_CUSTOM_FAN_MODES,
CONF_CUSTOM_PRESETS,
CONF_ID,
CONF_NUM_ATTEMPTS,
CONF_PERIOD,
CONF_SUPPORTED_MODES,
CONF_SUPPORTED_PRESETS,
CONF_SUPPORTED_SWING_MODES,
CONF_TIMEOUT,
CONF_TEMPERATURE,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
ICON_POWER,
ICON_THERMOMETER,
ICON_WATER_PERCENT,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PERCENT,
UNIT_WATT,
)
from esphome.components.climate import (
ClimateMode,
ClimatePreset,
ClimateSwingMode,
)
CODEOWNERS = ["@dudanov"]
DEPENDENCIES = ["climate", "uart", "wifi"]
AUTO_LOAD = ["sensor"]
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
CONF_POWER_USAGE = "power_usage"
CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
midea_ns = cg.esphome_ns.namespace("midea")
AirConditioner = midea_ns.class_("AirConditioner", climate.Climate, cg.Component)
Capabilities = midea_ns.namespace("Constants")
def templatize(value):
if isinstance(value, cv.Schema):
value = value.schema
ret = {}
for key, val in value.items():
ret[key] = cv.templatable(val)
return cv.Schema(ret)
def register_action(name, type_, schema):
validator = templatize(schema).extend(MIDEA_ACTION_BASE_SCHEMA)
registerer = automation.register_action(f"midea_ac.{name}", type_, validator)
def decorator(func):
async def new_func(config, action_id, template_arg, args):
ac_ = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg)
cg.add(var.set_parent(ac_))
await coroutine(func)(var, config, args)
return var
return registerer(new_func)
return decorator
ALLOWED_CLIMATE_MODES = {
"HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL,
"COOL": ClimateMode.CLIMATE_MODE_COOL,
"HEAT": ClimateMode.CLIMATE_MODE_HEAT,
"DRY": ClimateMode.CLIMATE_MODE_DRY,
"FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
}
ALLOWED_CLIMATE_PRESETS = {
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
}
ALLOWED_CLIMATE_SWING_MODES = {
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
}
CUSTOM_FAN_MODES = {
"SILENT": Capabilities.SILENT,
"TURBO": Capabilities.TURBO,
}
CUSTOM_PRESETS = {
"FREEZE_PROTECTION": Capabilities.FREEZE_PROTECTION,
}
validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True)
validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True)
validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True)
validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True)
validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True)
CONFIG_SCHEMA = cv.All(
climate.CLIMATE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(AirConditioner),
cv.Optional(CONF_PERIOD, default="1s"): cv.time_period,
cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period,
cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5),
cv.Optional(CONF_TRANSMITTER_ID): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
),
cv.Optional(CONF_BEEPER, default=False): cv.boolean,
cv.Optional(CONF_AUTOCONF, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes),
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
validate_swing_modes
),
cv.Optional(CONF_SUPPORTED_PRESETS): cv.ensure_list(validate_presets),
cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(validate_custom_presets),
cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(
validate_custom_fan_modes
),
cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
icon=ICON_POWER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_WATER_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
# Actions
FollowMeAction = midea_ns.class_("FollowMeAction", automation.Action)
DisplayToggleAction = midea_ns.class_("DisplayToggleAction", automation.Action)
SwingStepAction = midea_ns.class_("SwingStepAction", automation.Action)
BeeperOnAction = midea_ns.class_("BeeperOnAction", automation.Action)
BeeperOffAction = midea_ns.class_("BeeperOffAction", automation.Action)
PowerOnAction = midea_ns.class_("PowerOnAction", automation.Action)
PowerOffAction = midea_ns.class_("PowerOffAction", automation.Action)
MIDEA_ACTION_BASE_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(AirConditioner),
}
)
# FollowMe action
MIDEA_FOLLOW_ME_MIN = 0
MIDEA_FOLLOW_ME_MAX = 37
MIDEA_FOLLOW_ME_SCHEMA = cv.Schema(
{
cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature),
cv.Optional(CONF_BEEPER, default=False): cv.templatable(cv.boolean),
}
)
@register_action("follow_me", FollowMeAction, MIDEA_FOLLOW_ME_SCHEMA)
async def follow_me_to_code(var, config, args):
template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_)
cg.add(var.set_beeper(template_))
template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_)
cg.add(var.set_temperature(template_))
# Toggle Display action
@register_action(
"display_toggle",
DisplayToggleAction,
cv.Schema({}),
)
async def display_toggle_to_code(var, config, args):
pass
# Swing Step action
@register_action(
"swing_step",
SwingStepAction,
cv.Schema({}),
)
async def swing_step_to_code(var, config, args):
pass
# Beeper On action
@register_action(
"beeper_on",
BeeperOnAction,
cv.Schema({}),
)
async def beeper_on_to_code(var, config, args):
pass
# Beeper Off action
@register_action(
"beeper_off",
BeeperOffAction,
cv.Schema({}),
)
async def beeper_off_to_code(var, config, args):
pass
# Power On action
@register_action(
"power_on",
PowerOnAction,
cv.Schema({}),
)
async def power_on_to_code(var, config, args):
pass
# Power Off action
@register_action(
"power_off",
PowerOffAction,
cv.Schema({}),
)
async def power_off_to_code(var, config, args):
pass
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)
await climate.register_climate(var, config)
cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds))
cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds))
cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS]))
if CONF_TRANSMITTER_ID in config:
cg.add_define("USE_REMOTE_TRANSMITTER")
transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter_))
cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
cg.add(var.set_autoconf(config[CONF_AUTOCONF]))
if CONF_SUPPORTED_MODES in config:
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
if CONF_SUPPORTED_SWING_MODES in config:
cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
if CONF_SUPPORTED_PRESETS in config:
cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS]))
if CONF_CUSTOM_PRESETS in config:
cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
if CONF_CUSTOM_FAN_MODES in config:
cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))
if CONF_OUTDOOR_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
cg.add(var.set_outdoor_temperature_sensor(sens))
if CONF_POWER_USAGE in config:
sens = await sensor.new_sensor(config[CONF_POWER_USAGE])
cg.add(var.set_power_sensor(sens))
if CONF_HUMIDITY_SETPOINT in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
cg.add(var.set_humidity_setpoint_sensor(sens))
cg.add_library("dudanov/MideaUART", "1.1.5")

View File

@@ -0,0 +1,42 @@
#pragma once
#ifdef USE_REMOTE_TRANSMITTER
#include "esphome/components/remote_base/midea_protocol.h"
namespace esphome {
namespace midea {
using IrData = remote_base::MideaData;
class IrFollowMeData : public IrData {
public:
// Default constructor (temp: 30C, beeper: off)
IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {}
// Copy from Base
IrFollowMeData(const IrData &data) : IrData(data) {}
// Direct from temperature and beeper values
IrFollowMeData(uint8_t temp, bool beeper = false) : IrFollowMeData() {
this->set_temp(temp);
this->set_beeper(beeper);
}
/* TEMPERATURE */
uint8_t temp() const { return this->data_[4] - 1; }
void set_temp(uint8_t val) { this->data_[4] = std::min(MAX_TEMP, val) + 1; }
/* BEEPER */
bool beeper() const { return this->data_[3] & 128; }
void set_beeper(bool val) { this->set_value_(3, 1, 7, val); }
protected:
static const uint8_t MAX_TEMP = 37;
};
class IrSpecialData : public IrData {
public:
IrSpecialData(uint8_t code) : IrData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {}
};
} // namespace midea
} // namespace esphome
#endif

View File

@@ -1,115 +1,3 @@
from esphome.components import climate, sensor
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_CUSTOM_FAN_MODES,
CONF_CUSTOM_PRESETS,
CONF_ID,
CONF_PRESET_BOOST,
CONF_PRESET_ECO,
CONF_PRESET_SLEEP,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PERCENT,
UNIT_WATT,
ICON_THERMOMETER,
ICON_POWER,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
ICON_WATER_PERCENT,
DEVICE_CLASS_HUMIDITY,
)
from esphome.components.midea_dongle import CONF_MIDEA_DONGLE_ID, MideaDongle
AUTO_LOAD = ["climate", "sensor", "midea_dongle"]
CODEOWNERS = ["@dudanov"]
CONF_BEEPER = "beeper"
CONF_SWING_HORIZONTAL = "swing_horizontal"
CONF_SWING_BOTH = "swing_both"
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
CONF_POWER_USAGE = "power_usage"
CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
midea_ac_ns = cg.esphome_ns.namespace("midea_ac")
MideaAC = midea_ac_ns.class_("MideaAC", climate.Climate, cg.Component)
CLIMATE_CUSTOM_FAN_MODES = {
"SILENT": "silent",
"TURBO": "turbo",
}
validate_climate_custom_fan_mode = cv.enum(CLIMATE_CUSTOM_FAN_MODES, upper=True)
CLIMATE_CUSTOM_PRESETS = {
"FREEZE_PROTECTION": "freeze protection",
}
validate_climate_custom_preset = cv.enum(CLIMATE_CUSTOM_PRESETS, upper=True)
CONFIG_SCHEMA = cv.All(
climate.CLIMATE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(MideaAC),
cv.GenerateID(CONF_MIDEA_DONGLE_ID): cv.use_id(MideaDongle),
cv.Optional(CONF_BEEPER, default=False): cv.boolean,
cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(
validate_climate_custom_fan_mode
),
cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(
validate_climate_custom_preset
),
cv.Optional(CONF_SWING_HORIZONTAL, default=False): cv.boolean,
cv.Optional(CONF_SWING_BOTH, default=False): cv.boolean,
cv.Optional(CONF_PRESET_ECO, default=False): cv.boolean,
cv.Optional(CONF_PRESET_SLEEP, default=False): cv.boolean,
cv.Optional(CONF_PRESET_BOOST, default=False): cv.boolean,
cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
icon=ICON_POWER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_WATER_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await climate.register_climate(var, config)
paren = await cg.get_variable(config[CONF_MIDEA_DONGLE_ID])
cg.add(var.set_midea_dongle_parent(paren))
cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
if CONF_CUSTOM_FAN_MODES in config:
cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))
if CONF_CUSTOM_PRESETS in config:
cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
cg.add(var.set_swing_horizontal(config[CONF_SWING_HORIZONTAL]))
cg.add(var.set_swing_both(config[CONF_SWING_BOTH]))
cg.add(var.set_preset_eco(config[CONF_PRESET_ECO]))
cg.add(var.set_preset_sleep(config[CONF_PRESET_SLEEP]))
cg.add(var.set_preset_boost(config[CONF_PRESET_BOOST]))
if CONF_OUTDOOR_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
cg.add(var.set_outdoor_temperature_sensor(sens))
if CONF_POWER_USAGE in config:
sens = await sensor.new_sensor(config[CONF_POWER_USAGE])
cg.add(var.set_power_sensor(sens))
if CONF_HUMIDITY_SETPOINT in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
cg.add(var.set_humidity_setpoint_sensor(sens))
CONFIG_SCHEMA = cv.invalid("This platform has been renamed to midea in 2021.9")

View File

@@ -1,208 +0,0 @@
#include "esphome/core/log.h"
#include "midea_climate.h"
namespace esphome {
namespace midea_ac {
static const char *const TAG = "midea_ac";
static void set_sensor(sensor::Sensor *sensor, float value) {
if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value))
sensor->publish_state(value);
}
template<typename T> void set_property(T &property, T value, bool &flag) {
if (property != value) {
property = value;
flag = true;
}
}
void MideaAC::on_frame(const midea_dongle::Frame &frame) {
const auto p = frame.as<PropertiesFrame>();
if (p.has_power_info()) {
set_sensor(this->power_sensor_, p.get_power_usage());
return;
} else if (!p.has_properties()) {
ESP_LOGW(TAG, "RX: frame has unknown type");
return;
}
if (p.get_type() == midea_dongle::MideaMessageType::DEVICE_CONTROL) {
ESP_LOGD(TAG, "RX: control frame");
this->ctrl_request_ = false;
} else {
ESP_LOGD(TAG, "RX: query frame");
}
if (this->ctrl_request_)
return;
this->cmd_frame_.set_properties(p); // copy properties from response
bool need_publish = false;
set_property(this->mode, p.get_mode(), need_publish);
set_property(this->target_temperature, p.get_target_temp(), need_publish);
set_property(this->current_temperature, p.get_indoor_temp(), need_publish);
if (p.is_custom_fan_mode()) {
this->fan_mode.reset();
optional<std::string> mode = p.get_custom_fan_mode();
set_property(this->custom_fan_mode, mode, need_publish);
} else {
this->custom_fan_mode.reset();
optional<climate::ClimateFanMode> mode = p.get_fan_mode();
set_property(this->fan_mode, mode, need_publish);
}
set_property(this->swing_mode, p.get_swing_mode(), need_publish);
if (p.is_custom_preset()) {
this->preset.reset();
optional<std::string> preset = p.get_custom_preset();
set_property(this->custom_preset, preset, need_publish);
} else {
this->custom_preset.reset();
set_property(this->preset, p.get_preset(), need_publish);
}
if (need_publish)
this->publish_state();
set_sensor(this->outdoor_sensor_, p.get_outdoor_temp());
set_sensor(this->humidity_sensor_, p.get_humidity_setpoint());
}
void MideaAC::on_update() {
if (this->ctrl_request_) {
ESP_LOGD(TAG, "TX: control");
this->parent_->write_frame(this->cmd_frame_);
} else {
ESP_LOGD(TAG, "TX: query");
if (this->power_sensor_ == nullptr || this->request_num_++ % 32)
this->parent_->write_frame(this->query_frame_);
else
this->parent_->write_frame(this->power_frame_);
}
}
bool MideaAC::allow_preset(climate::ClimatePreset preset) const {
switch (preset) {
case climate::CLIMATE_PRESET_ECO:
if (this->mode == climate::CLIMATE_MODE_COOL) {
return true;
} else {
ESP_LOGD(TAG, "ECO preset is only available in COOL mode");
}
break;
case climate::CLIMATE_PRESET_SLEEP:
if (this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_DRY) {
ESP_LOGD(TAG, "SLEEP preset is not available in FAN_ONLY or DRY mode");
} else {
return true;
}
break;
case climate::CLIMATE_PRESET_BOOST:
if (this->mode == climate::CLIMATE_MODE_HEAT || this->mode == climate::CLIMATE_MODE_COOL) {
return true;
} else {
ESP_LOGD(TAG, "BOOST preset is only available in HEAT or COOL mode");
}
break;
case climate::CLIMATE_PRESET_NONE:
return true;
default:
break;
}
return false;
}
bool MideaAC::allow_custom_preset(const std::string &custom_preset) const {
if (custom_preset == MIDEA_FREEZE_PROTECTION_PRESET) {
if (this->mode == climate::CLIMATE_MODE_HEAT) {
return true;
} else {
ESP_LOGD(TAG, "%s is only available in HEAT mode", MIDEA_FREEZE_PROTECTION_PRESET.c_str());
}
}
return false;
}
void MideaAC::control(const climate::ClimateCall &call) {
if (call.get_mode().has_value() && call.get_mode().value() != this->mode) {
this->cmd_frame_.set_mode(call.get_mode().value());
this->ctrl_request_ = true;
}
if (call.get_target_temperature().has_value() && call.get_target_temperature().value() != this->target_temperature) {
this->cmd_frame_.set_target_temp(call.get_target_temperature().value());
this->ctrl_request_ = true;
}
if (call.get_fan_mode().has_value() &&
(!this->fan_mode.has_value() || this->fan_mode.value() != call.get_fan_mode().value())) {
this->custom_fan_mode.reset();
this->cmd_frame_.set_fan_mode(call.get_fan_mode().value());
this->ctrl_request_ = true;
}
if (call.get_custom_fan_mode().has_value() &&
(!this->custom_fan_mode.has_value() || this->custom_fan_mode.value() != call.get_custom_fan_mode().value())) {
this->fan_mode.reset();
this->cmd_frame_.set_custom_fan_mode(call.get_custom_fan_mode().value());
this->ctrl_request_ = true;
}
if (call.get_swing_mode().has_value() && call.get_swing_mode().value() != this->swing_mode) {
this->cmd_frame_.set_swing_mode(call.get_swing_mode().value());
this->ctrl_request_ = true;
}
if (call.get_preset().has_value() && this->allow_preset(call.get_preset().value()) &&
(!this->preset.has_value() || this->preset.value() != call.get_preset().value())) {
this->custom_preset.reset();
this->cmd_frame_.set_preset(call.get_preset().value());
this->ctrl_request_ = true;
}
if (call.get_custom_preset().has_value() && this->allow_custom_preset(call.get_custom_preset().value()) &&
(!this->custom_preset.has_value() || this->custom_preset.value() != call.get_custom_preset().value())) {
this->preset.reset();
this->cmd_frame_.set_custom_preset(call.get_custom_preset().value());
this->ctrl_request_ = true;
}
if (this->ctrl_request_) {
this->cmd_frame_.set_beeper_feedback(this->beeper_feedback_);
this->cmd_frame_.finalize();
}
}
climate::ClimateTraits MideaAC::traits() {
auto traits = climate::ClimateTraits();
traits.set_visual_min_temperature(17);
traits.set_visual_max_temperature(30);
traits.set_visual_temperature_step(0.5);
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_HEAT_COOL,
climate::CLIMATE_MODE_COOL,
climate::CLIMATE_MODE_DRY,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_FAN_ONLY,
});
traits.set_supported_fan_modes({
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH,
});
traits.set_supported_custom_fan_modes(this->traits_custom_fan_modes_);
traits.set_supported_swing_modes({
climate::CLIMATE_SWING_OFF,
climate::CLIMATE_SWING_VERTICAL,
});
if (traits_swing_horizontal_)
traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL);
if (traits_swing_both_)
traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH);
traits.set_supported_presets({
climate::CLIMATE_PRESET_NONE,
});
if (traits_preset_eco_)
traits.add_supported_preset(climate::CLIMATE_PRESET_ECO);
if (traits_preset_sleep_)
traits.add_supported_preset(climate::CLIMATE_PRESET_SLEEP);
if (traits_preset_boost_)
traits.add_supported_preset(climate::CLIMATE_PRESET_BOOST);
traits.set_supported_custom_presets(this->traits_custom_presets_);
traits.set_supports_current_temperature(true);
return traits;
}
} // namespace midea_ac
} // namespace esphome

View File

@@ -1,68 +0,0 @@
#pragma once
#include <utility>
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/midea_dongle/midea_dongle.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/midea_dongle/midea_dongle.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "midea_frame.h"
namespace esphome {
namespace midea_ac {
class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, public Component {
public:
float get_setup_priority() const override { return setup_priority::LATE; }
void on_frame(const midea_dongle::Frame &frame) override;
void on_update() override;
void setup() override { this->parent_->set_appliance(this); }
void set_midea_dongle_parent(midea_dongle::MideaDongle *parent) { this->parent_ = parent; }
void set_outdoor_temperature_sensor(sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; }
void set_humidity_setpoint_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; }
void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }
void set_beeper_feedback(bool state) { this->beeper_feedback_ = state; }
void set_swing_horizontal(bool state) { this->traits_swing_horizontal_ = state; }
void set_swing_both(bool state) { this->traits_swing_both_ = state; }
void set_preset_eco(bool state) { this->traits_preset_eco_ = state; }
void set_preset_sleep(bool state) { this->traits_preset_sleep_ = state; }
void set_preset_boost(bool state) { this->traits_preset_boost_ = state; }
bool allow_preset(climate::ClimatePreset preset) const;
void set_custom_fan_modes(std::set<std::string> custom_fan_modes) {
this->traits_custom_fan_modes_ = std::move(custom_fan_modes);
}
void set_custom_presets(std::set<std::string> custom_presets) {
this->traits_custom_presets_ = std::move(custom_presets);
}
bool allow_custom_preset(const std::string &custom_preset) const;
protected:
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override;
/// Return the traits of this controller.
climate::ClimateTraits traits() override;
const QueryFrame query_frame_;
const PowerQueryFrame power_frame_;
CommandFrame cmd_frame_;
midea_dongle::MideaDongle *parent_{nullptr};
sensor::Sensor *outdoor_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
uint8_t request_num_{0};
bool ctrl_request_{false};
bool beeper_feedback_{false};
bool traits_swing_horizontal_{false};
bool traits_swing_both_{false};
bool traits_preset_eco_{false};
bool traits_preset_sleep_{false};
bool traits_preset_boost_{false};
std::set<std::string> traits_custom_fan_modes_{{}};
std::set<std::string> traits_custom_presets_{{}};
};
} // namespace midea_ac
} // namespace esphome

View File

@@ -1,238 +0,0 @@
#include "midea_frame.h"
namespace esphome {
namespace midea_ac {
static const char *const TAG = "midea_ac";
const std::string MIDEA_SILENT_FAN_MODE = "silent";
const std::string MIDEA_TURBO_FAN_MODE = "turbo";
const std::string MIDEA_FREEZE_PROTECTION_PRESET = "freeze protection";
const uint8_t QueryFrame::INIT[] = {0xAA, 0x21, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x81,
0x00, 0xFF, 0x03, 0xFF, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x37, 0x31};
const uint8_t PowerQueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x21,
0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x17, 0x6A};
const uint8_t CommandFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x40, 0x00,
0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
float PropertiesFrame::get_target_temp() const {
float temp = static_cast<float>((this->pbuf_[12] & 0x0F) + 16);
if (this->pbuf_[12] & 0x10)
temp += 0.5;
return temp;
}
void PropertiesFrame::set_target_temp(float temp) {
uint8_t tmp = static_cast<uint8_t>(temp * 16.0) + 4;
tmp = ((tmp & 8) << 1) | (tmp >> 4);
this->pbuf_[12] &= ~0x1F;
this->pbuf_[12] |= tmp;
}
static float i16tof(int16_t in) { return static_cast<float>(in - 50) / 2.0; }
float PropertiesFrame::get_indoor_temp() const { return i16tof(this->pbuf_[21]); }
float PropertiesFrame::get_outdoor_temp() const { return i16tof(this->pbuf_[22]); }
float PropertiesFrame::get_humidity_setpoint() const { return static_cast<float>(this->pbuf_[29] & 0x7F); }
climate::ClimateMode PropertiesFrame::get_mode() const {
if (!this->get_power_())
return climate::CLIMATE_MODE_OFF;
switch (this->pbuf_[12] >> 5) {
case MIDEA_MODE_AUTO:
return climate::CLIMATE_MODE_HEAT_COOL;
case MIDEA_MODE_COOL:
return climate::CLIMATE_MODE_COOL;
case MIDEA_MODE_DRY:
return climate::CLIMATE_MODE_DRY;
case MIDEA_MODE_HEAT:
return climate::CLIMATE_MODE_HEAT;
case MIDEA_MODE_FAN_ONLY:
return climate::CLIMATE_MODE_FAN_ONLY;
default:
return climate::CLIMATE_MODE_OFF;
}
}
void PropertiesFrame::set_mode(climate::ClimateMode mode) {
uint8_t m;
switch (mode) {
case climate::CLIMATE_MODE_HEAT_COOL:
m = MIDEA_MODE_AUTO;
break;
case climate::CLIMATE_MODE_COOL:
m = MIDEA_MODE_COOL;
break;
case climate::CLIMATE_MODE_DRY:
m = MIDEA_MODE_DRY;
break;
case climate::CLIMATE_MODE_HEAT:
m = MIDEA_MODE_HEAT;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
m = MIDEA_MODE_FAN_ONLY;
break;
default:
this->set_power_(false);
return;
}
this->set_power_(true);
this->pbuf_[12] &= ~0xE0;
this->pbuf_[12] |= m << 5;
}
optional<climate::ClimatePreset> PropertiesFrame::get_preset() const {
if (this->get_eco_mode())
return climate::CLIMATE_PRESET_ECO;
if (this->get_sleep_mode())
return climate::CLIMATE_PRESET_SLEEP;
if (this->get_turbo_mode())
return climate::CLIMATE_PRESET_BOOST;
return climate::CLIMATE_PRESET_NONE;
}
void PropertiesFrame::set_preset(climate::ClimatePreset preset) {
this->clear_presets();
switch (preset) {
case climate::CLIMATE_PRESET_ECO:
this->set_eco_mode(true);
break;
case climate::CLIMATE_PRESET_SLEEP:
this->set_sleep_mode(true);
break;
case climate::CLIMATE_PRESET_BOOST:
this->set_turbo_mode(true);
break;
default:
break;
}
}
void PropertiesFrame::clear_presets() {
this->set_eco_mode(false);
this->set_sleep_mode(false);
this->set_turbo_mode(false);
this->set_freeze_protection_mode(false);
}
bool PropertiesFrame::is_custom_preset() const { return this->get_freeze_protection_mode(); }
const std::string &PropertiesFrame::get_custom_preset() const { return midea_ac::MIDEA_FREEZE_PROTECTION_PRESET; };
void PropertiesFrame::set_custom_preset(const std::string &preset) {
this->clear_presets();
if (preset == MIDEA_FREEZE_PROTECTION_PRESET)
this->set_freeze_protection_mode(true);
}
bool PropertiesFrame::is_custom_fan_mode() const {
switch (this->pbuf_[13]) {
case MIDEA_FAN_SILENT:
case MIDEA_FAN_TURBO:
return true;
default:
return false;
}
}
climate::ClimateFanMode PropertiesFrame::get_fan_mode() const {
switch (this->pbuf_[13]) {
case MIDEA_FAN_LOW:
return climate::CLIMATE_FAN_LOW;
case MIDEA_FAN_MEDIUM:
return climate::CLIMATE_FAN_MEDIUM;
case MIDEA_FAN_HIGH:
return climate::CLIMATE_FAN_HIGH;
default:
return climate::CLIMATE_FAN_AUTO;
}
}
void PropertiesFrame::set_fan_mode(climate::ClimateFanMode mode) {
uint8_t m;
switch (mode) {
case climate::CLIMATE_FAN_LOW:
m = MIDEA_FAN_LOW;
break;
case climate::CLIMATE_FAN_MEDIUM:
m = MIDEA_FAN_MEDIUM;
break;
case climate::CLIMATE_FAN_HIGH:
m = MIDEA_FAN_HIGH;
break;
default:
m = MIDEA_FAN_AUTO;
break;
}
this->pbuf_[13] = m;
}
const std::string &PropertiesFrame::get_custom_fan_mode() const {
switch (this->pbuf_[13]) {
case MIDEA_FAN_SILENT:
return MIDEA_SILENT_FAN_MODE;
default:
return MIDEA_TURBO_FAN_MODE;
}
}
void PropertiesFrame::set_custom_fan_mode(const std::string &mode) {
uint8_t m;
if (mode == MIDEA_SILENT_FAN_MODE) {
m = MIDEA_FAN_SILENT;
} else {
m = MIDEA_FAN_TURBO;
}
this->pbuf_[13] = m;
}
climate::ClimateSwingMode PropertiesFrame::get_swing_mode() const {
switch (this->pbuf_[17] & 0x0F) {
case MIDEA_SWING_VERTICAL:
return climate::CLIMATE_SWING_VERTICAL;
case MIDEA_SWING_HORIZONTAL:
return climate::CLIMATE_SWING_HORIZONTAL;
case MIDEA_SWING_BOTH:
return climate::CLIMATE_SWING_BOTH;
default:
return climate::CLIMATE_SWING_OFF;
}
}
void PropertiesFrame::set_swing_mode(climate::ClimateSwingMode mode) {
uint8_t m;
switch (mode) {
case climate::CLIMATE_SWING_VERTICAL:
m = MIDEA_SWING_VERTICAL;
break;
case climate::CLIMATE_SWING_HORIZONTAL:
m = MIDEA_SWING_HORIZONTAL;
break;
case climate::CLIMATE_SWING_BOTH:
m = MIDEA_SWING_BOTH;
break;
default:
m = MIDEA_SWING_OFF;
break;
}
this->pbuf_[17] = 0x30 | m;
}
float PropertiesFrame::get_power_usage() const {
uint32_t power = 0;
const uint8_t *ptr = this->pbuf_ + 28;
for (uint32_t weight = 1;; weight *= 10, ptr--) {
power += (*ptr % 16) * weight;
weight *= 10;
power += (*ptr / 16) * weight;
if (weight == 100000)
return static_cast<float>(power) * 0.1;
}
}
} // namespace midea_ac
} // namespace esphome

View File

@@ -1,165 +0,0 @@
#pragma once
#include "esphome/components/climate/climate.h"
#include "esphome/components/midea_dongle/midea_frame.h"
namespace esphome {
namespace midea_ac {
extern const std::string MIDEA_SILENT_FAN_MODE;
extern const std::string MIDEA_TURBO_FAN_MODE;
extern const std::string MIDEA_FREEZE_PROTECTION_PRESET;
/// Enum for all modes a Midea device can be in.
enum MideaMode : uint8_t {
/// The Midea device is set to automatically change the heating/cooling cycle
MIDEA_MODE_AUTO = 1,
/// The Midea device is manually set to cool mode (not in auto mode!)
MIDEA_MODE_COOL = 2,
/// The Midea device is manually set to dry mode
MIDEA_MODE_DRY = 3,
/// The Midea device is manually set to heat mode (not in auto mode!)
MIDEA_MODE_HEAT = 4,
/// The Midea device is manually set to fan only mode
MIDEA_MODE_FAN_ONLY = 5,
};
/// Enum for all modes a Midea fan can be in
enum MideaFanMode : uint8_t {
/// The fan mode is set to Auto
MIDEA_FAN_AUTO = 102,
/// The fan mode is set to Silent
MIDEA_FAN_SILENT = 20,
/// The fan mode is set to Low
MIDEA_FAN_LOW = 40,
/// The fan mode is set to Medium
MIDEA_FAN_MEDIUM = 60,
/// The fan mode is set to High
MIDEA_FAN_HIGH = 80,
/// The fan mode is set to Turbo
MIDEA_FAN_TURBO = 100,
};
/// Enum for all modes a Midea swing can be in
enum MideaSwingMode : uint8_t {
/// The sing mode is set to Off
MIDEA_SWING_OFF = 0b0000,
/// The fan mode is set to Both
MIDEA_SWING_BOTH = 0b1111,
/// The fan mode is set to Vertical
MIDEA_SWING_VERTICAL = 0b1100,
/// The fan mode is set to Horizontal
MIDEA_SWING_HORIZONTAL = 0b0011,
};
class PropertiesFrame : public midea_dongle::BaseFrame {
public:
PropertiesFrame() = delete;
PropertiesFrame(uint8_t *data) : BaseFrame(data) {}
PropertiesFrame(const Frame &frame) : BaseFrame(frame) {}
bool has_properties() const {
return this->has_response_type(0xC0) && (this->has_type(0x03) || this->has_type(0x02));
}
bool has_power_info() const { return this->has_response_type(0xC1); }
/* TARGET TEMPERATURE */
float get_target_temp() const;
void set_target_temp(float temp);
/* MODE */
climate::ClimateMode get_mode() const;
void set_mode(climate::ClimateMode mode);
/* FAN SPEED */
bool is_custom_fan_mode() const;
climate::ClimateFanMode get_fan_mode() const;
void set_fan_mode(climate::ClimateFanMode mode);
const std::string &get_custom_fan_mode() const;
void set_custom_fan_mode(const std::string &mode);
/* SWING MODE */
climate::ClimateSwingMode get_swing_mode() const;
void set_swing_mode(climate::ClimateSwingMode mode);
/* INDOOR TEMPERATURE */
float get_indoor_temp() const;
/* OUTDOOR TEMPERATURE */
float get_outdoor_temp() const;
/* HUMIDITY SETPOINT */
float get_humidity_setpoint() const;
/* ECO MODE */
bool get_eco_mode() const { return this->pbuf_[19] & 0x10; }
void set_eco_mode(bool state) { this->set_bytemask_(19, 0x80, state); }
/* SLEEP MODE */
bool get_sleep_mode() const { return this->pbuf_[20] & 0x01; }
void set_sleep_mode(bool state) { this->set_bytemask_(20, 0x01, state); }
/* TURBO MODE */
bool get_turbo_mode() const { return this->pbuf_[18] & 0x20 || this->pbuf_[20] & 0x02; }
void set_turbo_mode(bool state) {
this->set_bytemask_(18, 0x20, state);
this->set_bytemask_(20, 0x02, state);
}
/* FREEZE PROTECTION */
bool get_freeze_protection_mode() const { return this->pbuf_[31] & 0x80; }
void set_freeze_protection_mode(bool state) { this->set_bytemask_(31, 0x80, state); }
/* PRESET */
optional<climate::ClimatePreset> get_preset() const;
void set_preset(climate::ClimatePreset preset);
void clear_presets();
bool is_custom_preset() const;
const std::string &get_custom_preset() const;
void set_custom_preset(const std::string &preset);
/* POWER USAGE */
float get_power_usage() const;
/// Set properties from another frame
void set_properties(const PropertiesFrame &p) { memcpy(this->pbuf_ + 11, p.data() + 11, 10); }
protected:
/* POWER */
bool get_power_() const { return this->pbuf_[11] & 0x01; }
void set_power_(bool state) { this->set_bytemask_(11, 0x01, state); }
};
// Query state frame (read-only)
class QueryFrame : public midea_dongle::StaticFrame<midea_dongle::Frame> {
public:
QueryFrame() : StaticFrame(FPSTR(this->INIT)) {}
private:
static const uint8_t PROGMEM INIT[];
};
// Power query state frame (read-only)
class PowerQueryFrame : public midea_dongle::StaticFrame<midea_dongle::Frame> {
public:
PowerQueryFrame() : StaticFrame(FPSTR(this->INIT)) {}
private:
static const uint8_t PROGMEM INIT[];
};
// Command frame
class CommandFrame : public midea_dongle::StaticFrame<PropertiesFrame> {
public:
CommandFrame() : StaticFrame(FPSTR(this->INIT)) {}
void set_beeper_feedback(bool state) { this->set_bytemask_(11, 0x40, state); }
private:
static const uint8_t PROGMEM INIT[];
};
} // namespace midea_ac
} // namespace esphome

View File

@@ -1,30 +0,0 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
from esphome.const import CONF_ID
DEPENDENCIES = ["wifi", "uart"]
CODEOWNERS = ["@dudanov"]
midea_dongle_ns = cg.esphome_ns.namespace("midea_dongle")
MideaDongle = midea_dongle_ns.class_("MideaDongle", cg.Component, uart.UARTDevice)
CONF_MIDEA_DONGLE_ID = "midea_dongle_id"
CONF_STRENGTH_ICON = "strength_icon"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(MideaDongle),
cv.Optional(CONF_STRENGTH_ICON, default=False): cv.boolean,
}
)
.extend(cv.COMPONENT_SCHEMA)
.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)
cg.add(var.use_strength_icon(config[CONF_STRENGTH_ICON]))

Some files were not shown because too many files have changed in this diff Show More