Compare commits

..

192 Commits

Author SHA1 Message Date
Jesse Hills
d70ee03010 Merge branch 'dev' into socket-client-mode 2023-06-28 09:46:37 +12:00
dependabot[bot]
108fabe18f Bump pytest from 7.3.2 to 7.4.0 (#5000)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-28 09:41:39 +12:00
dependabot[bot]
8ce98dd15a Bump pyupgrade from 3.4.0 to 3.7.0 (#4971)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-06-28 09:41:02 +12:00
dependabot[bot]
9d21cccac1 Bump aioesphomeapi from 14.1.0 to 15.0.0 (#5012)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-28 09:40:08 +12:00
Jesse Hills
8f4abf6a63 Update sync workflow (#5017) 2023-06-28 09:34:31 +12:00
jerome992
bd9a4ff8de add water delivered to dsmr component (#4237)
Co-authored-by: Jerome <jerome992@internet.lu>
2023-06-27 15:35:20 -03:00
Philippe Vlérick
d9398a91d1 update dsmr to 0.7 (#5011) 2023-06-26 17:09:52 -03:00
Jesse Hills
ef84937fd6 Update webserver to 56d73b5 (#5007) 2023-06-26 10:27:03 +12:00
Guillermo Ruffino
2a2d20a7fc support empty schemas and one platform components (#4999) 2023-06-26 09:38:36 +12:00
Kamil Trzciński
8a1c49a4ae display: move Image, Font and Animation code into components (#4967)
* display: move `Font` to `components/font`

* display: move `Animation` to `components/animation`

* display: move `Image` to `components/image`
2023-06-24 17:56:29 -05:00
Jesse Hills
eb145757e5 Fix rp2040 pio tool download (#4994) 2023-06-23 16:42:37 +12:00
Samuel Sieb
fc0e1a3cb9 remove unused static declarations (#4993) 2023-06-23 13:03:31 +12:00
Jesse Hills
ec3d5fc427 Merge branch 'release' into dev 2023-06-23 07:49:08 +12:00
Kamil Trzciński
85608a8ab7 display: fix white screen on binary displays (#4991) 2023-06-22 14:18:29 -05:00
Jesse Hills
5ef9cd5f86 Merge pull request #4990 from esphome/bump-2023.6.0
2023.6.0
2023-06-22 17:15:13 +12:00
Jimmy Hedman
52d7d2cae7 Make ethernet_info work with esp-idf framework (#4976) 2023-06-22 16:09:00 +12:00
Jesse Hills
ceca91d1e7 Bump version to 2023.6.0 2023-06-22 13:39:10 +12:00
Jesse Hills
bc7c11be96 Merge pull request #4989 from esphome/bump-2023.6.0b7
2023.6.0b7
2023-06-22 13:31:03 +12:00
Jesse Hills
a2734330e1 Bump version to 2023.6.0b7 2023-06-22 11:50:46 +12:00
F.D.Castel
ef8180c8a8 dashboard: Adds "compressed=1" to /download.bin endpoint. (...) (#4966)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-06-22 11:50:45 +12:00
J. Nick Koston
cd773a1dec Migrate VOC sensors that use ppb to use volatile_organic_compounds_parts device class (#4982) 2023-06-22 11:50:45 +12:00
Kevin P. Fleming
244a212592 airthings_wave: refactor to eliminate code duplication (#4910) 2023-06-22 11:50:45 +12:00
F.D.Castel
f72b07eb0e dashboard: Adds "compressed=1" to /download.bin endpoint. (...) (#4966)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-06-22 11:48:17 +12:00
J. Nick Koston
314c1c8b5c Migrate VOC sensors that use ppb to use volatile_organic_compounds_parts device class (#4982) 2023-06-22 11:45:41 +12:00
Jesse Hills
98e3426769 Merge pull request #4987 from esphome/bump-2023.6.0b6
2023.6.0b6
2023-06-22 11:22:48 +12:00
Jesse Hills
7ef8d67831 Bump version to 2023.6.0b6 2023-06-22 10:31:23 +12:00
Jesse Hills
c455a5dd6a Update webserver and captive portal pages to 67c48ee9 (#4986) 2023-06-22 10:31:23 +12:00
Dion Hulse
74daca668e Add configuration option to disable the log UI. (#4419)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-06-22 10:31:23 +12:00
Jesse Hills
211453df43 Update webserver and captive portal pages to 67c48ee9 (#4986) 2023-06-22 10:12:25 +12:00
Dion Hulse
1cc7428445 Add configuration option to disable the log UI. (#4419)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-06-22 09:58:49 +12:00
Jesse Hills
a40fa22055 Merge pull request #4984 from esphome/bump-2023.6.0b5
2023.6.0b5
2023-06-21 17:17:13 +12:00
Jesse Hills
2047bba4f7 Bump version to 2023.6.0b5 2023-06-21 16:39:52 +12:00
Jesse Hills
a064ab5c2b Fix pypi release (#4983) 2023-06-21 16:39:52 +12:00
Jesse Hills
e8ce7048d8 Fix pypi release (#4983) 2023-06-21 16:36:54 +12:00
Jesse Hills
0c37d06936 Merge pull request #4981 from esphome/bump-2023.6.0b4
2023.6.0b4
2023-06-21 15:49:32 +12:00
Jesse Hills
d919373853 Bump version to 2023.6.0b4 2023-06-21 14:32:53 +12:00
Jesse Hills
6c00be0a63 Bump esphome-dashboard to 20230621.0 (#4980) 2023-06-21 14:32:47 +12:00
Onne
0671287b80 Make growatt play nicer with other modbus components. (#4947)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-06-21 14:31:24 +12:00
Martin Murray
867b4719d1 Apply configured IIR filter setting in generated BMP280 code (#4975)
Co-authored-by: Martin Murray <murrayma@gmail.com>
2023-06-21 14:31:24 +12:00
Jesse Hills
de6c527ca4 Bump esphome-dashboard to 20230621.0 (#4980) 2023-06-21 14:30:19 +12:00
Onne
9e7e3708e3 Make growatt play nicer with other modbus components. (#4947)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-06-21 00:22:32 +00:00
Kevin P. Fleming
8bd9f50659 airthings_wave: refactor to eliminate code duplication (#4910) 2023-06-21 11:53:44 +12:00
Stijn Tintel
cb5a01da29 mqtt: add ESP-IDF >= 5.0 support (#4854)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-06-20 23:53:32 +00:00
Martin Murray
bfe85dd710 Apply configured IIR filter setting in generated BMP280 code (#4975)
Co-authored-by: Martin Murray <murrayma@gmail.com>
2023-06-21 11:53:21 +12:00
Jesse Hills
fc544dc389 Merge pull request #4974 from esphome/bump-2023.6.0b3
2023.6.0b3
2023-06-21 11:50:59 +12:00
dependabot[bot]
24067312f6 Bump zeroconf from 0.63.0 to 0.69.0 (#4970)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-20 11:18:06 +12:00
Jesse Hills
501fe83710 Bump version to 2023.6.0b3 2023-06-20 11:10:48 +12:00
Stanislav Habich
5513b0e121 Update pca9685_output.cpp (#4929) 2023-06-20 11:10:48 +12:00
J. Nick Koston
959e7745a6 Construct web_server assets at build time instead of run time (#4944)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-06-20 11:10:48 +12:00
J. Nick Koston
76e947651d Store app comment and compilation_time in flash (#4945) 2023-06-20 11:10:48 +12:00
Hawawa McTaru
5ba04eb620 Fix for Fujitsu AC not having Quiet Fan Mode (#4962) 2023-06-20 11:10:48 +12:00
guillempages
ee12c68b8f Add actions to animation (#4959) 2023-06-20 10:50:02 +12:00
dependabot[bot]
b2ccd32cd7 Bump aioesphomeapi from 14.0.0 to 14.1.0 (#4972)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-20 10:35:26 +12:00
Jimmy Hedman
7ceb16cc5a Preprocess away unused code when IPv6 is disabled (#4973) 2023-06-20 10:34:46 +12:00
Jesse Hills
5a8b7c17da Fix python venv restoring (#4965)
* Fix python venv restoring

* Add shell

* Fix indentation
2023-06-19 16:08:23 -05:00
MrEditor97
41a618737b XL9535 I/O Expander (#4899)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-06-19 15:26:06 +12:00
Carson Full
67771abc9d Add read/write for 16bit registers (#4844) 2023-06-19 14:10:05 +12:00
dependabot[bot]
c151df32bc Bump pytest from 7.3.1 to 7.3.2 (#4936)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 13:58:01 +12:00
Stanislav Habich
b346ad8080 Update pca9685_output.cpp (#4929) 2023-06-19 13:56:12 +12:00
J. Nick Koston
cd57271386 Construct web_server assets at build time instead of run time (#4944)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-06-19 13:51:19 +12:00
J. Nick Koston
b9f20b36cb Store app comment and compilation_time in flash (#4945) 2023-06-19 11:35:47 +12:00
Kamil Trzciński
62d2640c37 display: move Rect into rect.cpp/.h (#4957) 2023-06-18 23:32:39 +00:00
Kamil Trzciński
54eb52c19a display/font: optimise font rendering by about 25% (#4956) 2023-06-18 23:29:43 +00:00
Hawawa McTaru
77a7d3f24b Fix for Fujitsu AC not having Quiet Fan Mode (#4962) 2023-06-19 11:20:32 +12:00
Kamil Trzciński
8c9d63f48f display: add BaseFont and introduce Font::draw methods (#4963) 2023-06-19 11:04:19 +12:00
Pavlo Dudnytskyi
5a8e93ed0a Upgraded Haier climate component implementation (#4521)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Pavlo Dudnytskyi <pdudnytskyi@astrata.eu>
Co-authored-by: esphomebot <esphome@nabucasa.com>
2023-06-19 10:24:52 +12:00
Jesse Hills
2d32e89b87 Merge pull request #4964 from esphome/bump-2023.6.0b2
2023.6.0b2
2023-06-19 09:22:14 +12:00
Jesse Hills
88c13768e3 Bump version to 2023.6.0b2 2023-06-19 07:35:03 +12:00
Jesse Hills
29f4430658 Use HW SPI for rp2040 (#4955) 2023-06-19 07:35:03 +12:00
Kamil Trzciński
b558a1c9dd display: allow to align image with ImageAlign (#4933) 2023-06-19 07:35:03 +12:00
guillempages
a4ef26749b Add support for ESP32-S3-BOX displays (#4942)
The ESP32-S3-BOX display has an ILI9xxx driver
Add the needed configuration so that it works.
2023-06-19 07:35:03 +12:00
guillempages
6aa3092be0 Split display_buffer sub-components into own files (#4950)
* Split display_buffer sub-components into own files

Move the Image, Animation and Font classes to their own h/cpp pairs,
instead of having everything into the display_buffer h/cpp files.

* Fixed COLOR_ON duplicate definition
2023-06-19 07:35:03 +12:00
guillempages
abca47f36f Add support for ESP32-S3-BOX-Lite displays (#4941) 2023-06-19 07:35:03 +12:00
Samuel Sieb
407b5e199e fix vbus sensor offsets (#4952) 2023-06-19 07:35:03 +12:00
Clyde Stubbs
2ffd430b0b Add support in vbus component for Deltasol BS 2009 (#4943) 2023-06-19 07:35:03 +12:00
Jesse Hills
d4099d68a7 Use HW SPI for rp2040 (#4955) 2023-06-19 07:24:44 +12:00
Kamil Trzciński
e1b0d86098 display: allow to align image with ImageAlign (#4933) 2023-06-19 07:24:23 +12:00
guillempages
1a7f121ac6 Add support for ESP32-S3-BOX displays (#4942)
The ESP32-S3-BOX display has an ILI9xxx driver
Add the needed configuration so that it works.
2023-06-17 03:38:44 -05:00
guillempages
ffa669899a Split display_buffer sub-components into own files (#4950)
* Split display_buffer sub-components into own files

Move the Image, Animation and Font classes to their own h/cpp pairs,
instead of having everything into the display_buffer h/cpp files.

* Fixed COLOR_ON duplicate definition
2023-06-17 03:32:07 -05:00
guillempages
17fed954bf Add support for ESP32-S3-BOX-Lite displays (#4941) 2023-06-16 11:39:50 +12:00
Samuel Sieb
467e42d8aa fix vbus sensor offsets (#4952) 2023-06-15 01:05:28 -07:00
Clyde Stubbs
a023f24a08 Add support in vbus component for Deltasol BS 2009 (#4943) 2023-06-14 23:51:44 -07:00
Keith Burzinski
81736447ee Merge pull request #4951 from esphome/bump-2023.6.0b1
2023.6.0b1
2023-06-14 22:35:29 -05:00
Jesse Hills
27f69f5439 Bump version to 2023.7.0-dev 2023-06-15 14:25:36 +12:00
Jesse Hills
5ebb468ccf Bump version to 2023.6.0b1 2023-06-15 14:25:36 +12:00
Jesse Hills
0ead802333 Merge branch 'dev' into bump-2023.6.0b1 2023-06-15 14:25:35 +12:00
Graham Brown
54474e5b33 Add Alarm Control Panel (#4770)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-06-15 12:34:39 +12:00
Kamil Trzciński
0411d52420 display: add BaseImage and provide only Image::get_pixel method (#4932) 2023-06-15 11:15:46 +12:00
Kamil Trzciński
cef659b0de display: Improve Image rendering by removing usage of virtual functions (#4931) 2023-06-15 09:50:24 +12:00
Jesse Hills
035c3ef8fe Add MULTI_CONF to pn53_i2c (#4938) 2023-06-13 15:30:40 -05:00
Carlos Cordero
5afdb1e97f I2S media player allow setting communication format for external DACs (#4918)
Co-authored-by: Carlos Cordero <ccordero@bkool.com>
2023-06-13 07:48:01 +12:00
Jesse Hills
f7c0ec6595 proto generation updates (#4653) 2023-06-12 17:00:34 +12:00
Pascal
0a407c5425 [max7219digit] fix 270° rotation (#4930)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-06-12 16:59:25 +12:00
jimtng
b9e7fdd2b0 Add !extend to devcontainer's customTags (#4749) 2023-06-12 13:31:00 +12:00
guillempages
c74105aad7 Add SVG image support (#4922) 2023-06-12 09:36:37 +12:00
RoboMagus
5f0892dec4 Allow multiple MAC addresses for 'on_ble_advertise' filter (#4773) 2023-06-09 12:53:30 +12:00
Jesse Hills
302dea4169 Move ESPTime into core esphome namespace (#4926)
* Prep-work for datetime entities

* Fix some includes and remove some restrictions on printing time on displays

* format

* format

* More formatting

* Move function contents

* Ignore clang-tidy
2023-06-08 17:24:44 -05:00
Jesse Hills
ce13979690 Bluetooth Proxy: Raw bundled advertisements (#4924) 2023-06-09 07:41:09 +12:00
dependabot[bot]
1cf210fa25 Bump aioesphomeapi from 13.9.0 to 14.0.0 (#4925)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-08 08:27:39 +12:00
Simone Rossetto
d1253922c3 Increase SNTP setup priority (#4917) 2023-06-07 10:38:18 +12:00
guillempages
6b00622329 Add support for mdi images (#4654) 2023-06-07 09:32:21 +12:00
PlainTechEnthusiast
aeb94e166b Support for Adafruit ESP32-S2 TFT Feather (#4912)
Support for optional PowerSupply component for ST7789V

This commit makes the power supply required if the model configured in the ST7789V component is set to ADAFRUIT_S2_TFT_FEATHER_240X135. There are at least two boards from Adafruit with this configuration but with a different pin out.

This also adds the board pins definition for the board I have. There is discussion on the forums about the other board's documentation not matching reality and I don't have a physical board to confirm.
2023-06-03 16:07:24 -05:00
Nick Owens
8bb4c65272 prometheus: fix compilation with EntityBase (#4895) 2023-06-01 14:31:58 +12:00
dependabot[bot]
09a4d869f6 Bump esptool from 4.5.1 to 4.6 (#4906)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 01:47:16 +00:00
dependabot[bot]
ae4c130a61 Bump zeroconf from 0.62.0 to 0.63.0 (#4890)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 13:18:29 +12:00
dependabot[bot]
5621d592b4 Bump pytest-cov from 4.0.0 to 4.1.0 (#4888)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 13:18:12 +12:00
dependabot[bot]
4858882816 Bump frenck/action-yamllint from 1.4.0 to 1.4.1 (#4876)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 13:17:57 +12:00
Jesse Hills
b06bdc2da3 Allow WIFI to be disabled and enabled (#4810)
Co-authored-by: Péter Sárközi <xmisterhu@gmail.com>
Co-authored-by: Ash McKenzie <ash@the-rebellion.net>
2023-06-01 11:34:35 +12:00
Jesse Hills
1ea5d90ea3 Continuous voice_assistant and silence detection (#4892) 2023-05-31 16:30:53 +12:00
Stijn Tintel
f9f335e692 light: fix compile with ESP-IDF >= 5 (#4855) 2023-05-31 13:49:31 +12:00
Stijn Tintel
3ead48f0db ota: fix TWDT with ESP-IDF >= 5 (#4858) 2023-05-31 13:48:34 +12:00
Stijn Tintel
ccba94197d ota: fix compile with ESP-IDF >= 5 (#4857) 2023-05-31 13:47:11 +12:00
dependabot[bot]
8adb7dc97c Bump aioesphomeapi from 13.7.5 to 13.9.0 (#4907)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-31 09:05:40 +12:00
Stijn Tintel
796b64541f esp32_rmt_led_strip: fix compile with ESP-IDF >= 5 (#4856)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-05-30 11:07:36 +12:00
Jesse Hills
b8c27bc17d Merge pull request #4905 from esphome/bump-2023.5.5
2023.5.5
2023-05-29 10:25:22 +12:00
Regev Brody
7dcdf80f49 add SUB_SWITCH macro (#4898) 2023-05-29 09:44:35 +12:00
Regev Brody
57023457ee add SUB_SELECT macro (#4897) 2023-05-29 09:44:05 +12:00
Jesse Hills
1057ac4db7 Bump version to 2023.5.5 2023-05-29 09:42:12 +12:00
Jesse Hills
69f5674d9e Fix version printing not breaking yaml parsing (#4904) 2023-05-29 09:42:12 +12:00
Jesse Hills
ebad407586 Fix version printing not breaking yaml parsing (#4904) 2023-05-28 21:18:01 +00:00
Samuel Sieb
71387846dc move pio tools to LED component (#4903) 2023-05-28 20:49:27 +00:00
Sybren A. Stüvel
97c1c34708 Add support for TMP1075 temperature sensor (#4776)
* Add support for TMP1075 temperature sensor

TMP1075 is a temperature sensor with I2C interface in industry standard
LM75 form factor and pinout.

https://www.ti.com/product/TMP1075

Example YAML:

```yaml
sensor:
  - platform: tmp1075
    name: TMP1075 Temperature
    id: radiator_temp
    update_interval: 10s
    i2c_id: i2c_bus_1
    conversion_rate: 27.5ms
    alert:
      limit_low: 50
      limit_high: 75
      fault_count: 1
      polarity: active_high
```

* Add myself as codeowner of the TMP1075 component

* Include '°C' unit when logging low/high limit setting

* Reformat

No functional changes.

* Fix logging: use %.4f for temperatures, not %d

* Fix config initialisation

* Use relative include for `tmp1075.h`

* Apply formatting changes suggested by script/clang-tidy for ESP32

* Add YAML to test1.yaml

* Fix test1.yaml by giving TMP1075 a name

* Less verbose logging (debug -> verbose level)

* Schema: reduce accuracy_decimals to 2

* I2C address as hexadecimal

* Proper name for enum in Python

The enum on the C++ side was renamed (clang-tidy) but I forgot to take that
into account in the Python code.

* Expose 'alert function' to the code generator/YAML params and remove 'shutdown'

Shutdown mode doesn't work the way I expect it, so remove it until someone
actually asks for it.

Also 'alert mode' was renamed to 'alert function' for clarity.

* Move simple setters to header file

* Remove `load_config_();` function
2023-05-26 00:01:21 -05:00
Jesse Hills
79abd773a2 Allow i2s microphone bits per sample to be configured (#4884) 2023-05-26 15:50:44 +12:00
guillempages
9cd173ef83 Allow partially looping animations (#4693)
Add the possibility of specifying a "loop" in an animation; where the
requested frames (start - end) will be repeateadly shown for "count" times.
2023-05-25 16:49:52 -05:00
Keith Burzinski
b723728177 Merge pull request #4889 from esphome/bump-2023.5.4
2023.5.4
2023-05-24 17:21:24 -05:00
Keith Burzinski
316171491f Bump version to 2023.5.4 2023-05-24 15:58:54 -05:00
Jesse Hills
91ff502872 Fix esp32_rmt_led_strip color modes (#4886) 2023-05-24 15:58:53 -05:00
Samuel Sieb
6e414180e0 fix modbus sending FP32_R values (#4882) 2023-05-24 15:58:53 -05:00
Jesse Hills
7d2ae4e252 Print ESPHome version when running commands (#4883) 2023-05-24 15:58:53 -05:00
Davrosx
fb4cb07c6f Update cover.h for compile errors with stop() (#4879) 2023-05-24 15:58:53 -05:00
Fabian
19f91a7deb [internal_temperature] ESP32-S3 needs ESP IDF V4.4.3 or higher (#4873)
Co-authored-by: Your Name <you@example.com>
2023-05-24 15:58:53 -05:00
Rajan Patel
bb044a789c Add i2s mclk (#4885) 2023-05-24 19:28:08 +12:00
Jesse Hills
2153cfc749 Fix esp32_rmt_led_strip color modes (#4886) 2023-05-24 02:20:06 -05:00
Jesse Hills
2e8b4fbdc8 Fix rp2040_pio_led_strip color modes (#4887) 2023-05-24 02:19:49 -05:00
Samuel Sieb
4141100b1c fix modbus sending FP32_R values (#4882) 2023-05-23 15:00:33 -07:00
Jesse Hills
baa08160bb Print ESPHome version when running commands (#4883) 2023-05-23 21:56:15 +00:00
Davrosx
35ef4aad60 Update cover.h for compile errors with stop() (#4879) 2023-05-24 07:52:34 +12:00
Fabian
ffa5e29dab [internal_temperature] ESP32-S3 needs ESP IDF V4.4.3 or higher (#4873)
Co-authored-by: Your Name <you@example.com>
2023-05-24 07:51:12 +12:00
Evgeny
28b5c535ec add codeowners (#4875) 2023-05-22 22:28:35 +00:00
Keith Burzinski
3807350c61 Merge pull request #4877 from esphome/bump-2023.5.3
2023.5.3
2023-05-22 17:25:56 -05:00
Keith Burzinski
9d2467cf62 Bump version to 2023.5.3 2023-05-22 16:53:10 -05:00
Fabian
d2480d3194 [PSRam] Change log unit to KB to minimize rounding error. (#4872)
Co-authored-by: Your Name <you@example.com>
2023-05-22 16:53:10 -05:00
Jesse Hills
148eb03d13 Allow microphone channel to be specified in config (#4871) 2023-05-22 16:53:10 -05:00
Fabian
ed8aec62fc [PSRam] Change log unit to KB to minimize rounding error. (#4872)
Co-authored-by: Your Name <you@example.com>
2023-05-23 08:43:03 +12:00
Jesse Hills
f7b5c6307c Allow microphone channel to be specified in config (#4871) 2023-05-23 07:02:16 +12:00
Lucas Reiners
40d110fc3f Update ili9xxx_init.h for correct white balance (#4849) 2023-05-22 23:24:17 +12:00
Daniel Mahaney
a15ac06771 Rp2040 pio ledstrip (#4818)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-05-22 10:31:27 +12:00
Stefan Rado
784cc3bc29 Fix i2s_audio media_player mutex acquisition (#4867)
Co-authored-by: Rajan Patel <rpatel3001@gmail.com>
2023-05-22 08:10:23 +12:00
Stefan Rado
8384bd7fc7 Run YAML test 8 during CI and fix board used (#4862) 2023-05-22 08:08:33 +12:00
guillempages
8a518f0def Add transparency support to all image types (#4600) 2023-05-22 08:03:21 +12:00
Keith Burzinski
c61a3bf431 Sprinkler fixes (#4816) 2023-05-18 11:36:52 +12:00
Jesse Hills
1b77996ccd Remove i2c dependency from ttp229_bsf (#4851) 2023-05-18 11:34:18 +12:00
Jesse Hills
9e3ecc8372 Migrate e131 to use socket instead of WiFiUDP arduino library (#4832) 2023-05-18 08:41:21 +12:00
dependabot[bot]
18f4e41550 Bump platformio from 6.1.6 to 6.1.7 (#4795)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-05-17 05:29:15 +00:00
dependabot[bot]
26cdbaa3df Bump pyupgrade from 3.3.2 to 3.4.0 (#4842)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-05-17 05:26:29 +00:00
dependabot[bot]
c76c1337ba Bump zeroconf from 0.60.0 to 0.62.0 (#4781)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 16:53:23 +12:00
dependabot[bot]
fe3b779016 Bump pylint from 2.17.3 to 2.17.4 (#4843)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 16:50:11 +12:00
dependabot[bot]
682638d4de Bump tornado from 6.3.1 to 6.3.2 (#4841)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 16:42:50 +12:00
Lukas Lindner
c96663daca Insert Europe Tank Types from mopeka_std_check (#4757) 2023-05-17 16:41:53 +12:00
Joel Goguen
b3ed988119 Allow substitutions to be valid names (#4726) 2023-05-17 16:33:08 +12:00
Carson Full
77695aa55b Move some I2C logic out of header file (#4839) 2023-05-17 16:32:20 +12:00
Markus
c5a45645a6 allow to use MQTT for discovery of IPs if mDNS is no option (#3887)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-05-17 16:29:56 +12:00
Christian
0de47e2a4e Add DNS to Text info (#4821)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-05-17 16:29:21 +12:00
Jesse Hills
90598d2405 Rework CI into multiple dependent jobs (#4823) 2023-05-17 16:28:09 +12:00
Samuel Sieb
edfd82fd42 handle Wiegand 8-bit keys (#4837)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2023-05-17 09:30:14 +12:00
Samuel Sieb
492bad645b support sending keys to the collector (#4838)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2023-05-16 23:36:02 +12:00
Jesse Hills
ab4517f611 Fix stale bot ignoring not-stale (#4836) 2023-05-15 23:45:35 -05:00
Jesse Hills
2bfcfa6dae Use token so PR checks are run (#4834) 2023-05-16 11:30:25 +12:00
github-actions[bot]
d7fd23d8a8 Synchronise Device Classes from Home Assistant (#4825)
Co-authored-by: esphomebot <esphome@nabucasa.com>
2023-05-15 23:13:17 +00:00
Justin Gerace
d0ca69bc27 Start UART assignment at UART0 if the logger is not enabled or is not configured for hardware logging on ESP32 (#4762) 2023-05-16 11:00:05 +12:00
dependabot[bot]
2a7d6addde Bump tzlocal from 4.2 to 5.0.1 (#4829)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-16 10:30:08 +12:00
Federico G. Schwindt
8e4aeec3bd Fix time period validation for the auto cleaning interval (#4811) 2023-05-16 10:28:01 +12:00
Jesse Hills
7f83a6e667 Bump esphome-dashboard to 20230516.0 (#4831) 2023-05-16 10:25:49 +12:00
Christian
ae838b13a8 Update PulseLightEffect with range brightness (#4820)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-05-16 08:29:00 +12:00
dependabot[bot]
ce5dc6f100 Bump aioesphomeapi from 13.7.2 to 13.7.5 (#4830)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-16 08:24:58 +12:00
RoboMagus
b1551d0436 Fix missing stop trait in send_cover_info (#4826) 2023-05-16 08:24:03 +12:00
Jesse Hills
b4c2433bac Run black over tests folder (#4824) 2023-05-15 16:09:56 +12:00
Jesse Hills
1d8227788b Dontr try stop if not actually started (#4814) 2023-05-15 10:11:48 +12:00
Jesse Hills
5fdd8440ac Fix i2s media player volume control (#4813) 2023-05-13 00:19:06 -05:00
richardhopton
5d8daa6990 Tuya: Prevent loop when setting colors on case-sensitive dps (#4809)
Co-authored-by: Samuel Sieb <samuel-github@sieb.net>
2023-05-13 15:16:28 +12:00
Federico G. Schwindt
cf802d0d38 Wording (#4805) 2023-05-12 13:46:47 +12:00
Alex Dekker
9cbf437509 Supposed to fix #4069, by changing the default value to 0s (timeunit instead of int) to pass validation (#4806)
Co-authored-by: Alex1602 <alex1602@gmail.com>
2023-05-11 16:25:34 -05:00
NP v/d Spek
cd7e8e4bdd Add minimum RSSI check to ble presence (#4646)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-05-12 04:28:24 +12:00
Jesse Hills
ed024a0aa5 Remove AUTO_LOAD from apds9960 (#4746) 2023-05-11 11:33:59 +12:00
Jesse Hills
75d0514d05 Merge branch 'release' into dev 2023-05-11 10:50:16 +12:00
Jimmy Hedman
5b02715c7b Fixed access point for ESP32 IDF platform (#4784) 2023-05-11 10:00:28 +12:00
Jesse Hills
3b0eea69ce Bump version to 2023.6.0-dev 2023-05-11 09:38:21 +12:00
Otto winter
a81fc6e85d Add gai_strerror 2022-02-03 14:13:28 +01:00
Otto winter
c19d893e4e Lint 2022-02-03 09:23:48 +01:00
Otto winter
f4183778e3 simplify 2022-02-03 09:22:59 +01:00
Otto winter
11e8bd77e2 Lint 2022-02-03 09:16:46 +01:00
Otto winter
a39b2c4ac7 Add support for socket client mode and getaddrinfo 2022-02-02 22:38:27 +01:00
262 changed files with 11809 additions and 3159 deletions

View File

@@ -40,6 +40,7 @@
"yaml.customTags": [
"!secret scalar",
"!lambda scalar",
"!extend scalar",
"!include_dir_named scalar",
"!include_dir_list scalar",
"!include_dir_merge_list scalar",

1
.gitattributes vendored
View File

@@ -1,2 +1,3 @@
# Normalize line endings to LF in the repository
* text eol=lf
*.png binary

View File

@@ -0,0 +1,38 @@
name: Restore Python
inputs:
python-version:
description: Python version to restore
required: true
type: string
cache-key:
description: Cache key to use
required: true
type: string
outputs:
python-version:
description: Python version restored
value: ${{ steps.python.outputs.python-version }}
runs:
using: "composite"
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@v4.6.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@v3.3.1
with:
path: venv
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
shell: bash
run: |
python -m venv venv
. venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
pip install -e .

View File

@@ -12,60 +12,264 @@ on:
permissions:
contents: read
env:
DEFAULT_PYTHON: "3.9"
PYUPGRADE_TARGET: "--py39-plus"
CLANG_FORMAT_VERSION: "13.0.1"
concurrency:
# yamllint disable-line rule:line-length
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
ci:
name: ${{ matrix.name }}
common:
name: Create common environment
runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v3.3.1
with:
path: venv
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ steps.cache-key.outputs.key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
python -m venv venv
. venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
pip install -e .
yamllint:
name: yamllint
runs-on: ubuntu-latest
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
- name: Run yamllint
uses: frenck/action-yamllint@v1.4.1
black:
name: Check black
runs-on: ubuntu-latest
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run black
run: |
. venv/bin/activate
black --verbose esphome tests
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
flake8:
name: Check flake8
runs-on: ubuntu-latest
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run flake8
run: |
. venv/bin/activate
flake8 esphome
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
pylint:
name: Check pylint
runs-on: ubuntu-latest
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run pylint
run: |
. venv/bin/activate
pylint -f parseable --persistent=n esphome
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
pyupgrade:
name: Check pyupgrade
runs-on: ubuntu-latest
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run pyupgrade
run: |
. venv/bin/activate
pyupgrade ${{ env.PYUPGRADE_TARGET }} `find esphome -name "*.py" -type f`
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
ci-custom:
name: Run script/ci-custom
runs-on: ubuntu-latest
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json"
- name: Run script/ci-custom
run: |
. venv/bin/activate
script/ci-custom.py
script/build_codeowners.py --check
pytest:
name: Run pytest
runs-on: ubuntu-latest
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run pytest
run: |
. venv/bin/activate
pytest -vv --tb=native tests
clang-format:
name: Check clang-format
runs-on: ubuntu-latest
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Install clang-format
run: |
. venv/bin/activate
pip install clang-format==${{ env.CLANG_FORMAT_VERSION }}
- name: Run clang-format
run: |
. venv/bin/activate
script/clang-format -i
git diff-index --quiet HEAD --
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
compile-tests:
name: Run YAML test ${{ matrix.file }}
runs-on: ubuntu-latest
needs:
- common
- black
- ci-custom
- clang-format
- flake8
- pylint
- pytest
- pyupgrade
- yamllint
strategy:
fail-fast: false
max-parallel: 5
max-parallel: 2
matrix:
file: [1, 2, 3, 3.1, 4, 5, 6, 7, 8]
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache@v3.3.1
with:
path: ~/.platformio
# yamllint disable-line rule:line-length
key: platformio-test${{ matrix.file }}-${{ hashFiles('platformio.ini') }}
- name: Run esphome compile tests/test${{ matrix.file }}.yaml
run: |
. venv/bin/activate
esphome compile tests/test${{ matrix.file }}.yaml
clang-tidy:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
needs:
- common
- black
- ci-custom
- clang-format
- flake8
- pylint
- pytest
- pyupgrade
- yamllint
strategy:
fail-fast: false
max-parallel: 2
matrix:
include:
- id: ci-custom
name: Run script/ci-custom
- id: lint-python
name: Run script/lint-python
- id: test
file: tests/test1.yaml
name: Test tests/test1.yaml
pio_cache_key: test1
- id: test
file: tests/test2.yaml
name: Test tests/test2.yaml
pio_cache_key: test2
- id: test
file: tests/test3.yaml
name: Test tests/test3.yaml
pio_cache_key: test3
- id: test
file: tests/test3.1.yaml
name: Test tests/test3.1.yaml
pio_cache_key: test3.1
- id: test
file: tests/test4.yaml
name: Test tests/test4.yaml
pio_cache_key: test4
- id: test
file: tests/test5.yaml
name: Test tests/test5.yaml
pio_cache_key: test5
- id: test
file: tests/test6.yaml
name: Test tests/test6.yaml
pio_cache_key: test6
- id: test
file: tests/test7.yaml
name: Test tests/test7.yaml
pio_cache_key: test7
- id: pytest
name: Run pytest
- id: clang-format
name: Run script/clang-format
- id: clang-tidy
name: Run script/clang-tidy for ESP8266
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
@@ -90,119 +294,63 @@ jobs:
name: Run script/clang-tidy for ESP32 IDF
options: --environment esp32-idf-tidy --grep USE_ESP_IDF
pio_cache_key: tidyesp32-idf
- id: yamllint
name: Run yamllint
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
id: python
- name: Check out code from GitHub
uses: actions/checkout@v3.5.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: "3.9"
- name: Cache virtualenv
uses: actions/cache@v3
with:
path: .venv
# yamllint disable-line rule:line-length
key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }}
restore-keys: |
venv-${{ steps.python.outputs.python-version }}-
- name: Set up virtualenv
# yamllint disable rule:line-length
run: |
python -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
pip install -e .
echo "$GITHUB_WORKSPACE/.venv/bin" >> $GITHUB_PATH
echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> $GITHUB_ENV
# yamllint enable rule:line-length
# Use per check platformio cache because checks use different parts
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache@v3
uses: actions/cache@v3.3.1
with:
path: ~/.platformio
# yamllint disable-line rule:line-length
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
if: matrix.id == 'test' || matrix.id == 'clang-tidy'
- name: Install clang tools
run: |
sudo apt-get install \
clang-format-13 \
clang-tidy-11
if: matrix.id == 'clang-tidy' || matrix.id == 'clang-format'
- name: Install clang-tidy
run: sudo apt-get install clang-tidy-11
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/ci-custom.json"
echo "::add-matcher::.github/workflows/matchers/lint-python.json"
echo "::add-matcher::.github/workflows/matchers/python.json"
echo "::add-matcher::.github/workflows/matchers/pytest.json"
echo "::add-matcher::.github/workflows/matchers/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
- name: Lint Custom
run: |
script/ci-custom.py
script/build_codeowners.py --check
if: matrix.id == 'ci-custom'
- name: Lint Python
run: script/lint-python -a
if: matrix.id == 'lint-python'
- run: esphome compile ${{ matrix.file }}
if: matrix.id == 'test'
env:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Run pytest
run: |
pytest -vv --tb=native tests
if: matrix.id == 'pytest'
# Also run git-diff-index so that the step is marked as failed on
# formatting errors, since clang-format doesn't do anything but
# change files if -i is passed.
- name: Run clang-format
run: |
script/clang-format -i
git diff-index --quiet HEAD --
if: matrix.id == 'clang-format'
- name: Run clang-tidy
run: |
. venv/bin/activate
script/clang-tidy --all-headers --fix ${{ matrix.options }}
if: matrix.id == 'clang-tidy'
env:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Run yamllint
if: matrix.id == 'yamllint'
uses: frenck/action-yamllint@v1.4.0
- name: Suggested changes
run: script/ci-suggest-changes
# yamllint disable-line rule:line-length
if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format' || matrix.id == 'lint-python')
if: always()
ci-status:
name: CI Status
runs-on: ubuntu-latest
needs: [ci]
needs:
- common
- black
- ci-custom
- clang-format
- flake8
- pylint
- pytest
- pyupgrade
- yamllint
- compile-tests
- clang-tidy
if: always()
steps:
- name: Successful deploy
- name: Success
if: ${{ !(contains(needs.*.result, 'failure')) }}
run: exit 0
- name: Failing deploy
- name: Failure
if: ${{ contains(needs.*.result, 'failure') }}
run: exit 1

View File

@@ -49,9 +49,11 @@ jobs:
with:
python-version: "3.x"
- name: Set up python environment
env:
ESPHOME_NO_VENV: 1
run: |
script/setup
pip install setuptools wheel twine
pip install twine
- name: Build
run: python setup.py sdist bdist_wheel
- name: Upload

View File

@@ -26,7 +26,7 @@ jobs:
days-before-issue-close: -1
remove-stale-when-updated: true
stale-pr-label: "stale"
exempt-pr-labels: "no-stale"
exempt-pr-labels: "not-stale"
stale-pr-message: >
There hasn't been any activity on this pull request recently. This
pull request has been automatically marked as stale because of that

View File

@@ -6,14 +6,12 @@ on:
schedule:
- cron: '45 6 * * *'
permissions:
contents: write
pull-requests: write
jobs:
sync:
name: Sync Device Classes
runs-on: ubuntu-latest
if: github.repository == 'esphome/esphome'
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -38,23 +36,14 @@ jobs:
run: |
python ./script/sync-device_class.py
- name: Get PR template
id: pr-template-body
run: |
body=$(cat .github/PULL_REQUEST_TEMPLATE.md)
delimiter="$(openssl rand -hex 8)"
echo "body<<$delimiter" >> $GITHUB_OUTPUT
echo "$body" >> $GITHUB_OUTPUT
echo "$delimiter" >> $GITHUB_OUTPUT
- name: Commit changes
uses: peter-evans/create-pull-request@v5
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@nabucasa.com>
author: esphomebot <esphome@nabucasa.com>
branch: sync/device-classes/
branch-suffix: timestamp
branch: sync/device-classes
delete-branch: true
title: "Synchronise Device Classes from Home Assistant"
body: ${{ steps.pr-template-body.outputs.body }}
body-path: .github/PULL_REQUEST_TEMPLATE.md
token: ${{ secrets.DEVICE_CLASS_SYNC_TOKEN }}

View File

@@ -27,7 +27,7 @@ repos:
- --branch=release
- --branch=beta
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.2
rev: v3.7.0
hooks:
- id: pyupgrade
args: [--py39-plus]

18
.vscode/tasks.json vendored
View File

@@ -36,6 +36,24 @@
]
}
]
},
{
"label": "Generate proto files",
"type": "shell",
"command": "${command:python.interpreterPath}",
"args": [
"./script/api_protobuf/api_protobuf.py"
],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "never",
"close": true,
"panel": "new"
},
"problemMatcher": []
}
]
}

View File

@@ -17,8 +17,10 @@ esphome/components/adc/* @esphome/core
esphome/components/adc128s102/* @DeerMaximum
esphome/components/addressable_light/* @justfalter
esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_base/* @jeromelaban @ncareau
esphome/components/airthings_wave_mini/* @ncareau
esphome/components/airthings_wave_plus/* @jeromelaban
esphome/components/alarm_control_panel/* @grahambrown11
esphome/components/am43/* @buxtronix
esphome/components/am43/cover/* @buxtronix
esphome/components/am43/sensor/* @buxtronix
@@ -101,12 +103,13 @@ esphome/components/gpio/* @esphome/core
esphome/components/gps/* @coogle
esphome/components/graph/* @synco
esphome/components/growatt_solar/* @leeuwte
esphome/components/haier/* @Yarikx
esphome/components/haier/* @paveldn
esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hm3301/* @freekode
esphome/components/homeassistant/* @OttoWinter
esphome/components/honeywellabp/* @RubyBailey
esphome/components/host/* @esphome/core
@@ -220,6 +223,7 @@ esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz
esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rtttl/* @glmnet
esphome/components/safe_mode/* @jsuanet @paulmonigatti
@@ -275,13 +279,16 @@ esphome/components/tca9548a/* @andreashergert1984
esphome/components/tcl112/* @glmnet
esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax
esphome/components/template/alarm_control_panel/* @grahambrown11
esphome/components/thermostat/* @kbx81
esphome/components/time/* @OttoWinter
esphome/components/tlc5947/* @rnauber
esphome/components/tm1621/* @Philippe12
esphome/components/tm1637/* @glmnet
esphome/components/tm1638/* @skykingjwc
esphome/components/tm1651/* @freekode
esphome/components/tmp102/* @timsavage
esphome/components/tmp1075/* @sybrenstuvel
esphome/components/tmp117/* @Azimath
esphome/components/tof10120/* @wstrzalka
esphome/components/toshiba/* @kbx81
@@ -312,4 +319,5 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/* @nielsnl68 @numo68

View File

@@ -29,6 +29,8 @@ RUN \
git=1:2.30.2-1+deb11u2 \
curl=7.74.0-1.3+deb11u7 \
openssh-client=1:8.4p1-5+deb11u1 \
libcairo2=1.16.0-5 \
python3-cffi=1.14.5-1 \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \
@@ -52,7 +54,7 @@ RUN \
# Ubuntu python3-pip is missing wheel
pip3 install --no-cache-dir \
wheel==0.37.1 \
platformio==6.1.6 \
platformio==6.1.7 \
# Change some platformio settings
&& platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \

View File

@@ -18,6 +18,9 @@ from esphome.const import (
CONF_LOGGER,
CONF_NAME,
CONF_OTA,
CONF_MQTT,
CONF_MDNS,
CONF_DISABLED,
CONF_PASSWORD,
CONF_PORT,
CONF_ESPHOME,
@@ -42,7 +45,7 @@ from esphome.log import color, setup_log, Fore
_LOGGER = logging.getLogger(__name__)
def choose_prompt(options):
def choose_prompt(options, purpose: str = None):
if not options:
raise EsphomeError(
"Found no valid options for upload/logging, please make sure relevant "
@@ -53,7 +56,9 @@ def choose_prompt(options):
if len(options) == 1:
return options[0][1]
safe_print("Found multiple options, please choose one:")
safe_print(
f'Found multiple options{f" for {purpose}" if purpose else ""}, please choose one:'
)
for i, (desc, _) in enumerate(options):
safe_print(f" [{i+1}] {desc}")
@@ -72,7 +77,9 @@ def choose_prompt(options):
return options[opt - 1][1]
def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api):
def choose_upload_log_host(
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
):
options = []
for port in get_serial_ports():
options.append((f"{port.path} ({port.description})", port.path))
@@ -80,7 +87,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api
options.append((f"Over The Air ({CORE.address})", CORE.address))
if default == "OTA":
return CORE.address
if show_mqtt and "mqtt" in CORE.config:
if show_mqtt and CONF_MQTT in CORE.config:
options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT"))
if default == "OTA":
return "MQTT"
@@ -88,7 +95,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api
return default
if check_default is not None and check_default in [opt[1] for opt in options]:
return check_default
return choose_prompt(options)
return choose_prompt(options, purpose=purpose)
def get_port_type(port):
@@ -288,19 +295,30 @@ def upload_program(config, args, host):
return 1 # Unknown target platform
from esphome import espota2
if CONF_OTA not in config:
raise EsphomeError(
"Cannot upload Over the Air as the config does not include the ota: "
"component"
)
from esphome import espota2
ota_conf = config[CONF_OTA]
remote_port = ota_conf[CONF_PORT]
password = ota_conf.get(CONF_PASSWORD, "")
if (
get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED]
) and CONF_MQTT in config:
from esphome import mqtt
host = mqtt.get_esphome_device_ip(
config, args.username, args.password, args.client_id
)
if getattr(args, "file", None) is not None:
return espota2.run_ota(host, remote_port, password, args.file)
return espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
@@ -310,6 +328,13 @@ def show_logs(config, args, port):
if get_port_type(port) == "SERIAL":
return run_miniterm(config, port)
if get_port_type(port) == "NETWORK" and "api" in config:
if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config:
from esphome import mqtt
port = mqtt.get_esphome_device_ip(
config, args.username, args.password, args.client_id
)
from esphome.components.api.client import run_logs
return run_logs(config, port)
@@ -374,6 +399,7 @@ def command_upload(args, config):
show_ota=True,
show_mqtt=False,
show_api=False,
purpose="uploading",
)
exit_code = upload_program(config, args, port)
if exit_code != 0:
@@ -382,6 +408,15 @@ def command_upload(args, config):
return 0
def command_discover(args, config):
if "mqtt" in config:
from esphome import mqtt
return mqtt.show_discover(config, args.username, args.password, args.client_id)
raise EsphomeError("No discover method configured (mqtt)")
def command_logs(args, config):
port = choose_upload_log_host(
default=args.device,
@@ -389,6 +424,7 @@ def command_logs(args, config):
show_ota=False,
show_mqtt=True,
show_api=True,
purpose="logging",
)
return show_logs(config, args, port)
@@ -407,6 +443,7 @@ def command_run(args, config):
show_ota=True,
show_mqtt=False,
show_api=True,
purpose="uploading",
)
exit_code = upload_program(config, args, port)
if exit_code != 0:
@@ -420,6 +457,7 @@ def command_run(args, config):
show_ota=False,
show_mqtt=True,
show_api=True,
purpose="logging",
)
return show_logs(config, args, port)
@@ -623,6 +661,7 @@ POST_CONFIG_ACTIONS = {
"clean": command_clean,
"idedata": command_idedata,
"rename": command_rename,
"discover": command_discover,
}
@@ -711,6 +750,15 @@ def parse_args(argv):
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
)
parser_discover = subparsers.add_parser(
"discover",
help="Validate the configuration and show all discovered devices.",
parents=[mqtt_options],
)
parser_discover.add_argument(
"configuration", help="Your YAML configuration file.", nargs=1
)
parser_run = subparsers.add_parser(
"run",
help="Validate the configuration, create a binary, upload it, and start logs.",
@@ -932,6 +980,8 @@ def run_esphome(argv):
_LOGGER.error(e, exc_info=args.verbose)
return 1
_LOGGER.info("ESPHome %s", const.__version__)
for conf_path in args.configuration:
if any(os.path.basename(conf_path) == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", conf_path)

View File

@@ -0,0 +1,83 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, ble_client
from esphome.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
STATE_CLASS_MEASUREMENT,
UNIT_PERCENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
CONF_HUMIDITY,
CONF_TVOC,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
UNIT_PARTS_PER_BILLION,
ICON_RADIATOR,
)
CODEOWNERS = ["@ncareau", "@jeromelaban"]
DEPENDENCIES = ["ble_client"]
airthings_wave_base_ns = cg.esphome_ns.namespace("airthings_wave_base")
AirthingsWaveBase = airthings_wave_base_ns.class_(
"AirthingsWaveBase", cg.PollingComponent, ble_client.BLEClientNode
)
BASE_SCHEMA = (
sensor.SENSOR_SCHEMA.extend(
{
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=0,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("5min"))
.extend(ble_client.BLE_CLIENT_SCHEMA)
)
async def wave_base_to_code(var, config):
await cg.register_component(var, config)
await ble_client.register_ble_node(var, config)
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity(sens))
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature(sens))
if CONF_PRESSURE in config:
sens = await sensor.new_sensor(config[CONF_PRESSURE])
cg.add(var.set_pressure(sens))
if CONF_TVOC in config:
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))

View File

@@ -0,0 +1,83 @@
#include "airthings_wave_base.h"
#ifdef USE_ESP32
namespace esphome {
namespace airthings_wave_base {
static const char *const TAG = "airthings_wave_base";
void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_OPEN_EVT: {
if (param->open.status == ESP_GATT_OK) {
ESP_LOGI(TAG, "Connected successfully!");
}
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
ESP_LOGW(TAG, "Disconnected!");
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
this->handle_ = 0;
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_);
if (chr == nullptr) {
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(),
this->sensors_data_characteristic_uuid_.to_string().c_str());
break;
}
this->handle_ = chr->handle;
this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;
this->request_read_values_();
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->parent()->get_conn_id())
break;
if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
break;
}
if (param->read.handle == this->handle_) {
this->read_sensors(param->read.value, param->read.value_len);
}
break;
}
default:
break;
}
}
bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; }
void AirthingsWaveBase::update() {
if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) {
if (!this->parent()->enabled) {
ESP_LOGW(TAG, "Reconnecting to device");
this->parent()->set_enabled(true);
this->parent()->connect();
} else {
ESP_LOGW(TAG, "Connection in progress");
}
}
}
void AirthingsWaveBase::request_read_values_() {
auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_,
ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
}
}
} // namespace airthings_wave_base
} // namespace esphome
#endif // USE_ESP32

View File

@@ -0,0 +1,50 @@
#pragma once
#ifdef USE_ESP32
#include <esp_gattc_api.h>
#include <algorithm>
#include <iterator>
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"
namespace esphome {
namespace airthings_wave_base {
class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientNode {
public:
AirthingsWaveBase() = default;
void update() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
protected:
bool is_valid_voc_value_(uint16_t voc);
virtual void read_sensors(uint8_t *value, uint16_t value_len) = 0;
void request_read_values_();
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
sensor::Sensor *tvoc_sensor_{nullptr};
uint16_t handle_;
esp32_ble_tracker::ESPBTUUID service_uuid_;
esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_;
};
} // namespace airthings_wave_base
} // namespace esphome
#endif // USE_ESP32

View File

@@ -7,105 +7,47 @@ namespace airthings_wave_mini {
static const char *const TAG = "airthings_wave_mini";
void AirthingsWaveMini::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_OPEN_EVT: {
if (param->open.status == ESP_GATT_OK) {
ESP_LOGI(TAG, "Connected successfully!");
}
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
ESP_LOGW(TAG, "Disconnected!");
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
this->handle_ = 0;
auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_);
if (chr == nullptr) {
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(),
sensors_data_characteristic_uuid_.to_string().c_str());
break;
}
this->handle_ = chr->handle;
this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;
request_read_values_();
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->parent()->get_conn_id())
break;
if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
break;
}
if (param->read.handle == this->handle_) {
read_sensors_(param->read.value, param->read.value_len);
}
break;
}
default:
break;
}
}
void AirthingsWaveMini::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) {
auto *value = (WaveMiniReadings *) raw_value;
if (sizeof(WaveMiniReadings) <= value_len) {
this->humidity_sensor_->publish_state(value->humidity / 100.0f);
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f);
if (is_valid_voc_value_(value->voc)) {
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->publish_state(value->humidity / 100.0f);
}
if (this->pressure_sensor_ != nullptr) {
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
}
if (this->temperature_sensor_ != nullptr) {
this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f);
}
if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
this->tvoc_sensor_->publish_state(value->voc);
}
// This instance must not stay connected
// so other clients can connect to it (e.g. the
// mobile app).
parent()->set_enabled(false);
}
}
bool AirthingsWaveMini::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; }
void AirthingsWaveMini::update() {
if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) {
if (!parent()->enabled) {
ESP_LOGW(TAG, "Reconnecting to device");
parent()->set_enabled(true);
parent()->connect();
} else {
ESP_LOGW(TAG, "Connection in progress");
}
}
}
void AirthingsWaveMini::request_read_values_() {
auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_,
ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
this->parent()->set_enabled(false);
}
}
void AirthingsWaveMini::dump_config() {
// these really don't belong here, but there doesn't seem to be a
// practical way to have the base class use LOG_SENSOR and include
// the TAG from this component
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
}
AirthingsWaveMini::AirthingsWaveMini()
: PollingComponent(10000),
service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)),
sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {}
AirthingsWaveMini::AirthingsWaveMini() {
this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID);
this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
}
} // namespace airthings_wave_mini
} // namespace esphome

View File

@@ -2,14 +2,7 @@
#ifdef USE_ESP32
#include <esp_gattc_api.h>
#include <algorithm>
#include <iterator>
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "esphome/components/airthings_wave_base/airthings_wave_base.h"
namespace esphome {
namespace airthings_wave_mini {
@@ -17,35 +10,14 @@ namespace airthings_wave_mini {
static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba";
static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba";
class AirthingsWaveMini : public PollingComponent, public ble_client::BLEClientNode {
class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase {
public:
AirthingsWaveMini();
void dump_config() override;
void update() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
protected:
bool is_valid_voc_value_(uint16_t voc);
void read_sensors_(uint8_t *value, uint16_t value_len);
void request_read_values_();
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
sensor::Sensor *tvoc_sensor_{nullptr};
uint16_t handle_;
esp32_ble_tracker::ESPBTUUID service_uuid_;
esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_;
void read_sensors(uint8_t *value, uint16_t value_len) override;
struct WaveMiniReadings {
uint16_t unused01;

View File

@@ -1,82 +1,28 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, ble_client
from esphome.components import airthings_wave_base
from esphome.const import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
STATE_CLASS_MEASUREMENT,
UNIT_PERCENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
CONF_ID,
CONF_HUMIDITY,
CONF_TVOC,
CONF_PRESSURE,
CONF_TEMPERATURE,
UNIT_PARTS_PER_BILLION,
ICON_RADIATOR,
)
DEPENDENCIES = ["ble_client"]
DEPENDENCIES = airthings_wave_base.DEPENDENCIES
AUTO_LOAD = ["airthings_wave_base"]
airthings_wave_mini_ns = cg.esphome_ns.namespace("airthings_wave_mini")
AirthingsWaveMini = airthings_wave_mini_ns.class_(
"AirthingsWaveMini", cg.PollingComponent, ble_client.BLEClientNode
"AirthingsWaveMini", airthings_wave_base.AirthingsWaveBase
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(AirthingsWaveMini),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=2,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("5min"))
.extend(ble_client.BLE_CLIENT_SCHEMA),
CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(AirthingsWaveMini),
}
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await ble_client.register_ble_node(var, config)
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity(sens))
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature(sens))
if CONF_PRESSURE in config:
sens = await sensor.new_sensor(config[CONF_PRESSURE])
cg.add(var.set_pressure(sens))
if CONF_TVOC in config:
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))
await airthings_wave_base.wave_base_to_code(var, config)

View File

@@ -7,55 +7,7 @@ namespace airthings_wave_plus {
static const char *const TAG = "airthings_wave_plus";
void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_OPEN_EVT: {
if (param->open.status == ESP_GATT_OK) {
ESP_LOGI(TAG, "Connected successfully!");
}
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
ESP_LOGW(TAG, "Disconnected!");
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
this->handle_ = 0;
auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_);
if (chr == nullptr) {
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(),
sensors_data_characteristic_uuid_.to_string().c_str());
break;
}
this->handle_ = chr->handle;
this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;
request_read_values_();
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->parent()->get_conn_id())
break;
if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
break;
}
if (param->read.handle == this->handle_) {
read_sensors_(param->read.value, param->read.value_len);
}
break;
}
default:
break;
}
}
void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) {
auto *value = (WavePlusReadings *) raw_value;
if (sizeof(WavePlusReadings) <= value_len) {
@@ -64,26 +16,38 @@ void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
if (value->version == 1) {
ESP_LOGD(TAG, "ambient light = %d", value->ambientLight);
this->humidity_sensor_->publish_state(value->humidity / 2.0f);
if (is_valid_radon_value_(value->radon)) {
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->publish_state(value->humidity / 2.0f);
}
if ((this->radon_sensor_ != nullptr) && this->is_valid_radon_value_(value->radon)) {
this->radon_sensor_->publish_state(value->radon);
}
if (is_valid_radon_value_(value->radon_lt)) {
if ((this->radon_long_term_sensor_ != nullptr) && this->is_valid_radon_value_(value->radon_lt)) {
this->radon_long_term_sensor_->publish_state(value->radon_lt);
}
this->temperature_sensor_->publish_state(value->temperature / 100.0f);
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
if (is_valid_co2_value_(value->co2)) {
if (this->temperature_sensor_ != nullptr) {
this->temperature_sensor_->publish_state(value->temperature / 100.0f);
}
if (this->pressure_sensor_ != nullptr) {
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
}
if ((this->co2_sensor_ != nullptr) && this->is_valid_co2_value_(value->co2)) {
this->co2_sensor_->publish_state(value->co2);
}
if (is_valid_voc_value_(value->voc)) {
if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
this->tvoc_sensor_->publish_state(value->voc);
}
// This instance must not stay connected
// so other clients can connect to it (e.g. the
// mobile app).
parent()->set_enabled(false);
this->parent()->set_enabled(false);
} else {
ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version);
}
@@ -92,44 +56,26 @@ void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; }
bool AirthingsWavePlus::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; }
bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; }
void AirthingsWavePlus::update() {
if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) {
if (!parent()->enabled) {
ESP_LOGW(TAG, "Reconnecting to device");
parent()->set_enabled(true);
parent()->connect();
} else {
ESP_LOGW(TAG, "Connection in progress");
}
}
}
void AirthingsWavePlus::request_read_values_() {
auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_,
ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
}
}
void AirthingsWavePlus::dump_config() {
// these really don't belong here, but there doesn't seem to be a
// practical way to have the base class use LOG_SENSOR and include
// the TAG from this component
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
LOG_SENSOR(" ", "Radon", this->radon_sensor_);
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
LOG_SENSOR(" ", "Radon", this->radon_sensor_);
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
}
AirthingsWavePlus::AirthingsWavePlus()
: PollingComponent(10000),
service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)),
sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {}
AirthingsWavePlus::AirthingsWavePlus() {
this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID);
this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
}
} // namespace airthings_wave_plus
} // namespace esphome

View File

@@ -2,14 +2,7 @@
#ifdef USE_ESP32
#include <esp_gattc_api.h>
#include <algorithm>
#include <iterator>
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "esphome/components/airthings_wave_base/airthings_wave_base.h"
namespace esphome {
namespace airthings_wave_plus {
@@ -17,43 +10,25 @@ namespace airthings_wave_plus {
static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba";
static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba";
class AirthingsWavePlus : public PollingComponent, public ble_client::BLEClientNode {
class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
public:
AirthingsWavePlus();
void dump_config() override;
void update() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; }
void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
protected:
bool is_valid_radon_value_(uint16_t radon);
bool is_valid_voc_value_(uint16_t voc);
bool is_valid_co2_value_(uint16_t co2);
void read_sensors_(uint8_t *value, uint16_t value_len);
void request_read_values_();
void read_sensors(uint8_t *value, uint16_t value_len) override;
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *radon_sensor_{nullptr};
sensor::Sensor *radon_long_term_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
sensor::Sensor *co2_sensor_{nullptr};
sensor::Sensor *tvoc_sensor_{nullptr};
uint16_t handle_;
esp32_ble_tracker::ESPBTUUID service_uuid_;
esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_;
struct WavePlusReadings {
uint8_t version;

View File

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

View File

@@ -0,0 +1,165 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.automation import maybe_simple_id
from esphome.core import CORE, coroutine_with_priority
from esphome.const import (
CONF_ID,
CONF_ON_STATE,
CONF_TRIGGER_ID,
CONF_CODE,
)
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@grahambrown11"]
IS_PLATFORM_COMPONENT = True
CONF_ON_TRIGGERED = "on_triggered"
CONF_ON_CLEARED = "on_cleared"
alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel")
AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase)
StateTrigger = alarm_control_panel_ns.class_(
"StateTrigger", automation.Trigger.template()
)
TriggeredTrigger = alarm_control_panel_ns.class_(
"TriggeredTrigger", automation.Trigger.template()
)
ClearedTrigger = alarm_control_panel_ns.class_(
"ClearedTrigger", automation.Trigger.template()
)
ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action)
ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action)
DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action)
PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action)
TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action)
AlarmControlPanelCondition = alarm_control_panel_ns.class_(
"AlarmControlPanelCondition", automation.Condition
)
ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(AlarmControlPanel),
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
}
),
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger),
}
),
cv.Optional(CONF_ON_CLEARED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger),
}
),
}
)
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(AlarmControlPanel),
cv.Optional(CONF_CODE): cv.templatable(cv.string),
}
)
ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(AlarmControlPanel),
}
)
async def setup_alarm_control_panel_core_(var, config):
await setup_entity(var, config)
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_TRIGGERED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_CLEARED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
async def register_alarm_control_panel(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_alarm_control_panel(var))
await setup_alarm_control_panel_core_(var, config)
@automation.register_action(
"alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
)
async def alarm_action_arm_away_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_CODE in config:
templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string)
cg.add(var.set_code(templatable_))
return var
@automation.register_action(
"alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
)
async def alarm_action_arm_home_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_CODE in config:
templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string)
cg.add(var.set_code(templatable_))
return var
@automation.register_action(
"alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
)
async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_CODE in config:
templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string)
cg.add(var.set_code(templatable_))
return var
@automation.register_action(
"alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
)
async def alarm_action_pending_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_action(
"alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
)
async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_condition(
"alarm_control_panel.is_armed",
AlarmControlPanelCondition,
ALARM_CONTROL_PANEL_CONDITION_SCHEMA,
)
async def alarm_control_panel_is_armed_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_global(alarm_control_panel_ns.using)
cg.add_define("USE_ALARM_CONTROL_PANEL")

View File

@@ -0,0 +1,111 @@
#include <utility>
#include "alarm_control_panel.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace alarm_control_panel {
static const char *const TAG = "alarm_control_panel";
AlarmControlPanelCall AlarmControlPanel::make_call() { return AlarmControlPanelCall(this); }
bool AlarmControlPanel::is_state_armed(AlarmControlPanelState state) {
switch (state) {
case ACP_STATE_ARMED_AWAY:
case ACP_STATE_ARMED_HOME:
case ACP_STATE_ARMED_NIGHT:
case ACP_STATE_ARMED_VACATION:
case ACP_STATE_ARMED_CUSTOM_BYPASS:
return true;
default:
return false;
}
};
void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
this->last_update_ = millis();
if (state != this->current_state_) {
auto prev_state = this->current_state_;
ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state;
this->state_callback_.call();
if (state == ACP_STATE_TRIGGERED) {
this->triggered_callback_.call();
}
if (prev_state == ACP_STATE_TRIGGERED) {
this->cleared_callback_.call();
}
if (state == this->desired_state_) {
// only store when in the desired state
this->pref_.save(&state);
}
}
}
void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback) {
this->state_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_triggered_callback(std::function<void()> &&callback) {
this->triggered_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
this->cleared_callback_.add(std::move(callback));
}
void AlarmControlPanel::arm_away(optional<std::string> code) {
auto call = this->make_call();
call.arm_away();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::arm_home(optional<std::string> code) {
auto call = this->make_call();
call.arm_home();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::arm_night(optional<std::string> code) {
auto call = this->make_call();
call.arm_night();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::arm_vacation(optional<std::string> code) {
auto call = this->make_call();
call.arm_vacation();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::arm_custom_bypass(optional<std::string> code) {
auto call = this->make_call();
call.arm_custom_bypass();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::disarm(optional<std::string> code) {
auto call = this->make_call();
call.disarm();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -0,0 +1,136 @@
#pragma once
#include <map>
#include "alarm_control_panel_call.h"
#include "alarm_control_panel_state.h"
#include "esphome/core/automation.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/log.h"
namespace esphome {
namespace alarm_control_panel {
enum AlarmControlPanelFeature : uint8_t {
// Matches Home Assistant values
ACP_FEAT_ARM_HOME = 1 << 0,
ACP_FEAT_ARM_AWAY = 1 << 1,
ACP_FEAT_ARM_NIGHT = 1 << 2,
ACP_FEAT_TRIGGER = 1 << 3,
ACP_FEAT_ARM_CUSTOM_BYPASS = 1 << 4,
ACP_FEAT_ARM_VACATION = 1 << 5,
};
class AlarmControlPanel : public EntityBase {
public:
/** Make a AlarmControlPanelCall
*
*/
AlarmControlPanelCall make_call();
/** Set the state of the alarm_control_panel.
*
* @param state The AlarmControlPanelState.
*/
void publish_state(AlarmControlPanelState state);
/** Add a callback for when the state of the alarm_control_panel changes
*
* @param callback The callback function
*/
void add_on_state_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel chanes to triggered
*
* @param callback The callback function
*/
void add_on_triggered_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel clears from triggered
*
* @param callback The callback function
*/
void add_on_cleared_callback(std::function<void()> &&callback);
/** A numeric representation of the supported features as per HomeAssistant
*
*/
virtual uint32_t get_supported_features() const = 0;
/** Returns if the alarm_control_panel has a code
*
*/
virtual bool get_requires_code() const = 0;
/** Returns if the alarm_control_panel requires a code to arm
*
*/
virtual bool get_requires_code_to_arm() const = 0;
/** arm the alarm in away mode
*
* @param code The code
*/
void arm_away(optional<std::string> code = nullopt);
/** arm the alarm in home mode
*
* @param code The code
*/
void arm_home(optional<std::string> code = nullopt);
/** arm the alarm in night mode
*
* @param code The code
*/
void arm_night(optional<std::string> code = nullopt);
/** arm the alarm in vacation mode
*
* @param code The code
*/
void arm_vacation(optional<std::string> code = nullopt);
/** arm the alarm in custom bypass mode
*
* @param code The code
*/
void arm_custom_bypass(optional<std::string> code = nullopt);
/** disarm the alarm
*
* @param code The code
*/
void disarm(optional<std::string> code = nullopt);
/** Get the state
*
*/
AlarmControlPanelState get_state() const { return this->current_state_; }
// is the state one of the armed states
bool is_state_armed(AlarmControlPanelState state);
protected:
friend AlarmControlPanelCall;
// in order to store last panel state in flash
ESPPreferenceObject pref_;
// current state
AlarmControlPanelState current_state_;
// the desired (or previous) state
AlarmControlPanelState desired_state_;
// last time the state was updated
uint32_t last_update_;
// the call control function
virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback
CallbackManager<void()> state_callback_{};
// trigger callback
CallbackManager<void()> triggered_callback_{};
// clear callback
CallbackManager<void()> cleared_callback_{};
};
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -0,0 +1,99 @@
#include "alarm_control_panel_call.h"
#include "alarm_control_panel.h"
#include "esphome/core/log.h"
namespace esphome {
namespace alarm_control_panel {
static const char *const TAG = "alarm_control_panel";
AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {}
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) {
this->code_ = code;
return *this;
}
AlarmControlPanelCall &AlarmControlPanelCall::arm_away() {
this->state_ = ACP_STATE_ARMED_AWAY;
return *this;
}
AlarmControlPanelCall &AlarmControlPanelCall::arm_home() {
this->state_ = ACP_STATE_ARMED_HOME;
return *this;
}
AlarmControlPanelCall &AlarmControlPanelCall::arm_night() {
this->state_ = ACP_STATE_ARMED_NIGHT;
return *this;
}
AlarmControlPanelCall &AlarmControlPanelCall::arm_vacation() {
this->state_ = ACP_STATE_ARMED_VACATION;
return *this;
}
AlarmControlPanelCall &AlarmControlPanelCall::arm_custom_bypass() {
this->state_ = ACP_STATE_ARMED_CUSTOM_BYPASS;
return *this;
}
AlarmControlPanelCall &AlarmControlPanelCall::disarm() {
this->state_ = ACP_STATE_DISARMED;
return *this;
}
AlarmControlPanelCall &AlarmControlPanelCall::pending() {
this->state_ = ACP_STATE_PENDING;
return *this;
}
AlarmControlPanelCall &AlarmControlPanelCall::triggered() {
this->state_ = ACP_STATE_TRIGGERED;
return *this;
}
const optional<AlarmControlPanelState> &AlarmControlPanelCall::get_state() const { return this->state_; }
const optional<std::string> &AlarmControlPanelCall::get_code() const { return this->code_; }
void AlarmControlPanelCall::validate_() {
if (this->state_.has_value()) {
auto state = *this->state_;
if (this->parent_->is_state_armed(state) && this->parent_->get_state() != ACP_STATE_DISARMED) {
ESP_LOGW(TAG, "Cannot arm when not disarmed");
this->state_.reset();
return;
}
if (state == ACP_STATE_PENDING && this->parent_->get_state() == ACP_STATE_DISARMED) {
ESP_LOGW(TAG, "Cannot trip alarm when disarmed");
this->state_.reset();
return;
}
if (state == ACP_STATE_DISARMED &&
!(this->parent_->is_state_armed(this->parent_->get_state()) ||
this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_ARMING ||
this->parent_->get_state() == ACP_STATE_TRIGGERED)) {
ESP_LOGW(TAG, "Cannot disarm when not armed");
this->state_.reset();
return;
}
if (state == ACP_STATE_ARMED_HOME && (this->parent_->get_supported_features() & ACP_FEAT_ARM_HOME) == 0) {
ESP_LOGW(TAG, "Cannot arm home when not supported");
this->state_.reset();
return;
}
}
}
void AlarmControlPanelCall::perform() {
this->validate_();
if (this->state_) {
this->parent_->control(*this);
}
}
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -0,0 +1,40 @@
#pragma once
#include <string>
#include "alarm_control_panel_state.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace alarm_control_panel {
class AlarmControlPanel;
class AlarmControlPanelCall {
public:
AlarmControlPanelCall(AlarmControlPanel *parent);
AlarmControlPanelCall &set_code(const std::string &code);
AlarmControlPanelCall &arm_away();
AlarmControlPanelCall &arm_home();
AlarmControlPanelCall &arm_night();
AlarmControlPanelCall &arm_vacation();
AlarmControlPanelCall &arm_custom_bypass();
AlarmControlPanelCall &disarm();
AlarmControlPanelCall &pending();
AlarmControlPanelCall &triggered();
void perform();
const optional<AlarmControlPanelState> &get_state() const;
const optional<std::string> &get_code() const;
protected:
AlarmControlPanel *parent_;
optional<std::string> code_{};
optional<AlarmControlPanelState> state_{};
void validate_();
};
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -0,0 +1,34 @@
#include "alarm_control_panel_state.h"
namespace esphome {
namespace alarm_control_panel {
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) {
switch (state) {
case ACP_STATE_DISARMED:
return LOG_STR("DISARMED");
case ACP_STATE_ARMED_HOME:
return LOG_STR("ARMED_HOME");
case ACP_STATE_ARMED_AWAY:
return LOG_STR("ARMED_AWAY");
case ACP_STATE_ARMED_NIGHT:
return LOG_STR("NIGHT");
case ACP_STATE_ARMED_VACATION:
return LOG_STR("ARMED_VACATION");
case ACP_STATE_ARMED_CUSTOM_BYPASS:
return LOG_STR("ARMED_CUSTOM_BYPASS");
case ACP_STATE_PENDING:
return LOG_STR("PENDING");
case ACP_STATE_ARMING:
return LOG_STR("ARMING");
case ACP_STATE_DISARMING:
return LOG_STR("DISARMING");
case ACP_STATE_TRIGGERED:
return LOG_STR("TRIGGERED");
default:
return LOG_STR("UNKNOWN");
}
}
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -0,0 +1,29 @@
#pragma once
#include <cstdint>
#include "esphome/core/log.h"
namespace esphome {
namespace alarm_control_panel {
enum AlarmControlPanelState : uint8_t {
ACP_STATE_DISARMED = 0,
ACP_STATE_ARMED_HOME = 1,
ACP_STATE_ARMED_AWAY = 2,
ACP_STATE_ARMED_NIGHT = 3,
ACP_STATE_ARMED_VACATION = 4,
ACP_STATE_ARMED_CUSTOM_BYPASS = 5,
ACP_STATE_PENDING = 6,
ACP_STATE_ARMING = 7,
ACP_STATE_DISARMING = 8,
ACP_STATE_TRIGGERED = 9
};
/** Returns a string representation of the state.
*
* @param state The AlarmControlPanelState.
*/
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state);
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -0,0 +1,115 @@
#pragma once
#include "esphome/core/automation.h"
#include "alarm_control_panel.h"
namespace esphome {
namespace alarm_control_panel {
class StateTrigger : public Trigger<> {
public:
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_state_callback([this]() { this->trigger(); });
}
};
class TriggeredTrigger : public Trigger<> {
public:
explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); });
}
};
class ClearedTrigger : public Trigger<> {
public:
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_cleared_callback([this]() { this->trigger(); });
}
};
template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
public:
explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
TEMPLATABLE_VALUE(std::string, code)
void play(Ts... x) override {
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_away();
call.perform();
}
protected:
AlarmControlPanel *alarm_control_panel_;
};
template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
public:
explicit ArmHomeAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
TEMPLATABLE_VALUE(std::string, code)
void play(Ts... x) override {
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_home();
call.perform();
}
protected:
AlarmControlPanel *alarm_control_panel_;
};
template<typename... Ts> class DisarmAction : public Action<Ts...> {
public:
explicit DisarmAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
TEMPLATABLE_VALUE(std::string, code)
void play(Ts... x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;
};
template<typename... Ts> class PendingAction : public Action<Ts...> {
public:
explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
void play(Ts... x) override { this->alarm_control_panel_->make_call().pending().perform(); }
protected:
AlarmControlPanel *alarm_control_panel_;
};
template<typename... Ts> class TriggeredAction : public Action<Ts...> {
public:
explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
void play(Ts... x) override { this->alarm_control_panel_->make_call().triggered().perform(); }
protected:
AlarmControlPanel *alarm_control_panel_;
};
template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts...> {
public:
AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {}
bool check(Ts... x) override {
return this->parent_->is_state_armed(this->parent_->get_state()) ||
this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED;
}
protected:
AlarmControlPanel *parent_;
};
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -1,35 +1,124 @@
import logging
from esphome import core
from esphome.components import display, font
from esphome import automation, core
from esphome.components import font
import esphome.components.image as espImage
from esphome.components.image import CONF_USE_TRANSPARENCY
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_RESIZE, CONF_TYPE
from esphome.const import (
CONF_FILE,
CONF_ID,
CONF_RAW_DATA_ID,
CONF_REPEAT,
CONF_RESIZE,
CONF_TYPE,
)
from esphome.core import CORE, HexInt
_LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["image"]
CODEOWNERS = ["@syndlex"]
DEPENDENCIES = ["display"]
MULTI_CONF = True
Animation_ = display.display_ns.class_("Animation", espImage.Image_)
CONF_LOOP = "loop"
CONF_START_FRAME = "start_frame"
CONF_END_FRAME = "end_frame"
CONF_FRAME = "frame"
animation_ns = cg.esphome_ns.namespace("animation")
Animation_ = animation_ns.class_("Animation", espImage.Image_)
# Actions
NextFrameAction = animation_ns.class_(
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
)
PrevFrameAction = animation_ns.class_(
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
)
SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
)
def validate_cross_dependencies(config):
"""
Validate fields whose possible values depend on other fields.
For example, validate that explicitly transparent image types
have "use_transparency" set to True.
Also set the default value for those kind of dependent fields.
"""
image_type = config[CONF_TYPE]
is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"]
# If the use_transparency option was not specified, set the default depending on the image type
if CONF_USE_TRANSPARENCY not in config:
config[CONF_USE_TRANSPARENCY] = is_transparent_type
if is_transparent_type and not config[CONF_USE_TRANSPARENCY]:
raise cv.Invalid(f"Image type {image_type} must always be transparent.")
return config
ANIMATION_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Required(CONF_FILE): cv.file_,
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
espImage.IMAGE_TYPE, upper=True
),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
cv.All(
{
cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Required(CONF_FILE): cv.file_,
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
espImage.IMAGE_TYPE, upper=True
),
# Not setting default here on purpose; the default depends on the image type,
# and thus will be set in the "validate_cross_dependencies" validator.
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
cv.Optional(CONF_LOOP): cv.All(
{
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
cv.Optional(CONF_END_FRAME): cv.positive_int,
cv.Optional(CONF_REPEAT): cv.positive_int,
}
),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
},
validate_cross_dependencies,
)
)
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA)
CODEOWNERS = ["@syndlex"]
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
PREV_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
SET_FRAME_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(Animation_),
cv.Required(CONF_FRAME): cv.uint16_t,
}
)
@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA)
@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA)
@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA)
async def animation_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_FRAME in config:
template_ = await cg.templatable(config[CONF_FRAME], args, cg.uint16)
cg.add(var.set_frame(template_))
return var
async def to_code(config):
@@ -50,16 +139,19 @@ async def to_code(config):
else:
if width > 500 or height > 500:
_LOGGER.warning(
"The image you requested is very big. Please consider using"
" the resize parameter."
'The image "%s" you requested is very big. Please consider'
" using the resize parameter.",
path,
)
transparent = config[CONF_USE_TRANSPARENCY]
if config[CONF_TYPE] == "GRAYSCALE":
data = [0 for _ in range(height * width * frames)]
pos = 0
for frameIndex in range(frames):
image.seek(frameIndex)
frame = image.convert("L", dither=Image.NONE)
frame = image.convert("LA", dither=Image.NONE)
if CONF_RESIZE in config:
frame = frame.resize([width, height])
pixels = list(frame.getdata())
@@ -67,16 +159,22 @@ async def to_code(config):
raise core.EsphomeError(
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
)
for pix in pixels:
for pix, a in pixels:
if transparent:
if pix == 1:
pix = 0
if a < 0x80:
pix = 1
data[pos] = pix
pos += 1
elif config[CONF_TYPE] == "RGB24":
data = [0 for _ in range(height * width * 3 * frames)]
elif config[CONF_TYPE] == "RGBA":
data = [0 for _ in range(height * width * 4 * frames)]
pos = 0
for frameIndex in range(frames):
image.seek(frameIndex)
frame = image.convert("RGB")
frame = image.convert("RGBA")
if CONF_RESIZE in config:
frame = frame.resize([width, height])
pixels = list(frame.getdata())
@@ -91,13 +189,15 @@ async def to_code(config):
pos += 1
data[pos] = pix[2]
pos += 1
data[pos] = pix[3]
pos += 1
elif config[CONF_TYPE] == "RGB565":
data = [0 for _ in range(height * width * 2 * frames)]
elif config[CONF_TYPE] == "RGB24":
data = [0 for _ in range(height * width * 3 * frames)]
pos = 0
for frameIndex in range(frames):
image.seek(frameIndex)
frame = image.convert("RGB")
frame = image.convert("RGBA")
if CONF_RESIZE in config:
frame = frame.resize([width, height])
pixels = list(frame.getdata())
@@ -105,14 +205,50 @@ async def to_code(config):
raise core.EsphomeError(
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
)
for pix in pixels:
R = pix[0] >> 3
G = pix[1] >> 2
B = pix[2] >> 3
for r, g, b, a in pixels:
if transparent:
if r == 0 and g == 0 and b == 1:
b = 0
if a < 0x80:
r = 0
g = 0
b = 1
data[pos] = r
pos += 1
data[pos] = g
pos += 1
data[pos] = b
pos += 1
elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]:
data = [0 for _ in range(height * width * 2 * frames)]
pos = 0
for frameIndex in range(frames):
image.seek(frameIndex)
frame = image.convert("RGBA")
if CONF_RESIZE in config:
frame = frame.resize([width, height])
pixels = list(frame.getdata())
if len(pixels) != height * width:
raise core.EsphomeError(
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
)
for r, g, b, a in pixels:
R = r >> 3
G = g >> 2
B = b >> 3
rgb = (R << 11) | (G << 5) | B
if transparent:
if rgb == 0x0020:
rgb = 0
if a < 0x80:
rgb = 0x0020
data[pos] = rgb >> 8
pos += 1
data[pos] = rgb & 255
data[pos] = rgb & 0xFF
pos += 1
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
@@ -120,19 +256,31 @@ async def to_code(config):
data = [0 for _ in range((height * width8 // 8) * frames)]
for frameIndex in range(frames):
image.seek(frameIndex)
if transparent:
alpha = image.split()[-1]
has_alpha = alpha.getextrema()[0] < 0xFF
frame = image.convert("1", dither=Image.NONE)
if CONF_RESIZE in config:
frame = frame.resize([width, height])
for y in range(height):
for x in range(width):
if frame.getpixel((x, y)):
if transparent:
alpha = alpha.resize([width, height])
for x, y in [(i, j) for i in range(width) for j in range(height)]:
if transparent and has_alpha:
if not alpha.getpixel((x, y)):
continue
pos = x + y * width8 + (height * width8 * frameIndex)
data[pos // 8] |= 0x80 >> (pos % 8)
elif frame.getpixel((x, y)):
continue
pos = x + y * width8 + (height * width8 * frameIndex)
data[pos // 8] |= 0x80 >> (pos % 8)
else:
raise core.EsphomeError(
f"Animation f{config[CONF_ID]} has not supported type {config[CONF_TYPE]}."
)
rhs = [HexInt(x) for x in data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
cg.new_Pvariable(
var = cg.new_Pvariable(
config[CONF_ID],
prog_arr,
width,
@@ -140,3 +288,9 @@ async def to_code(config):
frames,
espImage.IMAGE_TYPE[config[CONF_TYPE]],
)
cg.add(var.set_transparency(transparent))
if CONF_LOOP in config:
start = config[CONF_LOOP][CONF_START_FRAME]
end = config[CONF_LOOP].get(CONF_END_FRAME, frames)
count = config[CONF_LOOP].get(CONF_REPEAT, -1)
cg.add(var.set_loop(start, end, count))

View File

@@ -0,0 +1,70 @@
#include "animation.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace animation {
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
image::ImageType type)
: Image(data_start, width, height, type),
animation_data_start_(data_start),
current_frame_(0),
animation_frame_count_(animation_frame_count),
loop_start_frame_(0),
loop_end_frame_(animation_frame_count_),
loop_count_(0),
loop_current_iteration_(1) {}
void Animation::set_loop(uint32_t start_frame, uint32_t end_frame, int count) {
loop_start_frame_ = std::min(start_frame, animation_frame_count_);
loop_end_frame_ = std::min(end_frame, animation_frame_count_);
loop_count_ = count;
loop_current_iteration_ = 1;
}
uint32_t Animation::get_animation_frame_count() const { return this->animation_frame_count_; }
int Animation::get_current_frame() const { return this->current_frame_; }
void Animation::next_frame() {
this->current_frame_++;
if (loop_count_ && this->current_frame_ == loop_end_frame_ &&
(this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) {
this->current_frame_ = loop_start_frame_;
this->loop_current_iteration_++;
}
if (this->current_frame_ >= animation_frame_count_) {
this->loop_current_iteration_ = 1;
this->current_frame_ = 0;
}
this->update_data_start_();
}
void Animation::prev_frame() {
this->current_frame_--;
if (this->current_frame_ < 0) {
this->current_frame_ = this->animation_frame_count_ - 1;
}
this->update_data_start_();
}
void Animation::set_frame(int frame) {
unsigned abs_frame = abs(frame);
if (abs_frame < this->animation_frame_count_) {
if (frame >= 0) {
this->current_frame_ = frame;
} else {
this->current_frame_ = this->animation_frame_count_ - abs_frame;
}
}
this->update_data_start_();
}
void Animation::update_data_start_() {
const uint32_t image_size = image_type_to_width_stride(this->width_, this->type_) * this->height_;
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
}
} // namespace animation
} // namespace esphome

View File

@@ -0,0 +1,67 @@
#pragma once
#include "esphome/components/image/image.h"
#include "esphome/core/automation.h"
namespace esphome {
namespace animation {
class Animation : public image::Image {
public:
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type);
uint32_t get_animation_frame_count() const;
int get_current_frame() const;
void next_frame();
void prev_frame();
/** Selects a specific frame within the animation.
*
* @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame.
*/
void set_frame(int frame);
void set_loop(uint32_t start_frame, uint32_t end_frame, int count);
protected:
void update_data_start_();
const uint8_t *animation_data_start_;
int current_frame_;
uint32_t animation_frame_count_;
uint32_t loop_start_frame_;
uint32_t loop_end_frame_;
int loop_count_;
int loop_current_iteration_;
};
template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> {
public:
AnimationNextFrameAction(Animation *parent) : parent_(parent) {}
void play(Ts... x) override { this->parent_->next_frame(); }
protected:
Animation *parent_;
};
template<typename... Ts> class AnimationPrevFrameAction : public Action<Ts...> {
public:
AnimationPrevFrameAction(Animation *parent) : parent_(parent) {}
void play(Ts... x) override { this->parent_->prev_frame(); }
protected:
Animation *parent_;
};
template<typename... Ts> class AnimationSetFrameAction : public Action<Ts...> {
public:
AnimationSetFrameAction(Animation *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(uint16_t, frame)
void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); }
protected:
Animation *parent_;
};
} // namespace animation
} // namespace esphome

View File

@@ -56,6 +56,8 @@ service APIConnection {
rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {}
}
@@ -206,7 +208,8 @@ message DeviceInfoResponse {
uint32 webserver_port = 10;
uint32 bluetooth_proxy_version = 11;
uint32 legacy_bluetooth_proxy_version = 11;
uint32 bluetooth_proxy_feature_flags = 15;
string manufacturer = 12;
@@ -1130,6 +1133,8 @@ message SubscribeBluetoothLEAdvertisementsRequest {
option (id) = 66;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_BLUETOOTH_PROXY";
uint32 flags = 1;
}
message BluetoothServiceData {
@@ -1154,6 +1159,23 @@ message BluetoothLEAdvertisementResponse {
uint32 address_type = 7;
}
message BluetoothLERawAdvertisement {
uint64 address = 1;
sint32 rssi = 2;
uint32 address_type = 3;
bytes data = 4;
}
message BluetoothLERawAdvertisementsResponse {
option (id) = 93;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BLUETOOTH_PROXY";
option (no_delay) = true;
repeated BluetoothLERawAdvertisement advertisements = 1;
}
enum BluetoothDeviceRequestType {
BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0;
BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1;
@@ -1397,6 +1419,7 @@ message VoiceAssistantRequest {
option (ifdef) = "USE_VOICE_ASSISTANT";
bool start = 1;
string conversation_id = 2;
}
message VoiceAssistantResponse {
@@ -1433,3 +1456,63 @@ message VoiceAssistantEventResponse {
VoiceAssistantEvent event_type = 1;
repeated VoiceAssistantEventData data = 2;
}
// ==================== ALARM CONTROL PANEL ====================
enum AlarmControlPanelState {
ALARM_STATE_DISARMED = 0;
ALARM_STATE_ARMED_HOME = 1;
ALARM_STATE_ARMED_AWAY = 2;
ALARM_STATE_ARMED_NIGHT = 3;
ALARM_STATE_ARMED_VACATION = 4;
ALARM_STATE_ARMED_CUSTOM_BYPASS = 5;
ALARM_STATE_PENDING = 6;
ALARM_STATE_ARMING = 7;
ALARM_STATE_DISARMING = 8;
ALARM_STATE_TRIGGERED = 9;
}
enum AlarmControlPanelStateCommand {
ALARM_CONTROL_PANEL_DISARM = 0;
ALARM_CONTROL_PANEL_ARM_AWAY = 1;
ALARM_CONTROL_PANEL_ARM_HOME = 2;
ALARM_CONTROL_PANEL_ARM_NIGHT = 3;
ALARM_CONTROL_PANEL_ARM_VACATION = 4;
ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5;
ALARM_CONTROL_PANEL_TRIGGER = 6;
}
message ListEntitiesAlarmControlPanelResponse {
option (id) = 94;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 supported_features = 8;
bool requires_code = 9;
bool requires_code_to_arm = 10;
}
message AlarmControlPanelStateResponse {
option (id) = 95;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
option (no_delay) = true;
fixed32 key = 1;
AlarmControlPanelState state = 2;
}
message AlarmControlPanelCommandRequest {
option (id) = 96;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
option (no_delay) = true;
fixed32 key = 1;
AlarmControlPanelStateCommand command = 2;
string code = 3;
}

View File

@@ -51,6 +51,14 @@ void APIConnection::start() {
helper_->set_log_info(client_info_);
}
APIConnection::~APIConnection() {
#ifdef USE_BLUETOOTH_PROXY
if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) {
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
}
#endif
}
void APIConnection::loop() {
if (this->remove_)
return;
@@ -845,9 +853,13 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags);
}
void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
}
bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg) {
if (!this->bluetooth_le_advertisement_subscription_)
return false;
if (this->client_api_version_major_ < 1 || this->client_api_version_minor_ < 7) {
BluetoothLEAdvertisementResponse resp = msg;
for (auto &service : resp.service_data) {
@@ -895,11 +907,12 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_
#endif
#ifdef USE_VOICE_ASSISTANT
bool APIConnection::request_voice_assistant(bool start) {
bool APIConnection::request_voice_assistant(bool start, const std::string &conversation_id) {
if (!this->voice_assistant_subscription_)
return false;
VoiceAssistantRequest msg;
msg.start = start;
msg.conversation_id = conversation_id;
return this->send_voice_assistant_request(msg);
}
void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) {
@@ -918,6 +931,64 @@ void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventR
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
if (!this->state_subscription_)
return false;
AlarmControlPanelStateResponse resp{};
resp.key = a_alarm_control_panel->get_object_id_hash();
resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state());
return this->send_alarm_control_panel_state_response(resp);
}
bool APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
ListEntitiesAlarmControlPanelResponse msg;
msg.key = a_alarm_control_panel->get_object_id_hash();
msg.object_id = a_alarm_control_panel->get_object_id();
msg.name = a_alarm_control_panel->get_name();
msg.unique_id = get_default_unique_id("alarm_control_panel", a_alarm_control_panel);
msg.icon = a_alarm_control_panel->get_icon();
msg.disabled_by_default = a_alarm_control_panel->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(a_alarm_control_panel->get_entity_category());
msg.supported_features = a_alarm_control_panel->get_supported_features();
msg.requires_code = a_alarm_control_panel->get_requires_code();
msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm();
return this->send_list_entities_alarm_control_panel_response(msg);
}
void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) {
alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = App.get_alarm_control_panel_by_key(msg.key);
if (a_alarm_control_panel == nullptr)
return;
auto call = a_alarm_control_panel->make_call();
switch (msg.command) {
case enums::ALARM_CONTROL_PANEL_DISARM:
call.disarm();
break;
case enums::ALARM_CONTROL_PANEL_ARM_AWAY:
call.arm_away();
break;
case enums::ALARM_CONTROL_PANEL_ARM_HOME:
call.arm_home();
break;
case enums::ALARM_CONTROL_PANEL_ARM_NIGHT:
call.arm_night();
break;
case enums::ALARM_CONTROL_PANEL_ARM_VACATION:
call.arm_vacation();
break;
case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS:
call.arm_custom_bypass();
break;
case enums::ALARM_CONTROL_PANEL_TRIGGER:
call.pending();
break;
}
call.set_code(msg.code);
call.perform();
}
#endif
bool APIConnection::send_log_message(int level, const char *tag, const char *line) {
if (this->log_subscription_ < level)
return false;
@@ -942,7 +1013,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) {
HelloResponse resp;
resp.api_version_major = 1;
resp.api_version_minor = 8;
resp.api_version_minor = 9;
resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
resp.name = App.get_name();
@@ -994,9 +1065,8 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
resp.webserver_port = USE_WEBSERVER_PORT;
#endif
#ifdef USE_BLUETOOTH_PROXY
resp.bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->has_active()
? bluetooth_proxy::ACTIVE_CONNECTIONS_VERSION
: bluetooth_proxy::PASSIVE_ONLY_VERSION;
resp.legacy_bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->get_legacy_version();
resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags();
#endif
#ifdef USE_VOICE_ASSISTANT
resp.voice_assistant_version = voice_assistant::global_voice_assistant->get_version();

View File

@@ -16,7 +16,7 @@ namespace api {
class APIConnection : public APIServerConnection {
public:
APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
virtual ~APIConnection() = default;
virtual ~APIConnection();
void start();
void loop();
@@ -98,12 +98,8 @@ class APIConnection : public APIServerConnection {
this->send_homeassistant_service_response(call);
}
#ifdef USE_BLUETOOTH_PROXY
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override {
this->bluetooth_le_advertisement_subscription_ = true;
}
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override {
this->bluetooth_le_advertisement_subscription_ = false;
}
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg);
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
@@ -128,11 +124,17 @@ class APIConnection : public APIServerConnection {
void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override {
this->voice_assistant_subscription_ = msg.subscribe;
}
bool request_voice_assistant(bool start);
bool request_voice_assistant(bool start, const std::string &conversation_id);
void on_voice_assistant_response(const VoiceAssistantResponse &msg) override;
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
bool send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif
void on_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override {
// we initiated ping
@@ -211,9 +213,6 @@ class APIConnection : public APIServerConnection {
uint32_t last_traffic_;
bool sent_ping_{false};
bool service_call_subscription_{false};
#ifdef USE_BLUETOOTH_PROXY
bool bluetooth_le_advertisement_subscription_{false};
#endif
#ifdef USE_VOICE_ASSISTANT
bool voice_assistant_subscription_{false};
#endif

View File

@@ -433,6 +433,57 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::V
}
}
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
template<> const char *proto_enum_to_string<enums::AlarmControlPanelState>(enums::AlarmControlPanelState value) {
switch (value) {
case enums::ALARM_STATE_DISARMED:
return "ALARM_STATE_DISARMED";
case enums::ALARM_STATE_ARMED_HOME:
return "ALARM_STATE_ARMED_HOME";
case enums::ALARM_STATE_ARMED_AWAY:
return "ALARM_STATE_ARMED_AWAY";
case enums::ALARM_STATE_ARMED_NIGHT:
return "ALARM_STATE_ARMED_NIGHT";
case enums::ALARM_STATE_ARMED_VACATION:
return "ALARM_STATE_ARMED_VACATION";
case enums::ALARM_STATE_ARMED_CUSTOM_BYPASS:
return "ALARM_STATE_ARMED_CUSTOM_BYPASS";
case enums::ALARM_STATE_PENDING:
return "ALARM_STATE_PENDING";
case enums::ALARM_STATE_ARMING:
return "ALARM_STATE_ARMING";
case enums::ALARM_STATE_DISARMING:
return "ALARM_STATE_DISARMING";
case enums::ALARM_STATE_TRIGGERED:
return "ALARM_STATE_TRIGGERED";
default:
return "UNKNOWN";
}
}
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
template<>
const char *proto_enum_to_string<enums::AlarmControlPanelStateCommand>(enums::AlarmControlPanelStateCommand value) {
switch (value) {
case enums::ALARM_CONTROL_PANEL_DISARM:
return "ALARM_CONTROL_PANEL_DISARM";
case enums::ALARM_CONTROL_PANEL_ARM_AWAY:
return "ALARM_CONTROL_PANEL_ARM_AWAY";
case enums::ALARM_CONTROL_PANEL_ARM_HOME:
return "ALARM_CONTROL_PANEL_ARM_HOME";
case enums::ALARM_CONTROL_PANEL_ARM_NIGHT:
return "ALARM_CONTROL_PANEL_ARM_NIGHT";
case enums::ALARM_CONTROL_PANEL_ARM_VACATION:
return "ALARM_CONTROL_PANEL_ARM_VACATION";
case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS:
return "ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS";
case enums::ALARM_CONTROL_PANEL_TRIGGER:
return "ALARM_CONTROL_PANEL_TRIGGER";
default:
return "UNKNOWN";
}
}
#endif
bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
@@ -617,7 +668,11 @@ bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
return true;
}
case 11: {
this->bluetooth_proxy_version = value.as_uint32();
this->legacy_bluetooth_proxy_version = value.as_uint32();
return true;
}
case 15: {
this->bluetooth_proxy_feature_flags = value.as_uint32();
return true;
}
case 14: {
@@ -681,7 +736,8 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(8, this->project_name);
buffer.encode_string(9, this->project_version);
buffer.encode_uint32(10, this->webserver_port);
buffer.encode_uint32(11, this->bluetooth_proxy_version);
buffer.encode_uint32(11, this->legacy_bluetooth_proxy_version);
buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags);
buffer.encode_string(12, this->manufacturer);
buffer.encode_string(13, this->friendly_name);
buffer.encode_uint32(14, this->voice_assistant_version);
@@ -731,8 +787,13 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
out.append(buffer);
out.append("\n");
out.append(" bluetooth_proxy_version: ");
sprintf(buffer, "%u", this->bluetooth_proxy_version);
out.append(" legacy_bluetooth_proxy_version: ");
sprintf(buffer, "%u", this->legacy_bluetooth_proxy_version);
out.append(buffer);
out.append("\n");
out.append(" bluetooth_proxy_feature_flags: ");
sprintf(buffer, "%u", this->bluetooth_proxy_feature_flags);
out.append(buffer);
out.append("\n");
@@ -5041,10 +5102,28 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const {
out.append("}");
}
#endif
void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const {}
bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1: {
this->flags = value.as_uint32();
return true;
}
default:
return false;
}
}
void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(1, this->flags);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const {
out.append("SubscribeBluetoothLEAdvertisementsRequest {}");
__attribute__((unused)) char buffer[64];
out.append("SubscribeBluetoothLEAdvertisementsRequest {\n");
out.append(" flags: ");
sprintf(buffer, "%u", this->flags);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) {
@@ -5197,6 +5276,92 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const {
out.append("}");
}
#endif
bool BluetoothLERawAdvertisement::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1: {
this->address = value.as_uint64();
return true;
}
case 2: {
this->rssi = value.as_sint32();
return true;
}
case 3: {
this->address_type = value.as_uint32();
return true;
}
default:
return false;
}
}
bool BluetoothLERawAdvertisement::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 4: {
this->data = value.as_string();
return true;
}
default:
return false;
}
}
void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint64(1, this->address);
buffer.encode_sint32(2, this->rssi);
buffer.encode_uint32(3, this->address_type);
buffer.encode_string(4, this->data);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void BluetoothLERawAdvertisement::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("BluetoothLERawAdvertisement {\n");
out.append(" address: ");
sprintf(buffer, "%llu", this->address);
out.append(buffer);
out.append("\n");
out.append(" rssi: ");
sprintf(buffer, "%d", this->rssi);
out.append(buffer);
out.append("\n");
out.append(" address_type: ");
sprintf(buffer, "%u", this->address_type);
out.append(buffer);
out.append("\n");
out.append(" data: ");
out.append("'").append(this->data).append("'");
out.append("\n");
out.append("}");
}
#endif
bool BluetoothLERawAdvertisementsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->advertisements.push_back(value.as_message<BluetoothLERawAdvertisement>());
return true;
}
default:
return false;
}
}
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->advertisements) {
buffer.encode_message<BluetoothLERawAdvertisement>(1, it, true);
}
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("BluetoothLERawAdvertisementsResponse {\n");
for (const auto &it : this->advertisements) {
out.append(" advertisements: ");
it.dump_to(out);
out.append("\n");
}
out.append("}");
}
#endif
bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1: {
@@ -6187,7 +6352,20 @@ bool VoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
return false;
}
}
void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->start); }
bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2: {
this->conversation_id = value.as_string();
return true;
}
default:
return false;
}
}
void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(1, this->start);
buffer.encode_string(2, this->conversation_id);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantRequest::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
@@ -6195,6 +6373,10 @@ void VoiceAssistantRequest::dump_to(std::string &out) const {
out.append(" start: ");
out.append(YESNO(this->start));
out.append("\n");
out.append(" conversation_id: ");
out.append("'").append(this->conversation_id).append("'");
out.append("\n");
out.append("}");
}
#endif
@@ -6305,6 +6487,217 @@ void VoiceAssistantEventResponse::dump_to(std::string &out) const {
out.append("}");
}
#endif
bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 6: {
this->disabled_by_default = value.as_bool();
return true;
}
case 7: {
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 8: {
this->supported_features = value.as_uint32();
return true;
}
case 9: {
this->requires_code = value.as_bool();
return true;
}
case 10: {
this->requires_code_to_arm = value.as_bool();
return true;
}
default:
return false;
}
}
bool ListEntitiesAlarmControlPanelResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->object_id = value.as_string();
return true;
}
case 3: {
this->name = value.as_string();
return true;
}
case 4: {
this->unique_id = value.as_string();
return true;
}
case 5: {
this->icon = value.as_string();
return true;
}
default:
return false;
}
}
bool ListEntitiesAlarmControlPanelResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 2: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name);
buffer.encode_string(4, this->unique_id);
buffer.encode_string(5, this->icon);
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_uint32(8, this->supported_features);
buffer.encode_bool(9, this->requires_code);
buffer.encode_bool(10, this->requires_code_to_arm);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("ListEntitiesAlarmControlPanelResponse {\n");
out.append(" object_id: ");
out.append("'").append(this->object_id).append("'");
out.append("\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" name: ");
out.append("'").append(this->name).append("'");
out.append("\n");
out.append(" unique_id: ");
out.append("'").append(this->unique_id).append("'");
out.append("\n");
out.append(" icon: ");
out.append("'").append(this->icon).append("'");
out.append("\n");
out.append(" disabled_by_default: ");
out.append(YESNO(this->disabled_by_default));
out.append("\n");
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" supported_features: ");
sprintf(buffer, "%u", this->supported_features);
out.append(buffer);
out.append("\n");
out.append(" requires_code: ");
out.append(YESNO(this->requires_code));
out.append("\n");
out.append(" requires_code_to_arm: ");
out.append(YESNO(this->requires_code_to_arm));
out.append("\n");
out.append("}");
}
#endif
bool AlarmControlPanelStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->state = value.as_enum<enums::AlarmControlPanelState>();
return true;
}
default:
return false;
}
}
bool AlarmControlPanelStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_enum<enums::AlarmControlPanelState>(2, this->state);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void AlarmControlPanelStateResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("AlarmControlPanelStateResponse {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" state: ");
out.append(proto_enum_to_string<enums::AlarmControlPanelState>(this->state));
out.append("\n");
out.append("}");
}
#endif
bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->command = value.as_enum<enums::AlarmControlPanelStateCommand>();
return true;
}
default:
return false;
}
}
bool AlarmControlPanelCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 3: {
this->code = value.as_string();
return true;
}
default:
return false;
}
}
bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void AlarmControlPanelCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_enum<enums::AlarmControlPanelStateCommand>(2, this->command);
buffer.encode_string(3, this->code);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void AlarmControlPanelCommandRequest::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("AlarmControlPanelCommandRequest {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" command: ");
out.append(proto_enum_to_string<enums::AlarmControlPanelStateCommand>(this->command));
out.append("\n");
out.append(" code: ");
out.append("'").append(this->code).append("'");
out.append("\n");
out.append("}");
}
#endif
} // namespace api
} // namespace esphome

View File

@@ -176,6 +176,27 @@ enum VoiceAssistantEvent : uint32_t {
VOICE_ASSISTANT_TTS_START = 7,
VOICE_ASSISTANT_TTS_END = 8,
};
enum AlarmControlPanelState : uint32_t {
ALARM_STATE_DISARMED = 0,
ALARM_STATE_ARMED_HOME = 1,
ALARM_STATE_ARMED_AWAY = 2,
ALARM_STATE_ARMED_NIGHT = 3,
ALARM_STATE_ARMED_VACATION = 4,
ALARM_STATE_ARMED_CUSTOM_BYPASS = 5,
ALARM_STATE_PENDING = 6,
ALARM_STATE_ARMING = 7,
ALARM_STATE_DISARMING = 8,
ALARM_STATE_TRIGGERED = 9,
};
enum AlarmControlPanelStateCommand : uint32_t {
ALARM_CONTROL_PANEL_DISARM = 0,
ALARM_CONTROL_PANEL_ARM_AWAY = 1,
ALARM_CONTROL_PANEL_ARM_HOME = 2,
ALARM_CONTROL_PANEL_ARM_NIGHT = 3,
ALARM_CONTROL_PANEL_ARM_VACATION = 4,
ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5,
ALARM_CONTROL_PANEL_TRIGGER = 6,
};
} // namespace enums
@@ -287,7 +308,8 @@ class DeviceInfoResponse : public ProtoMessage {
std::string project_name{};
std::string project_version{};
uint32_t webserver_port{0};
uint32_t bluetooth_proxy_version{0};
uint32_t legacy_bluetooth_proxy_version{0};
uint32_t bluetooth_proxy_feature_flags{0};
std::string manufacturer{};
std::string friendly_name{};
uint32_t voice_assistant_version{0};
@@ -1247,12 +1269,14 @@ class MediaPlayerCommandRequest : public ProtoMessage {
};
class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage {
public:
uint32_t flags{0};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class BluetoothServiceData : public ProtoMessage {
public:
@@ -1286,6 +1310,32 @@ class BluetoothLEAdvertisementResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class BluetoothLERawAdvertisement : public ProtoMessage {
public:
uint64_t address{0};
int32_t rssi{0};
uint32_t address_type{0};
std::string data{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class BluetoothLERawAdvertisementsResponse : public ProtoMessage {
public:
std::vector<BluetoothLERawAdvertisement> advertisements{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class BluetoothDeviceRequest : public ProtoMessage {
public:
uint64_t address{0};
@@ -1604,12 +1654,14 @@ class SubscribeVoiceAssistantRequest : public ProtoMessage {
class VoiceAssistantRequest : public ProtoMessage {
public:
bool start{false};
std::string conversation_id{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class VoiceAssistantResponse : public ProtoMessage {
@@ -1649,6 +1701,56 @@ class VoiceAssistantEventResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesAlarmControlPanelResponse : public ProtoMessage {
public:
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
uint32_t supported_features{0};
bool requires_code{false};
bool requires_code_to_arm{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class AlarmControlPanelStateResponse : public ProtoMessage {
public:
uint32_t key{0};
enums::AlarmControlPanelState state{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class AlarmControlPanelCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
enums::AlarmControlPanelStateCommand command{};
std::string code{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
} // namespace api
} // namespace esphome

View File

@@ -339,6 +339,15 @@ bool APIServerConnectionBase::send_bluetooth_le_advertisement_response(const Blu
}
#endif
#ifdef USE_BLUETOOTH_PROXY
bool APIServerConnectionBase::send_bluetooth_le_raw_advertisements_response(
const BluetoothLERawAdvertisementsResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_bluetooth_le_raw_advertisements_response: %s", msg.dump().c_str());
#endif
return this->send_message_<BluetoothLERawAdvertisementsResponse>(msg, 93);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
#endif
#ifdef USE_BLUETOOTH_PROXY
bool APIServerConnectionBase::send_bluetooth_device_connection_response(const BluetoothDeviceConnectionResponse &msg) {
@@ -467,6 +476,25 @@ bool APIServerConnectionBase::send_voice_assistant_request(const VoiceAssistantR
#endif
#ifdef USE_VOICE_ASSISTANT
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response(
const ListEntitiesAlarmControlPanelResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_list_entities_alarm_control_panel_response: %s", msg.dump().c_str());
#endif
return this->send_message_<ListEntitiesAlarmControlPanelResponse>(msg, 94);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool APIServerConnectionBase::send_alarm_control_panel_state_response(const AlarmControlPanelStateResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_alarm_control_panel_state_response: %s", msg.dump().c_str());
#endif
return this->send_message_<AlarmControlPanelStateResponse>(msg, 95);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
#endif
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
switch (msg_type) {
case 1: {
@@ -874,6 +902,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_voice_assistant_event_response: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_event_response(msg);
#endif
break;
}
case 96: {
#ifdef USE_ALARM_CONTROL_PANEL
AlarmControlPanelCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_alarm_control_panel_command_request: %s", msg.dump().c_str());
#endif
this->on_alarm_control_panel_command_request(msg);
#endif
break;
}
@@ -1286,6 +1325,19 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo
this->subscribe_voice_assistant(msg);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->alarm_control_panel_command(msg);
}
#endif
} // namespace api
} // namespace esphome

View File

@@ -161,6 +161,9 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_BLUETOOTH_PROXY
bool send_bluetooth_le_advertisement_response(const BluetoothLEAdvertisementResponse &msg);
#endif
#ifdef USE_BLUETOOTH_PROXY
bool send_bluetooth_le_raw_advertisements_response(const BluetoothLERawAdvertisementsResponse &msg);
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_bluetooth_device_request(const BluetoothDeviceRequest &value){};
#endif
@@ -236,6 +239,15 @@ class APIServerConnectionBase : public ProtoService {
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){};
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg);
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state_response(const AlarmControlPanelStateResponse &msg);
#endif
#ifdef USE_ALARM_CONTROL_PANEL
virtual void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){};
#endif
protected:
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
@@ -321,6 +333,9 @@ class APIServerConnection : public APIServerConnectionBase {
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0;
#endif
protected:
void on_hello_request(const HelloRequest &msg) override;
@@ -402,6 +417,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_VOICE_ASSISTANT
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
#endif
};
} // namespace api

View File

@@ -291,112 +291,7 @@ void APIServer::send_homeassistant_service_call(const HomeassistantServiceRespon
client->send_homeassistant_service_call(call);
}
}
#ifdef USE_BLUETOOTH_PROXY
void APIServer::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call) {
for (auto &client : this->clients_) {
client->send_bluetooth_le_advertisement(call);
}
}
void APIServer::send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) {
BluetoothDeviceConnectionResponse call;
call.address = address;
call.connected = connected;
call.mtu = mtu;
call.error = error;
for (auto &client : this->clients_) {
client->send_bluetooth_device_connection_response(call);
}
}
void APIServer::send_bluetooth_device_pairing(uint64_t address, bool paired, esp_err_t error) {
BluetoothDevicePairingResponse call;
call.address = address;
call.paired = paired;
call.error = error;
for (auto &client : this->clients_) {
client->send_bluetooth_device_pairing_response(call);
}
}
void APIServer::send_bluetooth_device_unpairing(uint64_t address, bool success, esp_err_t error) {
BluetoothDeviceUnpairingResponse call;
call.address = address;
call.success = success;
call.error = error;
for (auto &client : this->clients_) {
client->send_bluetooth_device_unpairing_response(call);
}
}
void APIServer::send_bluetooth_device_clear_cache(uint64_t address, bool success, esp_err_t error) {
BluetoothDeviceClearCacheResponse call;
call.address = address;
call.success = success;
call.error = error;
for (auto &client : this->clients_) {
client->send_bluetooth_device_clear_cache_response(call);
}
}
void APIServer::send_bluetooth_connections_free(uint8_t free, uint8_t limit) {
BluetoothConnectionsFreeResponse call;
call.free = free;
call.limit = limit;
for (auto &client : this->clients_) {
client->send_bluetooth_connections_free_response(call);
}
}
void APIServer::send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call) {
for (auto &client : this->clients_) {
client->send_bluetooth_gatt_read_response(call);
}
}
void APIServer::send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call) {
for (auto &client : this->clients_) {
client->send_bluetooth_gatt_write_response(call);
}
}
void APIServer::send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call) {
for (auto &client : this->clients_) {
client->send_bluetooth_gatt_notify_data_response(call);
}
}
void APIServer::send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &call) {
for (auto &client : this->clients_) {
client->send_bluetooth_gatt_notify_response(call);
}
}
void APIServer::send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call) {
for (auto &client : this->clients_) {
client->send_bluetooth_gatt_get_services_response(call);
}
}
void APIServer::send_bluetooth_gatt_services_done(uint64_t address) {
BluetoothGATTGetServicesDoneResponse call;
call.address = address;
for (auto &client : this->clients_) {
client->send_bluetooth_gatt_get_services_done_response(call);
}
}
void APIServer::send_bluetooth_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) {
BluetoothGATTErrorResponse call;
call.address = address;
call.handle = handle;
call.error = error;
for (auto &client : this->clients_) {
client->send_bluetooth_gatt_error_response(call);
}
}
#endif
APIServer::APIServer() { global_api_server = this; }
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f) {
@@ -428,20 +323,29 @@ void APIServer::on_shutdown() {
}
#ifdef USE_VOICE_ASSISTANT
bool APIServer::start_voice_assistant() {
bool APIServer::start_voice_assistant(const std::string &conversation_id) {
for (auto &c : this->clients_) {
if (c->request_voice_assistant(true))
if (c->request_voice_assistant(true, conversation_id))
return true;
}
return false;
}
void APIServer::stop_voice_assistant() {
for (auto &c : this->clients_) {
if (c->request_voice_assistant(false))
if (c->request_voice_assistant(false, ""))
return;
}
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_alarm_control_panel_state(obj);
}
#endif
} // namespace api
} // namespace esphome

View File

@@ -75,31 +75,20 @@ class APIServer : public Component, public Controller {
void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
#ifdef USE_BLUETOOTH_PROXY
void send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call);
void send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK);
void send_bluetooth_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK);
void send_bluetooth_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK);
void send_bluetooth_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK);
void send_bluetooth_connections_free(uint8_t free, uint8_t limit);
void send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call);
void send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call);
void send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call);
void send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &call);
void send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call);
void send_bluetooth_gatt_services_done(uint64_t address);
void send_bluetooth_gatt_error(uint64_t address, uint16_t handle, esp_err_t error);
#endif
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
#ifdef USE_HOMEASSISTANT_TIME
void request_time();
#endif
#ifdef USE_VOICE_ASSISTANT
bool start_voice_assistant();
bool start_voice_assistant(const std::string &conversation_id);
void stop_voice_assistant();
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override;
#endif
bool is_connected() const;
struct HomeAssistantStateSubscription {

View File

@@ -69,6 +69,11 @@ bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_play
return this->client_->send_media_player_info(media_player);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
return this->client_->send_alarm_control_panel_info(a_alarm_control_panel);
}
#endif
} // namespace api
} // namespace esphome

View File

@@ -54,6 +54,9 @@ class ListEntitiesIterator : public ComponentIterator {
#endif
#ifdef USE_MEDIA_PLAYER
bool on_media_player(media_player::MediaPlayer *media_player) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override;
#endif
bool on_end() override;

View File

@@ -55,6 +55,11 @@ bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_play
return this->client_->send_media_player_state(media_player);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
return this->client_->send_alarm_control_panel_state(a_alarm_control_panel);
}
#endif
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
} // namespace api

View File

@@ -51,6 +51,9 @@ class InitialStateIterator : public ComponentIterator {
#endif
#ifdef USE_MEDIA_PLAYER
bool on_media_player(media_player::MediaPlayer *media_player) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override;
#endif
protected:
APIConnection *client_;

View File

@@ -442,7 +442,7 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) {
void BedJetHub::send_local_time() {
if (this->time_id_.has_value()) {
auto *time_id = *this->time_id_;
time::ESPTime now = time_id->now();
ESPTime now = time_id->now();
if (now.is_valid()) {
this->set_clock(now.hour, now.minute);
ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute);

View File

@@ -13,6 +13,7 @@
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#include "esphome/core/time.h"
#endif
#include <esp_gattc_api.h>

View File

@@ -7,6 +7,7 @@ from esphome.const import (
CONF_IBEACON_MAJOR,
CONF_IBEACON_MINOR,
CONF_IBEACON_UUID,
CONF_MIN_RSSI,
)
DEPENDENCIES = ["esp32_ble_tracker"]
@@ -37,6 +38,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t,
cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t,
cv.Optional(CONF_IBEACON_UUID): cv.uuid,
cv.Optional(CONF_MIN_RSSI): cv.All(
cv.decibel, cv.int_range(min=-90, max=-30)
),
}
)
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
@@ -51,6 +55,9 @@ async def to_code(config):
await cg.register_component(var, config)
await esp32_ble_tracker.register_ble_device(var, config)
if CONF_MIN_RSSI in config:
cg.add(var.set_minimum_rssi(config[CONF_MIN_RSSI]))
if CONF_MAC_ADDRESS in config:
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))

View File

@@ -41,12 +41,19 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
this->check_ibeacon_minor_ = true;
this->ibeacon_minor_ = minor;
}
void set_minimum_rssi(int rssi) {
this->check_minimum_rssi_ = true;
this->minimum_rssi_ = rssi;
}
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->check_minimum_rssi_ && this->minimum_rssi_ <= device.get_rssi()) {
return false;
}
switch (this->match_by_) {
case MATCH_BY_MAC_ADDRESS:
if (device.address_uint64() == this->address_) {
@@ -96,17 +103,21 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID };
MatchType match_by_;
bool found_{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_;
uint16_t ibeacon_major_{0};
uint16_t ibeacon_minor_{0};
int minimum_rssi_{0};
bool check_ibeacon_major_{false};
bool check_ibeacon_minor_{false};
bool check_minimum_rssi_{false};
bool found_{false};
};
} // namespace ble_presence

View File

@@ -102,8 +102,9 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi
esp32_ble_tracker::ESPBTUUID ibeacon_uuid_;
uint16_t ibeacon_major_;
bool check_ibeacon_major_;
uint16_t ibeacon_minor_;
bool check_ibeacon_major_;
bool check_ibeacon_minor_;
};

View File

@@ -1,6 +1,6 @@
#include "bluetooth_connection.h"
#include "esphome/components/api/api_server.h"
#include "esphome/components/api/api_pb2.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -20,24 +20,21 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
switch (event) {
case ESP_GATTC_DISCONNECT_EVT: {
api::global_api_server->send_bluetooth_device_connection(this->address_, false, 0, param->disconnect.reason);
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
this->set_address(0);
api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(),
this->proxy_->get_bluetooth_connections_limit());
this->proxy_->send_connections_free();
break;
}
case ESP_GATTC_OPEN_EVT: {
if (param->open.conn_id != this->conn_id_)
break;
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
api::global_api_server->send_bluetooth_device_connection(this->address_, false, 0, param->open.status);
this->proxy_->send_device_connection(this->address_, false, 0, param->open.status);
this->set_address(0);
api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(),
this->proxy_->get_bluetooth_connections_limit());
this->proxy_->send_connections_free();
} else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_);
api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(),
this->proxy_->get_bluetooth_connections_limit());
this->proxy_->send_device_connection(this->address_, true, this->mtu_);
this->proxy_->send_connections_free();
}
this->seen_mtu_or_services_ = false;
break;
@@ -52,9 +49,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
this->seen_mtu_or_services_ = true;
break;
}
api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_);
api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(),
this->proxy_->get_bluetooth_connections_limit());
this->proxy_->send_device_connection(this->address_, true, this->mtu_);
this->proxy_->send_connections_free();
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
@@ -67,9 +63,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
this->seen_mtu_or_services_ = true;
break;
}
api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_);
api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(),
this->proxy_->get_bluetooth_connections_limit());
this->proxy_->send_device_connection(this->address_, true, this->mtu_);
this->proxy_->send_connections_free();
break;
}
case ESP_GATTC_READ_DESCR_EVT:
@@ -79,7 +74,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_,
this->address_str_.c_str(), param->read.handle, param->read.status);
api::global_api_server->send_bluetooth_gatt_error(this->address_, param->read.handle, param->read.status);
this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status);
break;
}
api::BluetoothGATTReadResponse resp;
@@ -89,7 +84,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
for (uint16_t i = 0; i < param->read.value_len; i++) {
resp.data.push_back(param->read.value[i]);
}
api::global_api_server->send_bluetooth_gatt_read_response(resp);
this->proxy_->get_api_connection()->send_bluetooth_gatt_read_response(resp);
break;
}
case ESP_GATTC_WRITE_CHAR_EVT:
@@ -99,13 +94,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
if (param->write.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_,
this->address_str_.c_str(), param->write.handle, param->write.status);
api::global_api_server->send_bluetooth_gatt_error(this->address_, param->write.handle, param->write.status);
this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status);
break;
}
api::BluetoothGATTWriteResponse resp;
resp.address = this->address_;
resp.handle = param->write.handle;
api::global_api_server->send_bluetooth_gatt_write_response(resp);
this->proxy_->get_api_connection()->send_bluetooth_gatt_write_response(resp);
break;
}
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
@@ -113,28 +108,26 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d",
this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle,
param->unreg_for_notify.status);
api::global_api_server->send_bluetooth_gatt_error(this->address_, param->unreg_for_notify.handle,
param->unreg_for_notify.status);
this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status);
break;
}
api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_;
resp.handle = param->unreg_for_notify.handle;
api::global_api_server->send_bluetooth_gatt_notify_response(resp);
this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_response(resp);
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
if (param->reg_for_notify.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_,
this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status);
api::global_api_server->send_bluetooth_gatt_error(this->address_, param->reg_for_notify.handle,
param->reg_for_notify.status);
this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status);
break;
}
api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_;
resp.handle = param->reg_for_notify.handle;
api::global_api_server->send_bluetooth_gatt_notify_response(resp);
this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_response(resp);
break;
}
case ESP_GATTC_NOTIFY_EVT: {
@@ -149,7 +142,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
for (uint16_t i = 0; i < param->notify.value_len; i++) {
resp.data.push_back(param->notify.value[i]);
}
api::global_api_server->send_bluetooth_gatt_notify_data_response(resp);
this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_data_response(resp);
break;
}
default:
@@ -166,10 +159,9 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl
if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0)
break;
if (param->ble_security.auth_cmpl.success) {
api::global_api_server->send_bluetooth_device_pairing(this->address_, true);
this->proxy_->send_device_pairing(this->address_, true);
} else {
api::global_api_server->send_bluetooth_device_pairing(this->address_, false,
param->ble_security.auth_cmpl.fail_reason);
this->proxy_->send_device_pairing(this->address_, false, param->ble_security.auth_cmpl.fail_reason);
}
break;
default:

View File

@@ -1,11 +1,10 @@
#include "bluetooth_proxy.h"
#include "esphome/core/log.h"
#include "esphome/core/macros.h"
#ifdef USE_ESP32
#include "esphome/components/api/api_server.h"
namespace esphome {
namespace bluetooth_proxy {
@@ -27,15 +26,39 @@ std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) {
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
if (!api::global_api_server->is_connected())
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_)
return false;
ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(),
device.get_rssi());
this->send_api_packet_(device);
return true;
}
bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)
return false;
api::BluetoothLERawAdvertisementsResponse resp;
for (size_t i = 0; i < count; i++) {
auto &result = advertisements[i];
api::BluetoothLERawAdvertisement adv;
adv.address = esp32_ble::ble_addr_to_uint64(result.bda);
adv.rssi = result.rssi;
adv.address_type = result.ble_addr_type;
uint8_t length = result.adv_data_len + result.scan_rsp_len;
adv.data.reserve(length);
for (uint16_t i = 0; i < length; i++) {
adv.data.push_back(result.ble_adv[i]);
}
resp.advertisements.push_back(std::move(adv));
}
ESP_LOGV(TAG, "Proxying %d packets", count);
this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp);
return true;
}
void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) {
api::BluetoothLEAdvertisementResponse resp;
resp.address = device.address_uint64();
@@ -58,7 +81,7 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi
manufacturer_data.data.assign(data.data.begin(), data.data.end());
resp.manufacturer_data.push_back(std::move(manufacturer_data));
}
api::global_api_server->send_bluetooth_le_advertisement(resp);
this->api_connection_->send_bluetooth_le_advertisement(resp);
}
void BluetoothProxy::dump_config() {
@@ -81,7 +104,7 @@ int BluetoothProxy::get_bluetooth_connections_free() {
}
void BluetoothProxy::loop() {
if (!api::global_api_server->is_connected()) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
for (auto *connection : this->connections_) {
if (connection->get_address() != 0) {
connection->disconnect();
@@ -92,7 +115,7 @@ void BluetoothProxy::loop() {
for (auto *connection : this->connections_) {
if (connection->send_service_ == connection->service_count_) {
connection->send_service_ = DONE_SENDING_SERVICES;
api::global_api_server->send_bluetooth_gatt_services_done(connection->get_address());
this->send_gatt_services_done(connection->get_address());
if (connection->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
connection->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
connection->release_services();
@@ -170,7 +193,7 @@ void BluetoothProxy::loop() {
service_resp.characteristics.push_back(std::move(characteristic_resp));
}
resp.services.push_back(std::move(service_resp));
api::global_api_server->send_bluetooth_gatt_services(resp);
this->api_connection_->send_bluetooth_gatt_get_services_response(resp);
}
}
}
@@ -208,16 +231,15 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
auto *connection = this->get_connection_(msg.address, true);
if (connection == nullptr) {
ESP_LOGW(TAG, "No free connections available");
api::global_api_server->send_bluetooth_device_connection(msg.address, false);
this->send_device_connection(msg.address, false);
return;
}
if (connection->state() == espbt::ClientState::CONNECTED ||
connection->state() == espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%d] [%s] Connection already established", connection->get_connection_index(),
connection->address_str().c_str());
api::global_api_server->send_bluetooth_device_connection(msg.address, true);
api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(),
this->get_bluetooth_connections_limit());
this->send_device_connection(msg.address, true);
this->send_connections_free();
return;
} else if (connection->state() == espbt::ClientState::SEARCHING) {
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already searching for device",
@@ -263,25 +285,22 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
} else {
connection->set_state(espbt::ClientState::SEARCHING);
}
api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(),
this->get_bluetooth_connections_limit());
this->send_connections_free();
break;
}
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: {
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
api::global_api_server->send_bluetooth_device_connection(msg.address, false);
api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(),
this->get_bluetooth_connections_limit());
this->send_device_connection(msg.address, false);
this->send_connections_free();
return;
}
if (connection->state() != espbt::ClientState::IDLE) {
connection->disconnect();
} else {
connection->set_address(0);
api::global_api_server->send_bluetooth_device_connection(msg.address, false);
api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(),
this->get_bluetooth_connections_limit());
this->send_device_connection(msg.address, false);
this->send_connections_free();
}
break;
}
@@ -291,10 +310,10 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
if (!connection->is_paired()) {
auto err = connection->pair();
if (err != ESP_OK) {
api::global_api_server->send_bluetooth_device_pairing(msg.address, false, err);
this->send_device_pairing(msg.address, false, err);
}
} else {
api::global_api_server->send_bluetooth_device_pairing(msg.address, true);
this->send_device_pairing(msg.address, true);
}
}
break;
@@ -303,14 +322,20 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
esp_bd_addr_t address;
uint64_to_bd_addr(msg.address, address);
esp_err_t ret = esp_ble_remove_bond_device(address);
api::global_api_server->send_bluetooth_device_unpairing(msg.address, ret == ESP_OK, ret);
this->send_device_pairing(msg.address, ret == ESP_OK, ret);
break;
}
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: {
esp_bd_addr_t address;
uint64_to_bd_addr(msg.address, address);
esp_err_t ret = esp_ble_gattc_cache_clean(address);
api::global_api_server->send_bluetooth_device_clear_cache(msg.address, ret == ESP_OK, ret);
api::BluetoothDeviceClearCacheResponse call;
call.address = msg.address;
call.success = ret == ESP_OK;
call.error = ret;
this->api_connection_->send_bluetooth_device_clear_cache_response(call);
break;
}
}
@@ -320,13 +345,13 @@ void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &ms
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
auto err = connection->read_characteristic(msg.handle);
if (err != ESP_OK) {
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err);
this->send_gatt_error(msg.address, msg.handle, err);
}
}
@@ -334,13 +359,13 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
auto err = connection->write_characteristic(msg.handle, msg.data, msg.response);
if (err != ESP_OK) {
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err);
this->send_gatt_error(msg.address, msg.handle, err);
}
}
@@ -348,13 +373,13 @@ void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTRead
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
auto err = connection->read_descriptor(msg.handle);
if (err != ESP_OK) {
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err);
this->send_gatt_error(msg.address, msg.handle, err);
}
}
@@ -362,13 +387,13 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
auto err = connection->write_descriptor(msg.handle, msg.data, true);
if (err != ESP_OK) {
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err);
this->send_gatt_error(msg.address, msg.handle, err);
}
}
@@ -376,12 +401,12 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr || !connection->connected()) {
ESP_LOGW(TAG, "Cannot get GATT services, not connected");
api::global_api_server->send_bluetooth_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED);
this->send_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED);
return;
}
if (!connection->service_count_) {
ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str());
api::global_api_server->send_bluetooth_gatt_services_done(msg.address);
this->send_gatt_services_done(msg.address);
return;
}
if (connection->send_service_ ==
@@ -393,16 +418,89 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected");
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
auto err = connection->notify_characteristic(msg.handle, msg.enable);
if (err != ESP_OK) {
api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err);
this->send_gatt_error(msg.address, msg.handle, err);
}
}
void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) {
if (this->api_connection_ != nullptr) {
ESP_LOGE(TAG, "Only one API subscription is allowed at a time");
return;
}
this->api_connection_ = api_connection;
this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS;
}
void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) {
if (this->api_connection_ != api_connection) {
ESP_LOGV(TAG, "API connection is not subscribed");
return;
}
this->api_connection_ = nullptr;
this->raw_advertisements_ = false;
}
void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) {
if (this->api_connection_ == nullptr)
return;
api::BluetoothDeviceConnectionResponse call;
call.address = address;
call.connected = connected;
call.mtu = mtu;
call.error = error;
this->api_connection_->send_bluetooth_device_connection_response(call);
}
void BluetoothProxy::send_connections_free() {
if (this->api_connection_ == nullptr)
return;
api::BluetoothConnectionsFreeResponse call;
call.free = this->get_bluetooth_connections_free();
call.limit = this->get_bluetooth_connections_limit();
this->api_connection_->send_bluetooth_connections_free_response(call);
}
void BluetoothProxy::send_gatt_services_done(uint64_t address) {
if (this->api_connection_ == nullptr)
return;
api::BluetoothGATTGetServicesDoneResponse call;
call.address = address;
this->api_connection_->send_bluetooth_gatt_get_services_done_response(call);
}
void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) {
if (this->api_connection_ == nullptr)
return;
api::BluetoothGATTErrorResponse call;
call.address = address;
call.handle = handle;
call.error = error;
this->api_connection_->send_bluetooth_gatt_error_response(call);
}
void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) {
api::BluetoothDevicePairingResponse call;
call.address = address;
call.paired = paired;
call.error = error;
this->api_connection_->send_bluetooth_device_pairing_response(call);
}
void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) {
api::BluetoothDeviceUnpairingResponse call;
call.address = address;
call.success = success;
call.error = error;
this->api_connection_->send_bluetooth_device_unpairing_response(call);
}
BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace bluetooth_proxy

View File

@@ -5,6 +5,7 @@
#include <map>
#include <vector>
#include "esphome/components/api/api_connection.h"
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/esp32_ble_client/ble_client_base.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
@@ -21,10 +22,33 @@ static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
using namespace esp32_ble_client;
// Legacy versions:
// Version 1: Initial version without active connections
// Version 2: Support for active connections
// Version 3: New connection API
// Version 4: Pairing support
// Version 5: Cache clear support
static const uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5;
static const uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1;
enum BluetoothProxyFeature : uint32_t {
FEATURE_PASSIVE_SCAN = 1 << 0,
FEATURE_ACTIVE_CONNECTIONS = 1 << 1,
FEATURE_REMOTE_CACHING = 1 << 2,
FEATURE_PAIRING = 1 << 3,
FEATURE_CACHE_CLEARING = 1 << 4,
FEATURE_RAW_ADVERTISEMENTS = 1 << 5,
};
enum BluetoothProxySubscriptionFlag : uint32_t {
SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0,
};
class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
public:
BluetoothProxy();
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override;
void dump_config() override;
void loop() override;
@@ -44,6 +68,18 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
int get_bluetooth_connections_free();
int get_bluetooth_connections_limit() { return this->connections_.size(); }
void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags);
void unsubscribe_api_connection(api::APIConnection *api_connection);
api::APIConnection *get_api_connection() { return this->api_connection_; }
void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK);
void send_connections_free();
void send_gatt_services_done(uint64_t address);
void send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error);
void send_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK);
void send_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK);
void send_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK);
static void uint64_to_bd_addr(uint64_t address, esp_bd_addr_t bd_addr) {
bd_addr[0] = (address >> 40) & 0xff;
bd_addr[1] = (address >> 32) & 0xff;
@@ -56,6 +92,27 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
void set_active(bool active) { this->active_ = active; }
bool has_active() { return this->active_; }
uint32_t get_legacy_version() const {
if (this->active_) {
return LEGACY_ACTIVE_CONNECTIONS_VERSION;
}
return LEGACY_PASSIVE_ONLY_VERSION;
}
uint32_t get_feature_flags() const {
uint32_t flags = 0;
flags |= BluetoothProxyFeature::FEATURE_PASSIVE_SCAN;
flags |= BluetoothProxyFeature::FEATURE_RAW_ADVERTISEMENTS;
if (this->active_) {
flags |= BluetoothProxyFeature::FEATURE_ACTIVE_CONNECTIONS;
flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING;
flags |= BluetoothProxyFeature::FEATURE_PAIRING;
flags |= BluetoothProxyFeature::FEATURE_CACHE_CLEARING;
}
return flags;
}
protected:
void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device);
@@ -64,18 +121,12 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
bool active_;
std::vector<BluetoothConnection *> connections_{};
api::APIConnection *api_connection_{nullptr};
bool raw_advertisements_{false};
};
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Version 1: Initial version without active connections
// Version 2: Support for active connections
// Version 3: New connection API
// Version 4: Pairing support
// Version 5: Cache clear support
static const uint32_t ACTIVE_CONNECTIONS_VERSION = 5;
static const uint32_t PASSIVE_ONLY_VERSION = 1;
} // namespace bluetooth_proxy
} // namespace esphome

View File

@@ -94,3 +94,5 @@ async def to_code(config):
sens = await sensor.new_sensor(conf)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING]))
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))

View File

@@ -6,102 +6,100 @@ namespace esphome {
namespace captive_portal {
const uint8_t INDEX_GZ[] PROGMEM = {
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xdd, 0x58, 0x09, 0x6f, 0xdc, 0x36, 0x16, 0xfe, 0x2b,
0xac, 0x92, 0x74, 0x34, 0x8d, 0xc5, 0xd1, 0x31, 0x97, 0x35, 0xd2, 0x14, 0x89, 0x37, 0x45, 0x0b, 0x24, 0x69, 0x00,
0xbb, 0x5d, 0x14, 0x69, 0x00, 0x73, 0x24, 0x6a, 0xc4, 0x58, 0xa2, 0x54, 0x91, 0x9a, 0x23, 0x83, 0xd9, 0xdf, 0xde,
0x47, 0x52, 0x73, 0x38, 0x6b, 0x2f, 0x90, 0x62, 0x8b, 0xa2, 0x4d, 0x6c, 0x9a, 0xc7, 0x3b, 0x3f, 0xf2, 0xf1, 0x3d,
0x2a, 0xfa, 0x2a, 0xad, 0x12, 0xb9, 0xad, 0x29, 0xca, 0x65, 0x59, 0xcc, 0x23, 0xd5, 0xa2, 0x82, 0xf0, 0x65, 0x4c,
0x39, 0x8c, 0x28, 0x49, 0xe7, 0x51, 0x49, 0x25, 0x41, 0x49, 0x4e, 0x1a, 0x41, 0x65, 0xfc, 0xd3, 0xcd, 0x77, 0xce,
0x14, 0x0d, 0xe6, 0x51, 0xc1, 0xf8, 0x1d, 0x6a, 0x68, 0x11, 0xb3, 0xa4, 0xe2, 0x28, 0x6f, 0x68, 0x16, 0xa7, 0x44,
0x92, 0x90, 0x95, 0x64, 0x49, 0x15, 0x81, 0x66, 0xe3, 0xa4, 0xa4, 0xf1, 0x8a, 0xd1, 0x75, 0x5d, 0x35, 0x12, 0x01,
0xa5, 0xa4, 0x5c, 0xc6, 0xd6, 0x9a, 0xa5, 0x32, 0x8f, 0x53, 0xba, 0x62, 0x09, 0x75, 0xf4, 0xe0, 0x82, 0x71, 0x26,
0x19, 0x29, 0x1c, 0x91, 0x90, 0x82, 0xc6, 0xde, 0x45, 0x2b, 0x68, 0xa3, 0x07, 0x64, 0x01, 0x63, 0x5e, 0x59, 0x20,
0x52, 0x24, 0x0d, 0xab, 0x25, 0x52, 0xf6, 0xc6, 0x65, 0x95, 0xb6, 0x05, 0x9d, 0x67, 0x2d, 0x4f, 0x24, 0x03, 0x0b,
0x84, 0xcd, 0xfb, 0xbb, 0x82, 0x4a, 0x44, 0xe3, 0x37, 0x44, 0xe6, 0xb8, 0x24, 0x1b, 0xdb, 0x74, 0x18, 0xb7, 0xfd,
0x6f, 0x6c, 0xfe, 0xdc, 0x73, 0xdd, 0xfe, 0x85, 0x6e, 0xdc, 0xfe, 0x00, 0xfe, 0xce, 0x1a, 0x2a, 0xdb, 0x86, 0x23,
0x62, 0xdf, 0x46, 0x35, 0x50, 0xa2, 0x34, 0xb6, 0x4a, 0xcf, 0xc7, 0xae, 0x3b, 0x45, 0xde, 0x25, 0xf6, 0x47, 0x8e,
0xe7, 0xe1, 0xc0, 0xf1, 0x46, 0xc9, 0xc4, 0x19, 0x21, 0x6f, 0x08, 0x8d, 0xef, 0xe3, 0x11, 0x72, 0x3f, 0x59, 0x28,
0x63, 0x45, 0x11, 0x5b, 0xbc, 0xe2, 0xd4, 0x42, 0x42, 0x36, 0xd5, 0x1d, 0x8d, 0xad, 0xa4, 0x6d, 0x1a, 0xf0, 0xee,
0xaa, 0x2a, 0xaa, 0x06, 0xac, 0xfd, 0x95, 0xa3, 0x7b, 0xff, 0xbe, 0x58, 0x87, 0x6c, 0x08, 0x17, 0x59, 0xd5, 0x94,
0xb1, 0xa5, 0x41, 0xb1, 0x9f, 0xee, 0xe8, 0x1e, 0xa9, 0xa6, 0x7f, 0xb6, 0xe8, 0x54, 0x0d, 0x5b, 0x32, 0x1e, 0x5b,
0x9e, 0x8f, 0xbc, 0x29, 0xe8, 0xbd, 0xed, 0xef, 0x8f, 0xa0, 0x10, 0x05, 0x4a, 0xe7, 0x66, 0x65, 0xbf, 0xbf, 0x8d,
0xc4, 0x6a, 0x89, 0x36, 0x65, 0xc1, 0x45, 0x6c, 0xe5, 0x52, 0xd6, 0xe1, 0x60, 0xb0, 0x5e, 0xaf, 0xf1, 0x3a, 0xc0,
0x55, 0xb3, 0x1c, 0xf8, 0xae, 0xeb, 0x0e, 0x80, 0xc2, 0x42, 0x66, 0x7f, 0x2c, 0x7f, 0x68, 0xa1, 0x9c, 0xb2, 0x65,
0x2e, 0x75, 0x7f, 0xfe, 0x74, 0xc7, 0xf7, 0x91, 0xa2, 0x98, 0xdf, 0x7e, 0x38, 0xd3, 0xd2, 0x9c, 0x69, 0xe1, 0xdf,
0x12, 0xdb, 0x3a, 0xb8, 0xda, 0x7b, 0xa3, 0x8c, 0x9a, 0x10, 0x1f, 0xf9, 0xc8, 0xd5, 0xff, 0x7d, 0x47, 0xf5, 0xbb,
0x91, 0xf3, 0xd9, 0x08, 0x9d, 0x8d, 0xe0, 0xaf, 0x02, 0xd0, 0x2f, 0xc7, 0xce, 0xe5, 0x91, 0xdf, 0x53, 0xeb, 0x2b,
0xcf, 0x3d, 0x4d, 0x28, 0xa6, 0xef, 0xc7, 0xe7, 0x63, 0xc7, 0xff, 0x59, 0x11, 0x68, 0xf4, 0x8f, 0x5c, 0x8e, 0x9f,
0x7b, 0x3f, 0x8f, 0xc9, 0x08, 0x8d, 0xba, 0x99, 0x91, 0xa3, 0xfa, 0xc7, 0x91, 0xd6, 0x85, 0x46, 0x2b, 0x20, 0x2b,
0x9d, 0xb1, 0x33, 0x22, 0x01, 0x0a, 0x3a, 0xab, 0xa0, 0x07, 0xd3, 0x63, 0xe0, 0x3e, 0x9b, 0x73, 0x82, 0x4f, 0xbd,
0xc1, 0xdc, 0xea, 0x87, 0x96, 0x75, 0x82, 0xa1, 0x3a, 0x87, 0x01, 0x7f, 0xac, 0xe0, 0xdc, 0x59, 0x56, 0x7f, 0x6f,
0x7d, 0x2b, 0xc8, 0x8a, 0x5a, 0x71, 0x1c, 0x43, 0xa8, 0xb5, 0x25, 0x9c, 0x10, 0x5c, 0x54, 0x09, 0x51, 0x2c, 0x58,
0x50, 0xd2, 0x24, 0xf9, 0xd7, 0x5f, 0xdb, 0xc7, 0xa5, 0x25, 0x95, 0xaf, 0x0a, 0xaa, 0xba, 0xe2, 0xe5, 0xf6, 0x86,
0x2c, 0xdf, 0x42, 0x00, 0xd9, 0x16, 0x11, 0x2c, 0xa5, 0x56, 0xff, 0xbd, 0xfb, 0x01, 0x0b, 0xb9, 0x2d, 0x28, 0x4e,
0x99, 0xa8, 0x0b, 0xb2, 0x8d, 0xad, 0x05, 0xc8, 0xba, 0xb3, 0xfa, 0x17, 0x19, 0x95, 0x49, 0x6e, 0x5b, 0x03, 0x08,
0xb1, 0x8c, 0x2d, 0xf1, 0x47, 0x51, 0x71, 0xab, 0x8f, 0x65, 0x4e, 0xb9, 0x6d, 0x1f, 0x2c, 0x54, 0xf6, 0x71, 0xbd,
0x64, 0x3f, 0xb4, 0x74, 0xb4, 0x41, 0x32, 0xa9, 0x42, 0x0e, 0xab, 0xe0, 0xbd, 0x38, 0xce, 0x2e, 0xaa, 0x74, 0xfb,
0x88, 0x79, 0xb9, 0x67, 0x6c, 0x63, 0x9c, 0xd3, 0xe6, 0x86, 0x6e, 0xe0, 0xb8, 0xfc, 0x9b, 0x7d, 0xc7, 0xd0, 0x5b,
0x2a, 0xd7, 0x55, 0x73, 0x27, 0x42, 0x64, 0x3d, 0x37, 0xe2, 0x66, 0x26, 0x42, 0x39, 0x26, 0xb5, 0xc0, 0xa2, 0x80,
0xf0, 0xb7, 0xbd, 0x3e, 0xc4, 0x6a, 0x7d, 0xdf, 0x14, 0x83, 0xe2, 0x6d, 0x94, 0xb2, 0x15, 0x4a, 0x0a, 0x22, 0xe0,
0xb8, 0x72, 0x23, 0xcb, 0x42, 0x87, 0xb8, 0xaa, 0x78, 0x02, 0xfc, 0x77, 0xb1, 0xf5, 0x00, 0x76, 0x2f, 0xb7, 0x3f,
0xa4, 0x76, 0x4f, 0x00, 0x6a, 0xbd, 0x3e, 0x5e, 0x91, 0xa2, 0xa5, 0x28, 0x46, 0x32, 0x67, 0xe2, 0x64, 0xe2, 0xec,
0x51, 0xb6, 0x5a, 0xdc, 0x01, 0x57, 0x06, 0xcb, 0xc2, 0xee, 0x5b, 0xc7, 0x38, 0x8e, 0x88, 0xb9, 0xe5, 0xac, 0x27,
0xd6, 0x67, 0x36, 0x39, 0x05, 0xcd, 0xa4, 0x75, 0x16, 0xf0, 0x4f, 0x77, 0x70, 0x1b, 0xe1, 0x06, 0xf4, 0xf7, 0xf7,
0xa7, 0xd9, 0x48, 0xd4, 0x84, 0x7f, 0xce, 0xaa, 0x6c, 0xd4, 0x81, 0x85, 0x55, 0x4f, 0x45, 0x17, 0x10, 0x9d, 0x74,
0x0e, 0xc8, 0xb1, 0xff, 0x74, 0x07, 0x71, 0xa6, 0x8e, 0xce, 0xdd, 0x49, 0x68, 0x34, 0x00, 0x84, 0xe6, 0xb7, 0xfb,
0x7e, 0xff, 0xe4, 0xce, 0x6f, 0x2d, 0x6d, 0xb6, 0xd7, 0xb4, 0xa0, 0x89, 0xac, 0x1a, 0xdb, 0x7a, 0x02, 0x9a, 0xe0,
0x24, 0x68, 0xbf, 0xbf, 0xbf, 0x79, 0xf3, 0x3a, 0xae, 0x6c, 0xda, 0xbf, 0x78, 0x8c, 0x5a, 0xdd, 0xea, 0xef, 0xe1,
0x56, 0xff, 0x4f, 0xdc, 0x53, 0xf7, 0x7a, 0xef, 0x03, 0xb0, 0x1a, 0xaf, 0x4f, 0x97, 0xbb, 0xba, 0x00, 0x9e, 0xc3,
0x25, 0x72, 0x61, 0x3d, 0x17, 0xb6, 0x33, 0x1e, 0xf5, 0x41, 0x3d, 0xfc, 0x80, 0xe9, 0xfa, 0x7a, 0x86, 0x6b, 0x5a,
0x1d, 0xd1, 0xf9, 0x37, 0xbb, 0x45, 0xb5, 0x71, 0x04, 0xfb, 0xc4, 0xf8, 0x32, 0x64, 0x3c, 0xa7, 0x0d, 0x93, 0x7b,
0x30, 0x17, 0x6e, 0xfa, 0xba, 0x95, 0xbb, 0x9a, 0xa4, 0xa9, 0x5a, 0x19, 0xd5, 0x9b, 0x59, 0x06, 0x79, 0x41, 0x51,
0xd2, 0xd0, 0xa3, 0xe5, 0xde, 0xac, 0xeb, 0x2b, 0x28, 0xbc, 0x1c, 0x3d, 0xdb, 0xab, 0x83, 0xb7, 0x93, 0xb0, 0x65,
0x0e, 0x29, 0xd8, 0x92, 0x87, 0x09, 0xd8, 0x4d, 0x1b, 0xc3, 0x94, 0x91, 0x92, 0x15, 0xdb, 0x50, 0xc0, 0x65, 0xe8,
0x40, 0xc2, 0x60, 0xd9, 0x7e, 0xd1, 0x4a, 0x59, 0x71, 0xd0, 0xdd, 0xa4, 0xb4, 0x09, 0xdd, 0x99, 0xe9, 0x38, 0x0d,
0x49, 0x59, 0x2b, 0x42, 0x1c, 0x34, 0xb4, 0x9c, 0x2d, 0x48, 0x72, 0xb7, 0x6c, 0xaa, 0x96, 0xa7, 0x4e, 0xa2, 0x6e,
0xeb, 0xf0, 0x89, 0x97, 0x91, 0x80, 0x26, 0xb3, 0x6e, 0x94, 0x65, 0xd9, 0x0c, 0x90, 0xa0, 0x8e, 0xb9, 0xfc, 0x42,
0x1f, 0x0f, 0x15, 0xdb, 0x99, 0x99, 0xd8, 0x57, 0x13, 0xc6, 0x46, 0x48, 0x25, 0xcf, 0x66, 0x07, 0x77, 0xdc, 0x19,
0xa4, 0x01, 0x01, 0x42, 0x6a, 0x88, 0x7f, 0x30, 0x73, 0x5f, 0x12, 0xc6, 0xcf, 0xad, 0x57, 0x67, 0x65, 0xd6, 0x85,
0x2f, 0xc0, 0xa2, 0xd5, 0xe8, 0x20, 0x9e, 0x41, 0xa2, 0x32, 0xb9, 0x30, 0xf4, 0xc7, 0x6e, 0xbd, 0xd9, 0xe3, 0xee,
0x8c, 0xec, 0x0e, 0xd4, 0x59, 0x41, 0x37, 0xb3, 0x8f, 0xad, 0x90, 0x2c, 0xdb, 0x3a, 0x5d, 0x2e, 0x0d, 0xe1, 0xbc,
0x40, 0x0e, 0x5d, 0x00, 0x29, 0xa5, 0x7c, 0xa6, 0x75, 0x38, 0x4c, 0xd2, 0x52, 0x74, 0x38, 0x1d, 0xc5, 0xe8, 0x53,
0x7a, 0x5f, 0xd6, 0xff, 0xa2, 0x56, 0xc7, 0x71, 0x57, 0x92, 0x06, 0x72, 0x8b, 0xb3, 0xa8, 0x00, 0xd3, 0x32, 0x74,
0x26, 0xb0, 0x57, 0xdd, 0x94, 0x12, 0x06, 0x9e, 0x83, 0x99, 0xfa, 0x6e, 0x3a, 0xe0, 0xed, 0xd5, 0x1b, 0x24, 0xaa,
0x82, 0xa5, 0x1d, 0x9d, 0x26, 0x41, 0xee, 0x11, 0x1e, 0x0f, 0xb6, 0x1b, 0xa9, 0xb9, 0x03, 0xd4, 0xc3, 0x6c, 0x4a,
0x3c, 0xf7, 0x81, 0x1d, 0x49, 0xb3, 0xcc, 0x5f, 0x64, 0x47, 0xa4, 0x54, 0xaa, 0xdd, 0xb3, 0xee, 0x54, 0xf8, 0x43,
0x10, 0x70, 0xd8, 0x1b, 0xe8, 0xef, 0x99, 0x8e, 0x8b, 0xdd, 0x99, 0x14, 0x7d, 0x52, 0xc3, 0xb6, 0x29, 0xec, 0x87,
0x4e, 0xee, 0xb3, 0xe0, 0xea, 0x94, 0x09, 0x7b, 0x8f, 0x67, 0xc2, 0x1e, 0x52, 0xb5, 0xcb, 0xcb, 0x6a, 0x13, 0xf7,
0x74, 0x4e, 0x1a, 0xc2, 0x4f, 0xef, 0x59, 0xf0, 0x0a, 0xf8, 0xff, 0x2f, 0x29, 0xee, 0x0f, 0xa7, 0xb7, 0x2f, 0x48,
0x6d, 0x5f, 0x98, 0xd5, 0x8c, 0x77, 0xca, 0x79, 0xe8, 0x41, 0xfa, 0x62, 0x58, 0xb0, 0xa5, 0xf7, 0x67, 0x40, 0xfb,
0xdf, 0x38, 0x06, 0x2f, 0xbc, 0x29, 0xbe, 0x44, 0xba, 0x31, 0x10, 0xe1, 0x60, 0x8a, 0x26, 0x57, 0x43, 0x3c, 0xf4,
0x90, 0xaa, 0x9a, 0xc6, 0x68, 0x82, 0xa7, 0x40, 0x30, 0xc6, 0xc1, 0x04, 0x26, 0x90, 0xef, 0xe1, 0xd1, 0x6b, 0x3f,
0xc0, 0xe3, 0x11, 0x50, 0xf9, 0x2e, 0x0e, 0x7c, 0x64, 0x68, 0xc7, 0xd8, 0x07, 0x71, 0x8a, 0x24, 0x28, 0x01, 0xe8,
0x24, 0xc0, 0xee, 0x04, 0xc4, 0x8d, 0xb1, 0x7b, 0x89, 0xa7, 0x63, 0x34, 0xc5, 0x13, 0x80, 0x0e, 0x0f, 0x47, 0x85,
0x33, 0xc2, 0x1e, 0x4c, 0x07, 0x63, 0x32, 0xc5, 0xc3, 0x00, 0xe9, 0xc6, 0xc0, 0x31, 0x01, 0x11, 0x0e, 0x76, 0xbd,
0xd7, 0x01, 0xf6, 0x27, 0xa0, 0x77, 0x38, 0x7c, 0x01, 0x62, 0x2f, 0x87, 0xc8, 0xb4, 0x06, 0x5e, 0x50, 0x30, 0x7a,
0x0c, 0x34, 0xff, 0x9f, 0x0b, 0x1a, 0x40, 0xe2, 0xa1, 0x00, 0x5f, 0x42, 0xec, 0x7a, 0x8a, 0xdf, 0xb4, 0x06, 0x37,
0xcf, 0x43, 0xee, 0x1f, 0xc6, 0x2c, 0xf8, 0xe7, 0x62, 0xe6, 0x29, 0x04, 0xa0, 0x0b, 0xba, 0x41, 0x0e, 0xd2, 0x8d,
0xd1, 0x0d, 0xcc, 0xd3, 0xab, 0x4b, 0x34, 0x05, 0xae, 0xf1, 0x14, 0x5d, 0xa2, 0x91, 0x42, 0x17, 0xd8, 0x87, 0x86,
0xc9, 0x01, 0xa6, 0x2f, 0x84, 0x71, 0xf8, 0x37, 0x86, 0xf1, 0x31, 0x9f, 0xfe, 0xc6, 0x2e, 0xfd, 0x15, 0x57, 0x10,
0x94, 0x63, 0xba, 0x0c, 0x8b, 0x06, 0xe6, 0x15, 0xaf, 0xaa, 0x28, 0x78, 0x94, 0x43, 0x35, 0x02, 0xef, 0x7a, 0x0f,
0xb1, 0x34, 0xce, 0xbd, 0xf9, 0xbd, 0x2a, 0x1d, 0x28, 0xbd, 0x79, 0xa4, 0xd3, 0xf9, 0xfc, 0x26, 0xa7, 0xe8, 0xd5,
0xf5, 0x3b, 0x78, 0x08, 0x16, 0x05, 0xe2, 0xd5, 0x1a, 0xde, 0x9b, 0x5b, 0x24, 0x2b, 0xf5, 0x82, 0xe7, 0x50, 0x2a,
0xaa, 0x2e, 0x3c, 0x20, 0x50, 0x57, 0x2c, 0x60, 0x8c, 0xa3, 0x45, 0x33, 0x7f, 0x57, 0x50, 0x22, 0x28, 0x5a, 0xb2,
0x15, 0x45, 0x4c, 0x42, 0x1d, 0x50, 0x52, 0x24, 0x99, 0x6a, 0x8e, 0x8c, 0x9a, 0xee, 0x6d, 0x25, 0x69, 0x88, 0xae,
0xaa, 0x7a, 0xab, 0x85, 0x24, 0x39, 0xe1, 0x4b, 0x9a, 0x1e, 0x84, 0x29, 0xea, 0x6d, 0xd5, 0x36, 0xe8, 0x97, 0x17,
0x6f, 0x5e, 0xab, 0x87, 0x36, 0x45, 0x4e, 0xa7, 0x6c, 0x23, 0xd1, 0x8f, 0x37, 0x2f, 0x50, 0x5b, 0xc3, 0xa6, 0x53,
0x63, 0x5b, 0xb5, 0xa2, 0xcd, 0x1a, 0x2a, 0x4b, 0xaa, 0x48, 0x40, 0xb9, 0xa0, 0x52, 0x42, 0xa1, 0x21, 0x30, 0x94,
0xce, 0xda, 0x13, 0x53, 0x75, 0x83, 0xbb, 0x20, 0x7e, 0xde, 0x95, 0xd7, 0x51, 0x1e, 0x18, 0xd7, 0xaf, 0x3b, 0x6a,
0x70, 0x3d, 0x98, 0x47, 0xea, 0x39, 0x8d, 0x88, 0x7e, 0x84, 0xc4, 0x83, 0x35, 0xcb, 0x98, 0x7a, 0xb8, 0xcd, 0x23,
0x5d, 0x8f, 0x2a, 0x09, 0xaa, 0x24, 0x32, 0x5f, 0x34, 0x74, 0xaf, 0xa0, 0x7c, 0x09, 0xaf, 0x64, 0xd8, 0x70, 0xa8,
0x50, 0x12, 0x9a, 0x57, 0x05, 0x54, 0x40, 0xf1, 0xf5, 0xf5, 0x0f, 0xff, 0x52, 0x9f, 0x3f, 0xc0, 0xcf, 0x13, 0x27,
0x3c, 0x29, 0x0c, 0xa3, 0xea, 0x74, 0x7c, 0xe3, 0xa1, 0xf9, 0x90, 0x51, 0xc3, 0x7b, 0x00, 0xfc, 0x4e, 0xef, 0x49,
0x79, 0x77, 0x98, 0xec, 0x24, 0xe9, 0x5f, 0x5d, 0xd9, 0x1a, 0x26, 0xd1, 0x2e, 0x4a, 0x26, 0xe7, 0xd7, 0x60, 0x60,
0x34, 0x30, 0x0b, 0xe0, 0x9c, 0x72, 0xc0, 0xd0, 0xe6, 0x1d, 0x0f, 0xec, 0xa8, 0x42, 0xec, 0x27, 0x8d, 0x98, 0xd9,
0x60, 0xed, 0x65, 0x49, 0x65, 0x5e, 0xa5, 0xf1, 0xbb, 0x1f, 0xaf, 0x6f, 0x8e, 0x1e, 0x77, 0xb0, 0x52, 0x9e, 0x98,
0x0f, 0x2c, 0x6d, 0x21, 0x59, 0x4d, 0x1a, 0xa9, 0xc5, 0x3a, 0x2a, 0xce, 0x0e, 0x1e, 0xe9, 0x75, 0xbd, 0x33, 0xda,
0xa9, 0x8e, 0x71, 0x30, 0x47, 0x0f, 0xd9, 0x78, 0xd0, 0xfd, 0x99, 0x95, 0x03, 0x73, 0x14, 0x07, 0xe6, 0x5c, 0x0e,
0xf4, 0xe7, 0xa7, 0xdf, 0x01, 0xf1, 0x69, 0xfc, 0xac, 0x8e, 0x12, 0x00, 0x00};
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xdd, 0x58, 0x5b, 0x8f, 0xdb, 0x36, 0x16, 0x7e, 0xef,
0xaf, 0xe0, 0x2a, 0x49, 0x2d, 0x37, 0x23, 0xea, 0x66, 0xf9, 0x2a, 0xa9, 0x48, 0xb2, 0x29, 0x5a, 0x20, 0x69, 0x03,
0xcc, 0xb4, 0xfb, 0x10, 0x04, 0x18, 0x5a, 0xa2, 0x2c, 0x66, 0x24, 0x4a, 0x15, 0xe9, 0x5b, 0x0c, 0xef, 0x6f, 0xdf,
0x43, 0x52, 0xf6, 0x38, 0xb3, 0x99, 0x05, 0x52, 0xec, 0x62, 0xd1, 0x4e, 0x26, 0x1c, 0x92, 0x3a, 0xd7, 0x4f, 0x3c,
0x17, 0x2a, 0xfe, 0x5b, 0xde, 0x64, 0x72, 0xdf, 0x52, 0x54, 0xca, 0xba, 0x4a, 0x63, 0x35, 0xa2, 0x8a, 0xf0, 0x55,
0x42, 0x39, 0xac, 0x28, 0xc9, 0xd3, 0xb8, 0xa6, 0x92, 0xa0, 0xac, 0x24, 0x9d, 0xa0, 0x32, 0xf9, 0xf5, 0xe6, 0x07,
0x67, 0x8a, 0xdc, 0x34, 0xae, 0x18, 0xbf, 0x43, 0x1d, 0xad, 0x12, 0x96, 0x35, 0x1c, 0x95, 0x1d, 0x2d, 0x92, 0x9c,
0x48, 0x32, 0x67, 0x35, 0x59, 0x51, 0x45, 0xa0, 0xd9, 0x38, 0xa9, 0x69, 0xb2, 0x61, 0x74, 0xdb, 0x36, 0x9d, 0x44,
0x40, 0x29, 0x29, 0x97, 0x89, 0xb5, 0x65, 0xb9, 0x2c, 0x93, 0x9c, 0x6e, 0x58, 0x46, 0x1d, 0xbd, 0xb8, 0x62, 0x9c,
0x49, 0x46, 0x2a, 0x47, 0x64, 0xa4, 0xa2, 0x89, 0x7f, 0xb5, 0x16, 0xb4, 0xd3, 0x0b, 0xb2, 0x84, 0x35, 0x6f, 0x2c,
0x10, 0x29, 0xb2, 0x8e, 0xb5, 0x12, 0x29, 0x7b, 0x93, 0xba, 0xc9, 0xd7, 0x15, 0x4d, 0x5d, 0x97, 0x08, 0xb0, 0x4b,
0xb8, 0x8c, 0xe7, 0x74, 0x87, 0xa7, 0xb3, 0x68, 0x32, 0x9e, 0xe6, 0x13, 0xfc, 0x51, 0x7c, 0x03, 0x9e, 0xad, 0x6b,
0x50, 0x87, 0xab, 0x26, 0x23, 0x92, 0x35, 0x1c, 0x0b, 0x4a, 0xba, 0xac, 0x4c, 0x92, 0xc4, 0xfa, 0x5e, 0x90, 0x0d,
0xb5, 0xbe, 0xfd, 0xd6, 0x3e, 0x13, 0xad, 0xa8, 0x7c, 0x5d, 0x51, 0x35, 0x15, 0x2f, 0xf7, 0x37, 0x64, 0xf5, 0x33,
0x58, 0x6e, 0x5b, 0x44, 0xb0, 0x9c, 0x5a, 0xc3, 0xf7, 0xde, 0x07, 0x2c, 0xe4, 0xbe, 0xa2, 0x38, 0x67, 0xa2, 0xad,
0xc8, 0x3e, 0xb1, 0x96, 0x20, 0xf5, 0xce, 0x1a, 0x2e, 0x8a, 0x35, 0xcf, 0x94, 0x70, 0x24, 0x6c, 0x3a, 0x3c, 0x54,
0x14, 0xcc, 0x4b, 0xde, 0x12, 0x59, 0xe2, 0x9a, 0xec, 0x6c, 0x33, 0x61, 0xdc, 0x0e, 0xbe, 0xb3, 0xe9, 0x73, 0xdf,
0xf3, 0x86, 0x57, 0x7a, 0xf0, 0x86, 0x2e, 0xfc, 0x5d, 0x74, 0x54, 0xae, 0x3b, 0x8e, 0x88, 0x7d, 0x1b, 0xb7, 0x40,
0x89, 0xf2, 0xc4, 0xaa, 0xfd, 0x00, 0x7b, 0xde, 0x14, 0xf9, 0x33, 0x1c, 0x44, 0x8e, 0xef, 0xe3, 0xd0, 0xf1, 0xa3,
0x6c, 0xe2, 0x44, 0xc8, 0x1f, 0xc1, 0x10, 0x04, 0x38, 0x42, 0xde, 0x27, 0x0b, 0x15, 0xac, 0xaa, 0x12, 0x8b, 0x37,
0x9c, 0x5a, 0x48, 0xc8, 0xae, 0xb9, 0xa3, 0x89, 0x95, 0xad, 0xbb, 0x0e, 0xec, 0x7f, 0xd5, 0x54, 0x4d, 0x07, 0x70,
0x7d, 0x83, 0x3e, 0xfb, 0xf9, 0x6a, 0x15, 0xb2, 0x23, 0x5c, 0x14, 0x4d, 0x57, 0x27, 0x96, 0x7e, 0x29, 0xf6, 0xd3,
0x83, 0x3c, 0x22, 0x35, 0x0c, 0x2f, 0x1e, 0x3a, 0x4d, 0xc7, 0x56, 0x8c, 0x27, 0x96, 0x1f, 0x20, 0x7f, 0x0a, 0x6a,
0x6f, 0x87, 0xc7, 0x33, 0x26, 0x44, 0x61, 0xd2, 0x7b, 0xd9, 0xd8, 0xef, 0x6f, 0x63, 0xb1, 0x59, 0xa1, 0x5d, 0x5d,
0x71, 0x91, 0x58, 0xa5, 0x94, 0xed, 0xdc, 0x75, 0xb7, 0xdb, 0x2d, 0xde, 0x86, 0xb8, 0xe9, 0x56, 0x6e, 0xe0, 0x79,
0x9e, 0x0b, 0x14, 0x16, 0x32, 0xe7, 0xc3, 0x0a, 0x46, 0x16, 0x2a, 0x29, 0x5b, 0x95, 0x52, 0xcf, 0xd3, 0xa7, 0x07,
0x7a, 0x8c, 0x15, 0x45, 0x7a, 0xfb, 0xe1, 0x42, 0x4b, 0x77, 0xa1, 0x85, 0x7e, 0x7f, 0x81, 0xe6, 0xe0, 0xad, 0x32,
0x6a, 0x42, 0x02, 0x14, 0x20, 0x4f, 0xff, 0x0b, 0x1c, 0x35, 0xef, 0x57, 0xce, 0x83, 0x15, 0xba, 0x58, 0xc1, 0x5f,
0xc0, 0x2f, 0xa8, 0xc7, 0xce, 0xec, 0xcc, 0xee, 0xab, 0xc7, 0x1b, 0xdf, 0xbb, 0xdf, 0x50, 0x3c, 0x3f, 0x8e, 0x2f,
0xd7, 0x4e, 0xf0, 0x9b, 0x22, 0x50, 0xd8, 0x9f, 0x99, 0x9c, 0xa0, 0xf4, 0x7f, 0x1b, 0x93, 0x08, 0x45, 0xfd, 0x4e,
0xe4, 0xa8, 0xf9, 0x79, 0xa5, 0x34, 0xa1, 0x68, 0x03, 0x54, 0xb5, 0x33, 0x76, 0x22, 0x12, 0xa2, 0xb0, 0x37, 0x09,
0x66, 0xb0, 0x3d, 0x06, 0xe6, 0x8b, 0x3d, 0x27, 0xfc, 0x34, 0x50, 0x30, 0xcf, 0x2d, 0xeb, 0x1e, 0x83, 0xe6, 0x12,
0x03, 0xfc, 0xb1, 0x81, 0x33, 0x67, 0x59, 0x80, 0x11, 0x95, 0x59, 0x69, 0x5b, 0x2e, 0x44, 0x5e, 0xc1, 0x56, 0x10,
0x15, 0x0d, 0xb7, 0x86, 0x58, 0x96, 0x94, 0xdb, 0x27, 0x56, 0xc5, 0x48, 0xf5, 0x13, 0xfb, 0xe1, 0x13, 0x39, 0x3c,
0x9c, 0xe3, 0x43, 0x32, 0x09, 0x71, 0x28, 0xb1, 0x8a, 0xe8, 0xab, 0xf3, 0xee, 0xb2, 0xc9, 0xf7, 0x8f, 0x84, 0x4e,
0xe9, 0x9b, 0xb8, 0x61, 0x9c, 0xd3, 0xee, 0x86, 0xee, 0xe0, 0x1d, 0xfe, 0x83, 0xfd, 0xc0, 0xd0, 0xcf, 0x54, 0x6e,
0x9b, 0xee, 0x4e, 0xcc, 0x91, 0xf5, 0xdc, 0x88, 0x5b, 0xa8, 0xa8, 0x61, 0x20, 0x9b, 0xb4, 0x02, 0x8b, 0x0a, 0x72,
0x82, 0xed, 0x0f, 0x21, 0x7e, 0xda, 0x7b, 0x4b, 0xf8, 0xc9, 0xb9, 0xdb, 0x38, 0x67, 0x1b, 0x94, 0x55, 0x10, 0xf5,
0x70, 0xfc, 0x8d, 0x28, 0x0b, 0xf5, 0x47, 0xbd, 0xe1, 0x19, 0x70, 0xdf, 0x25, 0xd6, 0x17, 0xa2, 0xfa, 0xe5, 0xfe,
0xa7, 0xdc, 0x1e, 0x08, 0x88, 0xe7, 0xc1, 0x10, 0x6f, 0x48, 0xb5, 0xa6, 0x28, 0x41, 0xb2, 0x64, 0xe2, 0xde, 0xc0,
0xc5, 0xa3, 0x6c, 0xad, 0xb8, 0x03, 0xae, 0x02, 0x1e, 0x0b, 0x7b, 0x68, 0x9d, 0x22, 0x2b, 0x26, 0x26, 0xef, 0x59,
0x4f, 0xac, 0x07, 0x16, 0x39, 0x15, 0x2d, 0xa4, 0x75, 0x1f, 0x81, 0x4f, 0x0f, 0xc2, 0xe6, 0xb8, 0x03, 0xed, 0xc3,
0xe3, 0x79, 0x33, 0x16, 0x2d, 0xe1, 0x0f, 0x19, 0x95, 0x81, 0xea, 0xa0, 0x43, 0xb2, 0x82, 0x99, 0x3a, 0xed, 0x40,
0x74, 0x56, 0xe8, 0x92, 0xd3, 0xf4, 0xe9, 0xa1, 0x03, 0x89, 0x2a, 0x07, 0x9d, 0x25, 0xc6, 0x2e, 0x40, 0x93, 0xde,
0x1e, 0x87, 0xf7, 0x7e, 0xfc, 0xbe, 0xa6, 0xdd, 0xfe, 0x9a, 0x56, 0x34, 0x93, 0x4d, 0x67, 0x5b, 0x4f, 0x40, 0x0b,
0xbc, 0x7e, 0xed, 0xf0, 0x8f, 0x37, 0x6f, 0xdf, 0x24, 0x8d, 0xcd, 0x86, 0x57, 0x8f, 0x51, 0xab, 0x0c, 0xff, 0x1e,
0x32, 0xfc, 0x3f, 0x93, 0x81, 0xca, 0xf1, 0x83, 0x0f, 0xc0, 0xaa, 0xfd, 0xbd, 0xbd, 0x4f, 0xf4, 0x2a, 0x18, 0x9f,
0x43, 0x40, 0x5f, 0x29, 0x0f, 0x9d, 0x71, 0x34, 0x3c, 0x82, 0x7e, 0xb0, 0x00, 0xec, 0xd6, 0xb9, 0x1a, 0x72, 0xb6,
0x4a, 0x9b, 0xe9, 0x77, 0x87, 0x65, 0xb3, 0x73, 0x04, 0xfb, 0xc4, 0xf8, 0x6a, 0xce, 0x78, 0x49, 0x3b, 0x26, 0x8f,
0x60, 0x2e, 0xa4, 0xfd, 0x76, 0x2d, 0x0f, 0x2d, 0xc9, 0x73, 0xf5, 0x24, 0x6a, 0x77, 0x8b, 0x02, 0x8a, 0x84, 0xa2,
0xa4, 0x73, 0x9f, 0xd6, 0x47, 0xf3, 0x5c, 0xe7, 0x83, 0xf9, 0x2c, 0x7a, 0x76, 0x54, 0x07, 0xee, 0x20, 0xe1, 0x65,
0x39, 0xa4, 0x62, 0x2b, 0x3e, 0xcf, 0xc0, 0x70, 0xda, 0x19, 0xa6, 0x82, 0xd4, 0xac, 0xda, 0xcf, 0x05, 0x64, 0x26,
0x07, 0xaa, 0x07, 0x2b, 0x8e, 0xcb, 0xb5, 0x94, 0x0d, 0x07, 0xdd, 0x5d, 0x4e, 0xbb, 0xb9, 0xb7, 0x30, 0x13, 0xa7,
0x23, 0x39, 0x5b, 0x8b, 0x39, 0x0e, 0x3b, 0x5a, 0x2f, 0x96, 0x24, 0xbb, 0x5b, 0x75, 0xcd, 0x9a, 0xe7, 0x4e, 0xa6,
0x32, 0xe7, 0xfc, 0x89, 0x5f, 0x90, 0x90, 0x66, 0x8b, 0x7e, 0x55, 0x14, 0xc5, 0x02, 0xa0, 0xa0, 0x8e, 0xc9, 0x44,
0xf3, 0x00, 0x8f, 0x14, 0xdb, 0x85, 0x99, 0x38, 0x50, 0x1b, 0xc6, 0x46, 0x48, 0xeb, 0xcf, 0x16, 0x27, 0x77, 0xbc,
0x05, 0xa4, 0x64, 0x01, 0x42, 0x5a, 0x88, 0x47, 0x30, 0xf3, 0x58, 0x13, 0xc6, 0x2f, 0xad, 0x57, 0xc7, 0x64, 0xd1,
0x97, 0x14, 0x80, 0x45, 0xab, 0xd1, 0x85, 0x65, 0x01, 0x45, 0xc3, 0x14, 0xc6, 0x79, 0x30, 0xf6, 0xda, 0xdd, 0x11,
0xf7, 0x07, 0xe4, 0x70, 0xa2, 0x2e, 0x2a, 0xba, 0x5b, 0x7c, 0x5c, 0x0b, 0xc9, 0x8a, 0xbd, 0xd3, 0x17, 0xd6, 0x39,
0x1c, 0x16, 0x28, 0xa8, 0x4b, 0x20, 0xa5, 0x94, 0x2f, 0xb4, 0x0e, 0x87, 0x49, 0x5a, 0x8b, 0x1e, 0xa7, 0xb3, 0x18,
0x7d, 0x40, 0x3f, 0x97, 0xf5, 0x9f, 0xa8, 0xd5, 0x59, 0x3c, 0xd4, 0xa4, 0x83, 0x44, 0xef, 0x2c, 0x1b, 0xc0, 0xb4,
0x9e, 0x3b, 0x13, 0x78, 0x57, 0xfd, 0x96, 0x12, 0x06, 0x9e, 0x83, 0x99, 0xba, 0x5e, 0x9e, 0xf0, 0xf6, 0xdb, 0x1d,
0x12, 0x4d, 0xc5, 0xf2, 0x9e, 0x4e, 0x93, 0x20, 0xef, 0x0c, 0x8f, 0x0f, 0xaf, 0x1b, 0xa9, 0xbd, 0x13, 0xd4, 0xa3,
0x62, 0x4a, 0x7c, 0xef, 0x0b, 0x6f, 0x24, 0x2f, 0x8a, 0x60, 0x59, 0x9c, 0x91, 0x52, 0x65, 0xef, 0xc8, 0xfa, 0x53,
0x11, 0x8c, 0x40, 0xc0, 0xe9, 0xdd, 0xc0, 0xfc, 0xc8, 0x74, 0x58, 0x1c, 0x2e, 0xa4, 0xe8, 0xa3, 0x3a, 0x5f, 0x77,
0x95, 0x6d, 0x7d, 0xe1, 0xe8, 0x3e, 0x0b, 0x5f, 0xdd, 0x97, 0xa5, 0xc1, 0xe3, 0x65, 0x69, 0x80, 0x54, 0x23, 0xf3,
0xb2, 0xd9, 0x25, 0x03, 0x5d, 0x20, 0x46, 0xf0, 0x3b, 0x78, 0x16, 0xbe, 0x06, 0xfe, 0xff, 0x4a, 0xbd, 0xf9, 0xc3,
0xc5, 0xe6, 0x2b, 0x2a, 0xcd, 0x57, 0x56, 0x19, 0xe3, 0x9d, 0x72, 0x1e, 0x66, 0x50, 0x4e, 0x18, 0x16, 0x6c, 0xe5,
0xff, 0x2f, 0xa0, 0xfd, 0x77, 0x1c, 0xc3, 0x17, 0xfe, 0x14, 0xcf, 0x90, 0x1e, 0x0c, 0x44, 0x38, 0x9c, 0xa2, 0xc9,
0xab, 0x11, 0x1e, 0xf9, 0x48, 0xb5, 0x30, 0x63, 0x34, 0x81, 0x7e, 0x0f, 0xf9, 0x63, 0x1c, 0x4e, 0x60, 0x03, 0x05,
0x3e, 0x8e, 0xde, 0x04, 0x21, 0x1e, 0x47, 0x40, 0x15, 0x78, 0x38, 0x0c, 0x90, 0xa1, 0x1d, 0xe3, 0x00, 0xc4, 0x29,
0x92, 0xb0, 0x06, 0xa0, 0xb3, 0x10, 0x7b, 0x13, 0x10, 0x37, 0xc6, 0xde, 0x0c, 0x4f, 0xc7, 0x68, 0x8a, 0x27, 0x00,
0x1d, 0x1e, 0x45, 0x95, 0x13, 0x61, 0x1f, 0xb6, 0xc3, 0x31, 0x99, 0xe2, 0x51, 0x88, 0xf4, 0x60, 0xe0, 0x98, 0x80,
0x08, 0x07, 0x7b, 0xfe, 0x9b, 0x10, 0x07, 0x13, 0xd0, 0x3b, 0x1a, 0xbd, 0x00, 0xb1, 0xb3, 0x11, 0x32, 0xa3, 0x81,
0x17, 0x14, 0x44, 0x8f, 0x81, 0x16, 0xfc, 0x75, 0x41, 0x03, 0x48, 0x7c, 0x14, 0xe2, 0x19, 0xc4, 0xae, 0xaf, 0xf8,
0xcd, 0x68, 0x70, 0xf3, 0x7d, 0xe4, 0xfd, 0x61, 0xcc, 0xc2, 0xbf, 0x2e, 0x66, 0xbe, 0x42, 0x00, 0xa6, 0xa0, 0x1b,
0xe4, 0x20, 0x3d, 0x18, 0xdd, 0xc0, 0x3c, 0x7d, 0x35, 0x43, 0x53, 0xe0, 0x1a, 0x4f, 0xd1, 0x0c, 0x45, 0x0a, 0x5d,
0x60, 0x1f, 0x19, 0x26, 0x07, 0x98, 0xbe, 0x12, 0xc6, 0xd1, 0x9f, 0x18, 0xc6, 0xc7, 0x7c, 0xfa, 0x13, 0xbb, 0xf4,
0xff, 0x48, 0x41, 0xd0, 0x8e, 0xe9, 0x36, 0x2c, 0x76, 0xcd, 0x95, 0x5e, 0x75, 0x51, 0x70, 0x43, 0x87, 0x6e, 0x04,
0x2e, 0xf9, 0x3e, 0x62, 0x79, 0x52, 0xfa, 0xe9, 0x67, 0xdd, 0x39, 0x50, 0xfa, 0x69, 0xac, 0xcb, 0x79, 0x7a, 0x53,
0x52, 0xf4, 0xfa, 0xfa, 0x1d, 0xdc, 0xca, 0xaa, 0x0a, 0xf1, 0x66, 0x0b, 0x97, 0xbf, 0x3d, 0x92, 0x8d, 0xba, 0xce,
0x73, 0xe8, 0x15, 0xd5, 0x14, 0xee, 0x0d, 0xa8, 0x6f, 0x16, 0x30, 0xc6, 0xf1, 0xb2, 0x4b, 0xdf, 0x55, 0x94, 0x08,
0x8a, 0x56, 0x6c, 0x43, 0x11, 0x93, 0xd0, 0x07, 0xd4, 0x14, 0x49, 0xa6, 0x86, 0x33, 0xa3, 0xa6, 0x83, 0x9e, 0x56,
0x2b, 0x31, 0xdd, 0x30, 0x58, 0x02, 0x62, 0xd2, 0xbe, 0xed, 0x8d, 0xcb, 0xd0, 0x58, 0x75, 0x4d, 0xa5, 0x84, 0x8e,
0x41, 0x59, 0x15, 0xa6, 0xb1, 0xba, 0x76, 0x22, 0xa2, 0x2f, 0x06, 0x89, 0xbb, 0x65, 0x05, 0x53, 0x97, 0xf9, 0x34,
0xd6, 0xad, 0xa2, 0x92, 0xa0, 0xba, 0x15, 0xf3, 0xe5, 0x41, 0xcf, 0x2a, 0xca, 0x57, 0x70, 0x9b, 0x84, 0x77, 0x01,
0xcd, 0x43, 0x46, 0xcb, 0xa6, 0x82, 0xe6, 0x24, 0xb9, 0xbe, 0xfe, 0xe9, 0xef, 0xea, 0x33, 0x85, 0x32, 0xe1, 0xcc,
0x09, 0x7d, 0xbe, 0x61, 0x54, 0x93, 0x9e, 0x6f, 0x3c, 0x32, 0x1f, 0x1c, 0x5a, 0xe8, 0xd3, 0xc1, 0xbf, 0xfc, 0x33,
0x29, 0xef, 0x4e, 0x9b, 0xbd, 0x24, 0xfd, 0x5f, 0x37, 0x9d, 0x86, 0x49, 0xac, 0x97, 0x35, 0x93, 0xe9, 0x35, 0x18,
0x18, 0xbb, 0xe6, 0x01, 0x38, 0xa7, 0x1c, 0x30, 0xb4, 0x65, 0xcf, 0x03, 0x60, 0xff, 0x72, 0xf3, 0x02, 0xfd, 0xda,
0xc2, 0x09, 0xa6, 0x06, 0x7b, 0xed, 0x65, 0x4d, 0x65, 0xd9, 0xe4, 0xc9, 0xbb, 0x5f, 0xae, 0x6f, 0xce, 0x1e, 0xaf,
0x35, 0x11, 0xa2, 0x3c, 0x33, 0x1f, 0x42, 0xd6, 0x95, 0x64, 0x2d, 0xe9, 0xa4, 0x16, 0xeb, 0xa8, 0x10, 0x38, 0x79,
0xa4, 0x9f, 0x17, 0xac, 0xa2, 0xc6, 0xa9, 0x9e, 0xd1, 0x4d, 0xd1, 0x97, 0x6c, 0x3c, 0xe9, 0x7e, 0x60, 0xa5, 0x6b,
0x4e, 0x89, 0x6b, 0x8e, 0x8c, 0xab, 0x3f, 0x13, 0xfd, 0x0b, 0x65, 0x37, 0xa3, 0x8e, 0x36, 0x12, 0x00, 0x00};
} // namespace captive_portal
} // namespace esphome

View File

@@ -6,7 +6,7 @@ from esphome.const import (
ICON_RADIATOR,
ICON_RESTART,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_MILLION,
UNIT_PARTS_PER_BILLION,
@@ -43,7 +43,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(

View File

@@ -140,8 +140,9 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
/** Stop the cover.
*
* This is a legacy method and may be removed later, please use `.make_call()` instead.
* As per solution from issue #2885 the call should include perform()
*/
ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop() instead.", "2021.9")
ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop().perform() instead.", "2021.9")
void stop();
void add_on_state_callback(std::function<void()> &&f);

View File

@@ -1,9 +1,9 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32
#include <esp_sleep.h>
@@ -11,6 +11,7 @@
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#include "esphome/core/time.h"
#endif
namespace esphome {
@@ -170,7 +171,7 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> {
if (after_time)
timestamp += 60 * 60 * 24;
int32_t offset = time::ESPTime::timezone_offset();
int32_t offset = ESPTime::timezone_offset();
timestamp -= offset; // Change timestamp to utc
const uint32_t ms_left = (timestamp - timestamp_now) * 1000;
this->deep_sleep_->set_sleep_duration(ms_left);

View File

@@ -3,9 +3,9 @@
#include <utility>
#include "esphome/core/application.h"
#include "esphome/core/color.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace display {
@@ -15,93 +15,6 @@ static const char *const TAG = "display";
const Color COLOR_OFF(0, 0, 0, 0);
const Color COLOR_ON(255, 255, 255, 255);
void Rect::expand(int16_t horizontal, int16_t vertical) {
if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) {
this->x = this->x - horizontal;
this->y = this->y - vertical;
this->w = this->w + (2 * horizontal);
this->h = this->h + (2 * vertical);
}
}
void Rect::extend(Rect rect) {
if (!this->is_set()) {
this->x = rect.x;
this->y = rect.y;
this->w = rect.w;
this->h = rect.h;
} else {
if (this->x > rect.x) {
this->w = this->w + (this->x - rect.x);
this->x = rect.x;
}
if (this->y > rect.y) {
this->h = this->h + (this->y - rect.y);
this->y = rect.y;
}
if (this->x2() < rect.x2()) {
this->w = rect.x2() - this->x;
}
if (this->y2() < rect.y2()) {
this->h = rect.y2() - this->y;
}
}
}
void Rect::shrink(Rect rect) {
if (!this->inside(rect)) {
(*this) = Rect();
} else {
if (this->x2() > rect.x2()) {
this->w = rect.x2() - this->x;
}
if (this->x < rect.x) {
this->w = this->w + (this->x - rect.x);
this->x = rect.x;
}
if (this->y2() > rect.y2()) {
this->h = rect.y2() - this->y;
}
if (this->y < rect.y) {
this->h = this->h + (this->y - rect.y);
this->y = rect.y;
}
}
}
bool Rect::equal(Rect rect) {
return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
}
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) { // NOLINT
if (!this->is_set()) {
return true;
}
if (absolute) {
return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2()));
} else {
return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h));
}
}
bool Rect::inside(Rect rect, bool absolute) {
if (!this->is_set() || !rect.is_set()) {
return true;
}
if (absolute) {
return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y));
} else {
return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0));
}
}
void Rect::info(const std::string &prefix) {
if (this->is_set()) {
ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
this->y2());
} else
ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
}
void DisplayBuffer::init_internal_(uint32_t buffer_length) {
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->buffer_ = allocator.allocate(buffer_length);
@@ -252,99 +165,53 @@ void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, Color
} while (dx <= 0);
}
void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align, const char *text) {
void DisplayBuffer::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) {
int x_start, y_start;
int width, height;
this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height);
int i = 0;
int x_at = x_start;
while (text[i] != '\0') {
int match_length;
int glyph_n = font->match_next_glyph(text + i, &match_length);
if (glyph_n < 0) {
// Unknown char, skip
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
if (!font->get_glyphs().empty()) {
uint8_t glyph_width = font->get_glyphs()[0].glyph_data_->width;
for (int glyph_x = 0; glyph_x < glyph_width; glyph_x++) {
for (int glyph_y = 0; glyph_y < height; glyph_y++)
this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color);
}
x_at += glyph_width;
}
i++;
continue;
}
const Glyph &glyph = font->get_glyphs()[glyph_n];
int scan_x1, scan_y1, scan_width, scan_height;
glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
{
const int glyph_x_max = scan_x1 + scan_width;
const int glyph_y_max = scan_y1 + scan_height;
for (int glyph_x = scan_x1; glyph_x < glyph_x_max; glyph_x++) {
for (int glyph_y = scan_y1; glyph_y < glyph_y_max; glyph_y++) {
if (glyph.get_pixel(glyph_x, glyph_y)) {
this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color);
}
}
}
}
x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
i += match_length;
}
font->print(x_start, y_start, this, color, text);
}
void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg) {
void DisplayBuffer::vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format,
va_list arg) {
char buffer[256];
int ret = vsnprintf(buffer, sizeof(buffer), format, arg);
if (ret > 0)
this->print(x, y, font, color, align, buffer);
}
void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) {
switch (image->get_type()) {
case IMAGE_TYPE_BINARY:
for (int img_x = 0; img_x < image->get_width(); img_x++) {
for (int img_y = 0; img_y < image->get_height(); img_y++) {
this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? color_on : color_off);
}
}
void DisplayBuffer::image(int x, int y, BaseImage *image, Color color_on, Color color_off) {
this->image(x, y, image, ImageAlign::TOP_LEFT, color_on, color_off);
}
void DisplayBuffer::image(int x, int y, BaseImage *image, ImageAlign align, Color color_on, Color color_off) {
auto x_align = ImageAlign(int(align) & (int(ImageAlign::HORIZONTAL_ALIGNMENT)));
auto y_align = ImageAlign(int(align) & (int(ImageAlign::VERTICAL_ALIGNMENT)));
switch (x_align) {
case ImageAlign::RIGHT:
x -= image->get_width();
break;
case IMAGE_TYPE_GRAYSCALE:
for (int img_x = 0; img_x < image->get_width(); img_x++) {
for (int img_y = 0; img_y < image->get_height(); img_y++) {
this->draw_pixel_at(x + img_x, y + img_y, image->get_grayscale_pixel(img_x, img_y));
}
}
case ImageAlign::CENTER_HORIZONTAL:
x -= image->get_width() / 2;
break;
case IMAGE_TYPE_RGB24:
for (int img_x = 0; img_x < image->get_width(); img_x++) {
for (int img_y = 0; img_y < image->get_height(); img_y++) {
this->draw_pixel_at(x + img_x, y + img_y, image->get_color_pixel(img_x, img_y));
}
}
break;
case IMAGE_TYPE_TRANSPARENT_BINARY:
for (int img_x = 0; img_x < image->get_width(); img_x++) {
for (int img_y = 0; img_y < image->get_height(); img_y++) {
if (image->get_pixel(img_x, img_y))
this->draw_pixel_at(x + img_x, y + img_y, color_on);
}
}
break;
case IMAGE_TYPE_RGB565:
for (int img_x = 0; img_x < image->get_width(); img_x++) {
for (int img_y = 0; img_y < image->get_height(); img_y++) {
this->draw_pixel_at(x + img_x, y + img_y, image->get_rgb565_pixel(img_x, img_y));
}
}
case ImageAlign::LEFT:
default:
break;
}
switch (y_align) {
case ImageAlign::BOTTOM:
y -= image->get_height();
break;
case ImageAlign::CENTER_VERTICAL:
y -= image->get_height() / 2;
break;
case ImageAlign::TOP:
default:
break;
}
image->draw(x, y, this, color_on, color_off);
}
#ifdef USE_GRAPH
@@ -360,7 +227,7 @@ void DisplayBuffer::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_
}
#endif // USE_QR_CODE
void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1,
void DisplayBuffer::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1,
int *width, int *height) {
int x_offset, baseline;
font->measure(text, width, &x_offset, &baseline, height);
@@ -398,34 +265,34 @@ void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font,
break;
}
}
void DisplayBuffer::print(int x, int y, Font *font, Color color, const char *text) {
void DisplayBuffer::print(int x, int y, BaseFont *font, Color color, const char *text) {
this->print(x, y, font, color, TextAlign::TOP_LEFT, text);
}
void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char *text) {
void DisplayBuffer::print(int x, int y, BaseFont *font, TextAlign align, const char *text) {
this->print(x, y, font, COLOR_ON, align, text);
}
void DisplayBuffer::print(int x, int y, Font *font, const char *text) {
void DisplayBuffer::print(int x, int y, BaseFont *font, const char *text) {
this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text);
}
void DisplayBuffer::printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) {
void DisplayBuffer::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, color, align, format, arg);
va_end(arg);
}
void DisplayBuffer::printf(int x, int y, Font *font, Color color, const char *format, ...) {
void DisplayBuffer::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg);
va_end(arg);
}
void DisplayBuffer::printf(int x, int y, Font *font, TextAlign align, const char *format, ...) {
void DisplayBuffer::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, COLOR_ON, align, format, arg);
va_end(arg);
}
void DisplayBuffer::printf(int x, int y, Font *font, const char *format, ...) {
void DisplayBuffer::printf(int x, int y, BaseFont *font, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg);
@@ -472,24 +339,22 @@ void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) {
if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to))
this->trigger(from, to);
}
#ifdef USE_TIME
void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format,
time::ESPTime time) {
void DisplayBuffer::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format,
ESPTime time) {
char buffer[64];
size_t ret = time.strftime(buffer, sizeof(buffer), format);
if (ret > 0)
this->print(x, y, font, color, align, buffer);
}
void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) {
void DisplayBuffer::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) {
this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time);
}
void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) {
void DisplayBuffer::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) {
this->strftime(x, y, font, COLOR_ON, align, format, time);
}
void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, time::ESPTime time) {
void DisplayBuffer::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) {
this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time);
}
#endif
void DisplayBuffer::start_clipping(Rect rect) {
if (!this->clipping_rectangle_.empty()) {
@@ -526,217 +391,6 @@ Rect DisplayBuffer::get_clipping() {
return this->clipping_rectangle_.back();
}
}
bool Glyph::get_pixel(int x, int y) const {
const int x_data = x - this->glyph_data_->offset_x;
const int y_data = y - this->glyph_data_->offset_y;
if (x_data < 0 || x_data >= this->glyph_data_->width || y_data < 0 || y_data >= this->glyph_data_->height)
return false;
const uint32_t width_8 = ((this->glyph_data_->width + 7u) / 8u) * 8u;
const uint32_t pos = x_data + y_data * width_8;
return progmem_read_byte(this->glyph_data_->data + (pos / 8u)) & (0x80 >> (pos % 8u));
}
const char *Glyph::get_char() const { return this->glyph_data_->a_char; }
bool Glyph::compare_to(const char *str) const {
// 1 -> this->char_
// 2 -> str
for (uint32_t i = 0;; i++) {
if (this->glyph_data_->a_char[i] == '\0')
return true;
if (str[i] == '\0')
return false;
if (this->glyph_data_->a_char[i] > str[i])
return false;
if (this->glyph_data_->a_char[i] < str[i])
return true;
}
// this should not happen
return false;
}
int Glyph::match_length(const char *str) const {
for (uint32_t i = 0;; i++) {
if (this->glyph_data_->a_char[i] == '\0')
return i;
if (str[i] != this->glyph_data_->a_char[i])
return 0;
}
// this should not happen
return 0;
}
void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
*x1 = this->glyph_data_->offset_x;
*y1 = this->glyph_data_->offset_y;
*width = this->glyph_data_->width;
*height = this->glyph_data_->height;
}
int Font::match_next_glyph(const char *str, int *match_length) {
int lo = 0;
int hi = this->glyphs_.size() - 1;
while (lo != hi) {
int mid = (lo + hi + 1) / 2;
if (this->glyphs_[mid].compare_to(str)) {
lo = mid;
} else {
hi = mid - 1;
}
}
*match_length = this->glyphs_[lo].match_length(str);
if (*match_length <= 0)
return -1;
return lo;
}
void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
*baseline = this->baseline_;
*height = this->height_;
int i = 0;
int min_x = 0;
bool has_char = false;
int x = 0;
while (str[i] != '\0') {
int match_length;
int glyph_n = this->match_next_glyph(str + i, &match_length);
if (glyph_n < 0) {
// Unknown char, skip
if (!this->get_glyphs().empty())
x += this->get_glyphs()[0].glyph_data_->width;
i++;
continue;
}
const Glyph &glyph = this->glyphs_[glyph_n];
if (!has_char) {
min_x = glyph.glyph_data_->offset_x;
} else {
min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
}
x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
i += match_length;
has_char = true;
}
*x_offset = min_x;
*width = x - min_x;
}
Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) {
glyphs_.reserve(data_nr);
for (int i = 0; i < data_nr; ++i)
glyphs_.emplace_back(&data[i]);
}
bool Image::get_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return false;
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
const uint32_t pos = x + y * width_8;
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
}
Color Image::get_color_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK;
const uint32_t pos = (x + y * this->width_) * 3;
const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) |
(progmem_read_byte(this->data_start_ + pos + 1) << 8) |
(progmem_read_byte(this->data_start_ + pos + 0) << 16);
return Color(color32);
}
Color Image::get_rgb565_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK;
const uint32_t pos = (x + y * this->width_) * 2;
uint16_t rgb565 =
progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F;
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
}
Color Image::get_grayscale_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK;
const uint32_t pos = (x + y * this->width_);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
return Color(gray | gray << 8 | gray << 16 | gray << 24);
}
int Image::get_width() const { return this->width_; }
int Image::get_height() const { return this->height_; }
ImageType Image::get_type() const { return this->type_; }
Image::Image(const uint8_t *data_start, int width, int height, ImageType type)
: width_(width), height_(height), type_(type), data_start_(data_start) {}
int Image::get_current_frame() const { return 0; }
bool Animation::get_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return false;
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
const uint32_t frame_index = this->height_ * width_8 * this->current_frame_;
if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_))
return false;
const uint32_t pos = x + y * width_8 + frame_index;
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
}
Color Animation::get_color_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK;
const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_;
if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_))
return Color::BLACK;
const uint32_t pos = (x + y * this->width_ + frame_index) * 3;
const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) |
(progmem_read_byte(this->data_start_ + pos + 1) << 8) |
(progmem_read_byte(this->data_start_ + pos + 0) << 16);
return Color(color32);
}
Color Animation::get_rgb565_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK;
const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_;
if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_))
return Color::BLACK;
const uint32_t pos = (x + y * this->width_ + frame_index) * 2;
uint16_t rgb565 =
progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F;
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
}
Color Animation::get_grayscale_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK;
const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_;
if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_))
return Color::BLACK;
const uint32_t pos = (x + y * this->width_ + frame_index);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
return Color(gray | gray << 8 | gray << 16 | gray << 24);
}
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type)
: Image(data_start, width, height, type), current_frame_(0), animation_frame_count_(animation_frame_count) {}
int Animation::get_animation_frame_count() const { return this->animation_frame_count_; }
int Animation::get_current_frame() const { return this->current_frame_; }
void Animation::next_frame() {
this->current_frame_++;
if (this->current_frame_ >= animation_frame_count_) {
this->current_frame_ = 0;
}
}
void Animation::prev_frame() {
this->current_frame_--;
if (this->current_frame_ < 0) {
this->current_frame_ = this->animation_frame_count_ - 1;
}
}
void Animation::set_frame(int frame) {
unsigned abs_frame = abs(frame);
if (abs_frame < this->animation_frame_count_) {
if (frame >= 0) {
this->current_frame_ = frame;
} else {
this->current_frame_ = this->animation_frame_count_ - abs_frame;
}
}
}
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
void DisplayPage::show() { this->parent_->show_page(this); }

View File

@@ -1,15 +1,13 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/automation.h"
#include "display_color_utils.h"
#include <cstdarg>
#include <vector>
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#endif
#include "rect.h"
#include "display_color_utils.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/time.h"
#ifdef USE_GRAPH
#include "esphome/components/graph/graph.h"
@@ -73,17 +71,52 @@ enum class TextAlign {
BOTTOM_RIGHT = BOTTOM | RIGHT,
};
/// Turn the pixel OFF.
extern const Color COLOR_OFF;
/// Turn the pixel ON.
extern const Color COLOR_ON;
/** ImageAlign is used to tell the display class how to position a image. By default
* the coordinates you enter for the image() functions take the upper left corner of the image
* as the "anchor" point. You can customize this behavior to, for example, make the coordinates
* refer to the *center* of the image.
*
* All image alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis
* these options are allowed:
*
* - LEFT (x-coordinate of anchor point is on left)
* - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the image)
* - RIGHT (x-coordinate of anchor point is on right)
*
* For the Y-Axis alignment these options are allowed:
*
* - TOP (y-coordinate of anchor is on the top of the image)
* - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the image)
* - BOTTOM (y-coordinate of anchor is on the bottom of the image)
*
* These options are then combined to create combined TextAlignment options like:
* - TOP_LEFT (default)
* - CENTER (anchor point is in the middle of the image bounds)
* - ...
*/
enum class ImageAlign {
TOP = 0x00,
CENTER_VERTICAL = 0x01,
BOTTOM = 0x02,
enum ImageType {
IMAGE_TYPE_BINARY = 0,
IMAGE_TYPE_GRAYSCALE = 1,
IMAGE_TYPE_RGB24 = 2,
IMAGE_TYPE_TRANSPARENT_BINARY = 3,
IMAGE_TYPE_RGB565 = 4,
LEFT = 0x00,
CENTER_HORIZONTAL = 0x04,
RIGHT = 0x08,
TOP_LEFT = TOP | LEFT,
TOP_CENTER = TOP | CENTER_HORIZONTAL,
TOP_RIGHT = TOP | RIGHT,
CENTER_LEFT = CENTER_VERTICAL | LEFT,
CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL,
CENTER_RIGHT = CENTER_VERTICAL | RIGHT,
BOTTOM_LEFT = BOTTOM | LEFT,
BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL,
BOTTOM_RIGHT = BOTTOM | RIGHT,
HORIZONTAL_ALIGNMENT = LEFT | CENTER_HORIZONTAL | RIGHT,
VERTICAL_ALIGNMENT = TOP | CENTER_VERTICAL | BOTTOM
};
enum DisplayType {
@@ -99,35 +132,6 @@ enum DisplayRotation {
DISPLAY_ROTATION_270_DEGREES = 270,
};
static const int16_t VALUE_NO_SET = 32766;
class Rect {
public:
int16_t x; ///< X coordinate of corner
int16_t y; ///< Y coordinate of corner
int16_t w; ///< Width of region
int16_t h; ///< Height of region
Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT
inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {}
inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner
inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner
inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); }
void expand(int16_t horizontal, int16_t vertical);
void extend(Rect rect);
void shrink(Rect rect);
bool inside(Rect rect, bool absolute = true);
bool inside(int16_t test_x, int16_t test_y, bool absolute = true);
bool equal(Rect rect);
void info(const std::string &prefix = "rect info:");
};
class Font;
class Image;
class DisplayBuffer;
class DisplayPage;
class DisplayOnPageChangeTrigger;
@@ -141,6 +145,24 @@ using display_writer_t = std::function<void(DisplayBuffer &)>;
ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \
}
/// Turn the pixel OFF.
extern const Color COLOR_OFF;
/// Turn the pixel ON.
extern const Color COLOR_ON;
class BaseImage {
public:
virtual void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) = 0;
virtual int get_width() const = 0;
virtual int get_height() const = 0;
};
class BaseFont {
public:
virtual void print(int x, int y, DisplayBuffer *display, Color color, const char *text) = 0;
virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0;
};
class DisplayBuffer {
public:
/// Fill the entire screen with the given color.
@@ -187,7 +209,7 @@ class DisplayBuffer {
* @param align The alignment of the text.
* @param text The text to draw.
*/
void print(int x, int y, Font *font, Color color, TextAlign align, const char *text);
void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text);
/** Print `text` with the top left at [x,y] with `font`.
*
@@ -197,7 +219,7 @@ class DisplayBuffer {
* @param color The color to draw the text with.
* @param text The text to draw.
*/
void print(int x, int y, Font *font, Color color, const char *text);
void print(int x, int y, BaseFont *font, Color color, const char *text);
/** Print `text` with the anchor point at [x,y] with `font`.
*
@@ -207,7 +229,7 @@ class DisplayBuffer {
* @param align The alignment of the text.
* @param text The text to draw.
*/
void print(int x, int y, Font *font, TextAlign align, const char *text);
void print(int x, int y, BaseFont *font, TextAlign align, const char *text);
/** Print `text` with the top left at [x,y] with `font`.
*
@@ -216,7 +238,7 @@ class DisplayBuffer {
* @param font The font to draw the text with.
* @param text The text to draw.
*/
void print(int x, int y, Font *font, const char *text);
void print(int x, int y, BaseFont *font, const char *text);
/** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
*
@@ -228,7 +250,7 @@ class DisplayBuffer {
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
void printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...)
void printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...)
__attribute__((format(printf, 7, 8)));
/** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
@@ -240,7 +262,7 @@ class DisplayBuffer {
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
void printf(int x, int y, Font *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7)));
void printf(int x, int y, BaseFont *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7)));
/** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
*
@@ -251,7 +273,8 @@ class DisplayBuffer {
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
void printf(int x, int y, Font *font, TextAlign align, const char *format, ...) __attribute__((format(printf, 6, 7)));
void printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...)
__attribute__((format(printf, 6, 7)));
/** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
*
@@ -261,9 +284,8 @@ class DisplayBuffer {
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
void printf(int x, int y, Font *font, const char *format, ...) __attribute__((format(printf, 5, 6)));
void printf(int x, int y, BaseFont *font, const char *format, ...) __attribute__((format(printf, 5, 6)));
#ifdef USE_TIME
/** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
*
* @param x The x coordinate of the text alignment anchor point.
@@ -274,7 +296,7 @@ class DisplayBuffer {
* @param format The strftime format to use.
* @param time The time to format.
*/
void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time)
void strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time)
__attribute__((format(strftime, 7, 0)));
/** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
@@ -286,7 +308,7 @@ class DisplayBuffer {
* @param format The strftime format to use.
* @param time The time to format.
*/
void strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time)
void strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time)
__attribute__((format(strftime, 6, 0)));
/** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
@@ -298,7 +320,7 @@ class DisplayBuffer {
* @param format The strftime format to use.
* @param time The time to format.
*/
void strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time)
void strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time)
__attribute__((format(strftime, 6, 0)));
/** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
@@ -309,19 +331,28 @@ class DisplayBuffer {
* @param format The strftime format to use.
* @param time The time to format.
*/
void strftime(int x, int y, Font *font, const char *format, time::ESPTime time)
__attribute__((format(strftime, 5, 0)));
#endif
void strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0)));
/** Draw the `image` with the top-left corner at [x,y] to the screen.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param image The image to draw
* @param image The image to draw.
* @param color_on The color to replace in binary images for the on bits.
* @param color_off The color to replace in binary images for the off bits.
*/
void image(int x, int y, Image *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
/** Draw the `image` at [x,y] to the screen.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param image The image to draw.
* @param align The alignment of the image.
* @param color_on The color to replace in binary images for the on bits.
* @param color_off The color to replace in binary images for the off bits.
*/
void image(int x, int y, BaseImage *image, ImageAlign align, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
#ifdef USE_GRAPH
/** Draw the `graph` with the top-left corner at [x,y] to the screen.
@@ -370,7 +401,7 @@ class DisplayBuffer {
* @param width A pointer to store the returned text width in.
* @param height A pointer to store the returned text height in.
*/
void get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, int *width,
void get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width,
int *height);
/// Internal method to set the display writer lambda.
@@ -445,7 +476,7 @@ class DisplayBuffer {
bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
protected:
void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg);
void vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg);
virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0;
@@ -481,104 +512,6 @@ class DisplayPage {
DisplayPage *next_{nullptr};
};
struct GlyphData {
const char *a_char;
const uint8_t *data;
int offset_x;
int offset_y;
int width;
int height;
};
class Glyph {
public:
Glyph(const GlyphData *data) : glyph_data_(data) {}
bool get_pixel(int x, int y) const;
const char *get_char() const;
bool compare_to(const char *str) const;
int match_length(const char *str) const;
void scan_area(int *x1, int *y1, int *width, int *height) const;
protected:
friend Font;
friend DisplayBuffer;
const GlyphData *glyph_data_;
};
class Font {
public:
/** Construct the font with the given glyphs.
*
* @param glyphs A vector of glyphs, must be sorted lexicographically.
* @param baseline The y-offset from the top of the text to the baseline.
* @param bottom The y-offset from the top of the text to the bottom (i.e. height).
*/
Font(const GlyphData *data, int data_nr, int baseline, int height);
int match_next_glyph(const char *str, int *match_length);
void measure(const char *str, int *width, int *x_offset, int *baseline, int *height);
inline int get_baseline() { return this->baseline_; }
inline int get_height() { return this->height_; }
const std::vector<Glyph, ExternalRAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
protected:
std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_;
int baseline_;
int height_;
};
class Image {
public:
Image(const uint8_t *data_start, int width, int height, ImageType type);
virtual bool get_pixel(int x, int y) const;
virtual Color get_color_pixel(int x, int y) const;
virtual Color get_rgb565_pixel(int x, int y) const;
virtual Color get_grayscale_pixel(int x, int y) const;
int get_width() const;
int get_height() const;
ImageType get_type() const;
virtual int get_current_frame() const;
protected:
int width_;
int height_;
ImageType type_;
const uint8_t *data_start_;
};
class Animation : public Image {
public:
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type);
bool get_pixel(int x, int y) const override;
Color get_color_pixel(int x, int y) const override;
Color get_rgb565_pixel(int x, int y) const override;
Color get_grayscale_pixel(int x, int y) const override;
int get_animation_frame_count() const;
int get_current_frame() const override;
void next_frame();
void prev_frame();
/** Selects a specific frame within the animation.
*
* @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame.
*/
void set_frame(int frame);
protected:
int current_frame_;
int animation_frame_count_;
};
template<typename... Ts> class DisplayPageShowAction : public Action<Ts...> {
public:
TEMPLATABLE_VALUE(DisplayPage *, page)

View File

@@ -0,0 +1,98 @@
#include "rect.h"
#include "esphome/core/log.h"
namespace esphome {
namespace display {
static const char *const TAG = "display";
void Rect::expand(int16_t horizontal, int16_t vertical) {
if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) {
this->x = this->x - horizontal;
this->y = this->y - vertical;
this->w = this->w + (2 * horizontal);
this->h = this->h + (2 * vertical);
}
}
void Rect::extend(Rect rect) {
if (!this->is_set()) {
this->x = rect.x;
this->y = rect.y;
this->w = rect.w;
this->h = rect.h;
} else {
if (this->x > rect.x) {
this->w = this->w + (this->x - rect.x);
this->x = rect.x;
}
if (this->y > rect.y) {
this->h = this->h + (this->y - rect.y);
this->y = rect.y;
}
if (this->x2() < rect.x2()) {
this->w = rect.x2() - this->x;
}
if (this->y2() < rect.y2()) {
this->h = rect.y2() - this->y;
}
}
}
void Rect::shrink(Rect rect) {
if (!this->inside(rect)) {
(*this) = Rect();
} else {
if (this->x2() > rect.x2()) {
this->w = rect.x2() - this->x;
}
if (this->x < rect.x) {
this->w = this->w + (this->x - rect.x);
this->x = rect.x;
}
if (this->y2() > rect.y2()) {
this->h = rect.y2() - this->y;
}
if (this->y < rect.y) {
this->h = this->h + (this->y - rect.y);
this->y = rect.y;
}
}
}
bool Rect::equal(Rect rect) {
return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
}
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) { // NOLINT
if (!this->is_set()) {
return true;
}
if (absolute) {
return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2()));
} else {
return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h));
}
}
bool Rect::inside(Rect rect, bool absolute) {
if (!this->is_set() || !rect.is_set()) {
return true;
}
if (absolute) {
return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y));
} else {
return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0));
}
}
void Rect::info(const std::string &prefix) {
if (this->is_set()) {
ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
this->y2());
} else
ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
}
} // namespace display
} // namespace esphome

View File

@@ -0,0 +1,36 @@
#pragma once
#include "esphome/core/helpers.h"
namespace esphome {
namespace display {
static const int16_t VALUE_NO_SET = 32766;
class Rect {
public:
int16_t x; ///< X coordinate of corner
int16_t y; ///< Y coordinate of corner
int16_t w; ///< Width of region
int16_t h; ///< Height of region
Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT
inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {}
inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner
inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner
inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); }
void expand(int16_t horizontal, int16_t vertical);
void extend(Rect rect);
void shrink(Rect rect);
bool inside(Rect rect, bool absolute = true);
bool inside(int16_t test_x, int16_t test_y, bool absolute = true);
bool equal(Rect rect);
void info(const std::string &prefix = "rect info:");
};
} // namespace display
} // namespace esphome

View File

@@ -37,14 +37,14 @@ void DS1307Component::read_time() {
ESP_LOGW(TAG, "RTC halted, not syncing to system clock.");
return;
}
time::ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10),
.minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10),
.hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10),
.day_of_week = uint8_t(ds1307_.reg.weekday),
.day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10),
.day_of_year = 1, // ignored by recalc_timestamp_utc(false)
.month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10),
.year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)};
ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10),
.minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10),
.hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10),
.day_of_week = uint8_t(ds1307_.reg.weekday),
.day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10),
.day_of_year = 1, // ignored by recalc_timestamp_utc(false)
.month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10),
.year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");

View File

@@ -19,6 +19,7 @@ CONF_CRC_CHECK = "crc_check"
CONF_DECRYPTION_KEY = "decryption_key"
CONF_DSMR_ID = "dsmr_id"
CONF_GAS_MBUS_ID = "gas_mbus_id"
CONF_WATER_MBUS_ID = "water_mbus_id"
CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length"
CONF_REQUEST_INTERVAL = "request_interval"
CONF_REQUEST_PIN = "request_pin"
@@ -53,6 +54,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_DECRYPTION_KEY): _validate_key,
cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean,
cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_,
cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_,
cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_,
cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema,
cv.Optional(
@@ -82,9 +84,10 @@ async def to_code(config):
cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds))
cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID]))
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
# DSMR Parser
cg.add_library("glmnet/Dsmr", "0.5")
cg.add_library("glmnet/Dsmr", "0.7")
# Crypto
cg.add_library("rweather/Crypto", "0.4.0")

View File

@@ -8,6 +8,7 @@ from esphome.const import (
DEVICE_CLASS_GAS,
DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE,
DEVICE_CLASS_WATER,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
@@ -236,6 +237,12 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_GAS,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("water_delivered"): sensor.sensor_schema(
unit_of_measurement=UNIT_CUBIC_METER,
accuracy_decimals=3,
device_class=DEVICE_CLASS_WATER,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
}
).extend(cv.COMPONENT_SCHEMA)

View File

@@ -4,6 +4,7 @@ from esphome.components.light.types import AddressableLightEffect
from esphome.components.light.effects import register_addressable_effect
from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS
AUTO_LOAD = ["socket"]
DEPENDENCIES = ["network"]
e131_ns = cg.esphome_ns.namespace("e131")
@@ -23,16 +24,11 @@ CHANNELS = {
CONF_UNIVERSE = "universe"
CONF_E131_ID = "e131_id"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(E131Component),
cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(
*METHODS, upper=True
),
}
),
cv.only_with_arduino,
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(E131Component),
cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(*METHODS, upper=True),
}
)

View File

@@ -1,18 +1,7 @@
#ifdef USE_ARDUINO
#include "e131.h"
#include "e131_addressable_light_effect.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32
#include <WiFi.h>
#endif
#ifdef USE_ESP8266
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#endif
namespace esphome {
namespace e131 {
@@ -22,17 +11,41 @@ static const int PORT = 5568;
E131Component::E131Component() {}
E131Component::~E131Component() {
if (udp_) {
udp_->stop();
if (this->socket_) {
this->socket_->close();
}
}
void E131Component::setup() {
udp_ = make_unique<WiFiUDP>();
this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP);
if (!udp_->begin(PORT)) {
ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT);
mark_failed();
int enable = 1;
int err = this->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 = this->socket_->setblocking(false);
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
this->mark_failed();
return;
}
struct sockaddr_storage server;
socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), PORT);
if (sl == 0) {
ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno);
this->mark_failed();
return;
}
server.ss_family = AF_INET;
err = this->socket_->bind((struct sockaddr *) &server, sizeof(server));
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
this->mark_failed();
return;
}
@@ -43,22 +56,22 @@ void E131Component::loop() {
std::vector<uint8_t> payload;
E131Packet packet;
int universe = 0;
uint8_t buf[1460];
while (uint16_t packet_size = udp_->parsePacket()) {
payload.resize(packet_size);
ssize_t len = this->socket_->read(buf, sizeof(buf));
if (len == -1) {
return;
}
payload.resize(len);
memmove(&payload[0], buf, len);
if (!udp_->read(&payload[0], payload.size())) {
continue;
}
if (!this->packet_(payload, universe, packet)) {
ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size());
return;
}
if (!packet_(payload, universe, packet)) {
ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size());
continue;
}
if (!process_(universe, packet)) {
ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
}
if (!this->process_(universe, packet)) {
ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
}
}
@@ -106,5 +119,3 @@ bool E131Component::process_(int universe, const E131Packet &packet) {
} // namespace e131
} // namespace esphome
#endif // USE_ARDUINO

View File

@@ -1,7 +1,6 @@
#pragma once
#ifdef USE_ARDUINO
#include "esphome/components/socket/socket.h"
#include "esphome/core/component.h"
#include <map>
@@ -9,8 +8,6 @@
#include <set>
#include <vector>
class UDP;
namespace esphome {
namespace e131 {
@@ -47,7 +44,7 @@ class E131Component : public esphome::Component {
void leave_(int universe);
E131ListenMethod listen_method_{E131_MULTICAST};
std::unique_ptr<UDP> udp_;
std::unique_ptr<socket::Socket> socket_;
std::set<E131AddressableLightEffect *> light_effects_;
std::map<int, int> universe_consumers_;
std::map<int, E131Packet> universe_packets_;
@@ -55,5 +52,3 @@ class E131Component : public esphome::Component {
} // namespace e131
} // namespace esphome
#endif // USE_ARDUINO

View File

@@ -1,7 +1,5 @@
#ifdef USE_ARDUINO
#include "e131.h"
#include "e131_addressable_light_effect.h"
#include "e131.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -92,5 +90,3 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
} // namespace e131
} // namespace esphome
#endif // USE_ARDUINO

View File

@@ -1,7 +1,5 @@
#pragma once
#ifdef USE_ARDUINO
#include "esphome/core/component.h"
#include "esphome/components/light/addressable_light_effect.h"
@@ -44,5 +42,3 @@ class E131AddressableLightEffect : public light::AddressableLightEffect {
} // namespace e131
} // namespace esphome
#endif // USE_ARDUINO

View File

@@ -1,15 +1,13 @@
#ifdef USE_ARDUINO
#include <cstring>
#include "e131.h"
#include "esphome/components/network/ip_address.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#include "esphome/components/network/ip_address.h"
#include <cstring>
#include <lwip/init.h>
#include <lwip/ip_addr.h>
#include <lwip/ip4_addr.h>
#include <lwip/igmp.h>
#include <lwip/init.h>
#include <lwip/ip4_addr.h>
#include <lwip/ip_addr.h>
namespace esphome {
namespace e131 {
@@ -62,7 +60,7 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast<size_t>(&((E131RawPacket *)
bool E131Component::join_igmp_groups_() {
if (listen_method_ != E131_MULTICAST)
return false;
if (!udp_)
if (this->socket_ == nullptr)
return false;
for (auto universe : universe_consumers_) {
@@ -140,5 +138,3 @@ bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E13
} // namespace e131
} // namespace esphome
#endif // USE_ARDUINO

View File

@@ -42,6 +42,39 @@ ESP32_BASE_PINS = {
}
ESP32_BOARD_PINS = {
"adafruit_feather_esp32s2_tft": {
"BUTTON": 0,
"A0": 18,
"A1": 17,
"A2": 16,
"A3": 15,
"A4": 14,
"A5": 8,
"SCK": 36,
"MOSI": 35,
"MISO": 37,
"RX": 2,
"TX": 1,
"D13": 13,
"D12": 12,
"D11": 11,
"D10": 10,
"D9": 9,
"D6": 6,
"D5": 5,
"NEOPIXEL": 33,
"PIN_NEOPIXEL": 33,
"NEOPIXEL_POWER": 34,
"SCL": 41,
"SDA": 42,
"TFT_I2C_POWER": 21,
"TFT_CS": 7,
"TFT_DC": 39,
"TFT_RESET": 40,
"TFT_BACKLIGHT": 45,
"LED": 13,
"LED_BUILTIN": 13,
},
"adafruit_qtpy_esp32c3": {
"A0": 4,
"A1": 3,

View File

@@ -244,6 +244,17 @@ void ESP32BLE::dump_config() {
}
}
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) {
uint64_t u = 0;
u |= uint64_t(address[0] & 0xFF) << 40;
u |= uint64_t(address[1] & 0xFF) << 32;
u |= uint64_t(address[2] & 0xFF) << 24;
u |= uint64_t(address[3] & 0xFF) << 16;
u |= uint64_t(address[4] & 0xFF) << 8;
u |= uint64_t(address[5] & 0xFF) << 0;
return u;
}
ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esp32_ble

View File

@@ -18,6 +18,8 @@
namespace esphome {
namespace esp32_ble {
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address);
// NOLINTNEXTLINE(modernize-use-using)
typedef struct {
void *peer_device;

View File

@@ -167,7 +167,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger),
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address),
}
),
cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation(
@@ -223,7 +223,10 @@ async def to_code(config):
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_MAC_ADDRESS in conf:
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
addr_list = []
for it in conf[CONF_MAC_ADDRESS]:
addr_list.append(it.as_hex)
cg.add(trigger.set_addresses(addr_list))
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -10,18 +10,22 @@ namespace esp32_ble_tracker {
class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener {
public:
explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); }
void set_address(uint64_t address) { this->address_ = address; }
void set_addresses(const std::vector<uint64_t> &addresses) { this->address_vec_ = addresses; }
bool parse_device(const ESPBTDevice &device) override {
if (this->address_ && device.address_uint64() != this->address_) {
return false;
uint64_t u64_addr = device.address_uint64();
if (!address_vec_.empty()) {
if (std::find(address_vec_.begin(), address_vec_.end(), u64_addr) == address_vec_.end()) {
return false;
}
}
this->trigger(device);
return true;
}
protected:
uint64_t address_ = 0;
std::vector<uint64_t> address_vec_;
};
class BLEServiceDataAdvertiseTrigger : public Trigger<const adv_data_t &>, public ESPBTDeviceListener {

View File

@@ -34,17 +34,6 @@ static const char *const TAG = "esp32_ble_tracker";
ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) {
uint64_t u = 0;
u |= uint64_t(address[0] & 0xFF) << 40;
u |= uint64_t(address[1] & 0xFF) << 32;
u |= uint64_t(address[2] & 0xFF) << 24;
u |= uint64_t(address[3] & 0xFF) << 16;
u |= uint64_t(address[4] & 0xFF) << 8;
u |= uint64_t(address[5] & 0xFF) << 0;
return u;
}
float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void ESP32BLETracker::setup() {
@@ -114,10 +103,20 @@ void ESP32BLETracker::loop() {
if (this->scan_result_index_ && // if it looks like we have a scan result we will take the lock
xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) {
uint32_t index = this->scan_result_index_;
if (index) {
if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up.");
}
if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up.");
}
bool bulk_parsed = false;
for (auto *listener : this->listeners_) {
bulk_parsed |= listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
}
for (auto *client : this->clients_) {
bulk_parsed |= client->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
}
if (!bulk_parsed) {
for (size_t i = 0; i < index; i++) {
ESPBTDevice device;
device.parse_scan_rst(this->scan_result_buffer_[i]);
@@ -141,8 +140,8 @@ void ESP32BLETracker::loop() {
this->print_bt_device_info(device);
}
}
this->scan_result_index_ = 0;
}
this->scan_result_index_ = 0;
xSemaphoreGive(this->scan_result_lock_);
}
@@ -585,7 +584,7 @@ std::string ESPBTDevice::address_str() const {
this->address_[3], this->address_[4], this->address_[5]);
return mac;
}
uint64_t ESPBTDevice::address_uint64() const { return ble_addr_to_uint64(this->address_); }
uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); }
void ESP32BLETracker::dump_config() {
ESP_LOGCONFIG(TAG, "BLE Tracker:");

View File

@@ -113,6 +113,9 @@ class ESPBTDeviceListener {
public:
virtual void on_scan_end() {}
virtual bool parse_device(const ESPBTDevice &device) = 0;
virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) {
return false;
};
void set_parent(ESP32BLETracker *parent) { parent_ = parent; }
protected:

View File

@@ -1,3 +1,4 @@
#include <cinttypes>
#include "led_strip.h"
#ifdef USE_ESP32
@@ -195,7 +196,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() {
break;
}
ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order);
ESP_LOGCONFIG(TAG, " Max refresh rate: %u", *this->max_refresh_rate_);
ESP_LOGCONFIG(TAG, " Max refresh rate: %" PRIu32, *this->max_refresh_rate_);
ESP_LOGCONFIG(TAG, " Number of LEDs: %u", this->num_leds_);
}

View File

@@ -34,7 +34,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
if (this->is_rgbw_) {
traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::RGB_WHITE});
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
} else {
traits.set_supported_color_modes({light::ColorMode::RGB});
}

View File

@@ -1,7 +1,7 @@
#include "ethernet_info_text_sensor.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#ifdef USE_ESP32
namespace esphome {
namespace ethernet_info {
@@ -13,4 +13,4 @@ void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IP
} // namespace ethernet_info
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO
#endif // USE_ESP32

View File

@@ -4,7 +4,7 @@
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/ethernet/ethernet_component.h"
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#ifdef USE_ESP32
namespace esphome {
namespace ethernet_info {
@@ -30,4 +30,4 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS
} // namespace ethernet_info
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO
#endif // USE_ESP32

View File

@@ -7,7 +7,6 @@ import re
import requests
from esphome import core
from esphome.components import display
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.helpers import copy_file_if_changed
@@ -29,9 +28,11 @@ DOMAIN = "font"
DEPENDENCIES = ["display"]
MULTI_CONF = True
Font = display.display_ns.class_("Font")
Glyph = display.display_ns.class_("Glyph")
GlyphData = display.display_ns.struct("GlyphData")
font_ns = cg.esphome_ns.namespace("font")
Font = font_ns.class_("Font")
Glyph = font_ns.class_("Glyph")
GlyphData = font_ns.struct("GlyphData")
def validate_glyphs(value):

View File

@@ -0,0 +1,149 @@
#include "font.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/color.h"
#include "esphome/components/display/display_buffer.h"
namespace esphome {
namespace font {
static const char *const TAG = "font";
void Glyph::draw(int x_at, int y_start, display::DisplayBuffer *display, Color color) const {
int scan_x1, scan_y1, scan_width, scan_height;
this->scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
const unsigned char *data = this->glyph_data_->data;
const int max_x = x_at + scan_x1 + scan_width;
const int max_y = y_start + scan_y1 + scan_height;
for (int glyph_y = y_start + scan_y1; glyph_y < max_y; glyph_y++) {
for (int glyph_x = x_at + scan_x1; glyph_x < max_x; data++, glyph_x += 8) {
uint8_t pixel_data = progmem_read_byte(data);
const int pixel_max_x = std::min(max_x, glyph_x + 8);
for (int pixel_x = glyph_x; pixel_x < pixel_max_x && pixel_data; pixel_x++, pixel_data <<= 1) {
if (pixel_data & 0x80) {
display->draw_pixel_at(pixel_x, glyph_y, color);
}
}
}
}
}
const char *Glyph::get_char() const { return this->glyph_data_->a_char; }
bool Glyph::compare_to(const char *str) const {
// 1 -> this->char_
// 2 -> str
for (uint32_t i = 0;; i++) {
if (this->glyph_data_->a_char[i] == '\0')
return true;
if (str[i] == '\0')
return false;
if (this->glyph_data_->a_char[i] > str[i])
return false;
if (this->glyph_data_->a_char[i] < str[i])
return true;
}
// this should not happen
return false;
}
int Glyph::match_length(const char *str) const {
for (uint32_t i = 0;; i++) {
if (this->glyph_data_->a_char[i] == '\0')
return i;
if (str[i] != this->glyph_data_->a_char[i])
return 0;
}
// this should not happen
return 0;
}
void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
*x1 = this->glyph_data_->offset_x;
*y1 = this->glyph_data_->offset_y;
*width = this->glyph_data_->width;
*height = this->glyph_data_->height;
}
Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) {
glyphs_.reserve(data_nr);
for (int i = 0; i < data_nr; ++i)
glyphs_.emplace_back(&data[i]);
}
int Font::match_next_glyph(const char *str, int *match_length) {
int lo = 0;
int hi = this->glyphs_.size() - 1;
while (lo != hi) {
int mid = (lo + hi + 1) / 2;
if (this->glyphs_[mid].compare_to(str)) {
lo = mid;
} else {
hi = mid - 1;
}
}
*match_length = this->glyphs_[lo].match_length(str);
if (*match_length <= 0)
return -1;
return lo;
}
void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
*baseline = this->baseline_;
*height = this->height_;
int i = 0;
int min_x = 0;
bool has_char = false;
int x = 0;
while (str[i] != '\0') {
int match_length;
int glyph_n = this->match_next_glyph(str + i, &match_length);
if (glyph_n < 0) {
// Unknown char, skip
if (!this->get_glyphs().empty())
x += this->get_glyphs()[0].glyph_data_->width;
i++;
continue;
}
const Glyph &glyph = this->glyphs_[glyph_n];
if (!has_char) {
min_x = glyph.glyph_data_->offset_x;
} else {
min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
}
x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
i += match_length;
has_char = true;
}
*x_offset = min_x;
*width = x - min_x;
}
void Font::print(int x_start, int y_start, display::DisplayBuffer *display, Color color, const char *text) {
int i = 0;
int x_at = x_start;
while (text[i] != '\0') {
int match_length;
int glyph_n = this->match_next_glyph(text + i, &match_length);
if (glyph_n < 0) {
// Unknown char, skip
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
if (!this->get_glyphs().empty()) {
uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->width;
display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color);
x_at += glyph_width;
}
i++;
continue;
}
const Glyph &glyph = this->get_glyphs()[glyph_n];
glyph.draw(x_at, y_start, display, color);
x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
i += match_length;
}
}
} // namespace font
} // namespace esphome

View File

@@ -0,0 +1,67 @@
#pragma once
#include "esphome/core/datatypes.h"
#include "esphome/core/color.h"
#include "esphome/components/display/display_buffer.h"
namespace esphome {
namespace font {
class Font;
struct GlyphData {
const char *a_char;
const uint8_t *data;
int offset_x;
int offset_y;
int width;
int height;
};
class Glyph {
public:
Glyph(const GlyphData *data) : glyph_data_(data) {}
void draw(int x, int y, display::DisplayBuffer *display, Color color) const;
const char *get_char() const;
bool compare_to(const char *str) const;
int match_length(const char *str) const;
void scan_area(int *x1, int *y1, int *width, int *height) const;
protected:
friend Font;
const GlyphData *glyph_data_;
};
class Font : public display::BaseFont {
public:
/** Construct the font with the given glyphs.
*
* @param glyphs A vector of glyphs, must be sorted lexicographically.
* @param baseline The y-offset from the top of the text to the baseline.
* @param bottom The y-offset from the top of the text to the bottom (i.e. height).
*/
Font(const GlyphData *data, int data_nr, int baseline, int height);
int match_next_glyph(const char *str, int *match_length);
void print(int x_start, int y_start, display::DisplayBuffer *display, Color color, const char *text) override;
void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) override;
inline int get_baseline() { return this->baseline_; }
inline int get_height() { return this->height_; }
const std::vector<Glyph, ExternalRAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
protected:
std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_;
int baseline_;
int height_;
};
} // namespace font
} // namespace esphome

View File

@@ -151,11 +151,13 @@ void FujitsuGeneralClimate::transmit_state() {
case climate::CLIMATE_FAN_LOW:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW);
break;
case climate::CLIMATE_FAN_QUIET:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_SILENT);
break;
case climate::CLIMATE_FAN_AUTO:
default:
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO);
break;
// TODO Quiet / Silent
}
// Set swing
@@ -345,8 +347,9 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) {
const uint8_t recv_fan_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_FAN_NIBBLE);
ESP_LOGV(TAG, "Received fan mode %X", recv_fan_mode);
switch (recv_fan_mode) {
// TODO No Quiet / Silent in ESPH
case FUJITSU_GENERAL_FAN_SILENT:
this->fan_mode = climate::CLIMATE_FAN_QUIET;
break;
case FUJITSU_GENERAL_FAN_LOW:
this->fan_mode = climate::CLIMATE_FAN_LOW;
break;

View File

@@ -52,7 +52,7 @@ class FujitsuGeneralClimate : public climate_ir::ClimateIR {
FujitsuGeneralClimate()
: ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH},
climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_QUIET},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_BOTH}) {}

View File

@@ -16,7 +16,7 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) {
if (tiny_gps.date.year() < 2019)
return;
time::ESPTime val{};
ESPTime val{};
val.year = tiny_gps.date.year();
val.month = tiny_gps.date.month();
val.day_of_month = tiny_gps.date.day();

View File

@@ -11,7 +11,7 @@ namespace esphome {
// forward declare DisplayBuffer
namespace display {
class DisplayBuffer;
class Font;
class BaseFont;
} // namespace display
namespace graph {
@@ -45,8 +45,8 @@ enum ValuePositionType {
class GraphLegend {
public:
void init(Graph *g);
void set_name_font(display::Font *font) { this->font_label_ = font; }
void set_value_font(display::Font *font) { this->font_value_ = font; }
void set_name_font(display::BaseFont *font) { this->font_label_ = font; }
void set_value_font(display::BaseFont *font) { this->font_value_ = font; }
void set_width(uint32_t width) { this->width_ = width; }
void set_height(uint32_t height) { this->height_ = height; }
void set_border(bool val) { this->border_ = val; }
@@ -63,8 +63,8 @@ class GraphLegend {
ValuePositionType values_{VALUE_POSITION_TYPE_AUTO};
bool units_{true};
DirectionType direction_{DIRECTION_TYPE_AUTO};
display::Font *font_label_{nullptr};
display::Font *font_value_{nullptr};
display::BaseFont *font_label_{nullptr};
display::BaseFont *font_value_{nullptr};
// Calculated values
Graph *parent_{nullptr};
// (x0) (xs,ys) (xs,ys)

View File

@@ -9,11 +9,42 @@ static const char *const TAG = "growatt_solar";
static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04;
static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion
void GrowattSolar::loop() {
// If update() was unable to send we retry until we can send.
if (!this->waiting_to_update_)
return;
update();
}
void GrowattSolar::update() {
// If our last send has had no reply yet, and it wasn't that long ago, do nothing.
uint32_t now = millis();
if (now - this->last_send_ < this->get_update_interval() / 2) {
return;
}
// The bus might be slow, or there might be other devices, or other components might be talking to our device.
if (this->waiting_for_response()) {
this->waiting_to_update_ = true;
return;
}
this->waiting_to_update_ = false;
this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT[this->protocol_version_]);
this->last_send_ = millis();
}
void GrowattSolar::on_modbus_data(const std::vector<uint8_t> &data) {
// Other components might be sending commands to our device. But we don't get called with enough
// context to know what is what. So if we didn't do a send, we ignore the data.
if (!this->last_send_)
return;
this->last_send_ = 0;
// Also ignore the data if the message is too short. Otherwise we will publish invalid values.
if (data.size() < MODBUS_REGISTER_COUNT[this->protocol_version_] * 2)
return;
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void {
if (sensor == nullptr)
return;

View File

@@ -19,6 +19,7 @@ enum GrowattProtocolVersion {
class GrowattSolar : public PollingComponent, public modbus::ModbusDevice {
public:
void loop() override;
void update() override;
void on_modbus_data(const std::vector<uint8_t> &data) override;
void dump_config() override;
@@ -55,6 +56,9 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice {
}
protected:
bool waiting_to_update_;
uint32_t last_send_;
struct GrowattPhase {
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};

View File

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

View File

@@ -0,0 +1,130 @@
#pragma once
#include "esphome/core/automation.h"
#include "haier_base.h"
#include "hon_climate.h"
namespace esphome {
namespace haier {
template<typename... Ts> class DisplayOnAction : public Action<Ts...> {
public:
DisplayOnAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->set_display_state(true); }
protected:
HaierClimateBase *parent_;
};
template<typename... Ts> class DisplayOffAction : public Action<Ts...> {
public:
DisplayOffAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->set_display_state(false); }
protected:
HaierClimateBase *parent_;
};
template<typename... Ts> class BeeperOnAction : public Action<Ts...> {
public:
BeeperOnAction(HonClimate *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->set_beeper_state(true); }
protected:
HonClimate *parent_;
};
template<typename... Ts> class BeeperOffAction : public Action<Ts...> {
public:
BeeperOffAction(HonClimate *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->set_beeper_state(false); }
protected:
HonClimate *parent_;
};
template<typename... Ts> class VerticalAirflowAction : public Action<Ts...> {
public:
VerticalAirflowAction(HonClimate *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(AirflowVerticalDirection, direction)
void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); }
protected:
HonClimate *parent_;
};
template<typename... Ts> class HorizontalAirflowAction : public Action<Ts...> {
public:
HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(AirflowHorizontalDirection, direction)
void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); }
protected:
HonClimate *parent_;
};
template<typename... Ts> class HealthOnAction : public Action<Ts...> {
public:
HealthOnAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->set_health_mode(true); }
protected:
HaierClimateBase *parent_;
};
template<typename... Ts> class HealthOffAction : public Action<Ts...> {
public:
HealthOffAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->set_health_mode(false); }
protected:
HaierClimateBase *parent_;
};
template<typename... Ts> class StartSelfCleaningAction : public Action<Ts...> {
public:
StartSelfCleaningAction(HonClimate *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->start_self_cleaning(); }
protected:
HonClimate *parent_;
};
template<typename... Ts> class StartSteriCleaningAction : public Action<Ts...> {
public:
StartSteriCleaningAction(HonClimate *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->start_steri_cleaning(); }
protected:
HonClimate *parent_;
};
template<typename... Ts> class PowerOnAction : public Action<Ts...> {
public:
PowerOnAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->send_power_on_command(); }
protected:
HaierClimateBase *parent_;
};
template<typename... Ts> class PowerOffAction : public Action<Ts...> {
public:
PowerOffAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->send_power_off_command(); }
protected:
HaierClimateBase *parent_;
};
template<typename... Ts> class PowerToggleAction : public Action<Ts...> {
public:
PowerToggleAction(HaierClimateBase *parent) : parent_(parent) {}
void play(Ts... x) { this->parent_->toggle_power(); }
protected:
HaierClimateBase *parent_;
};
} // namespace haier
} // namespace esphome

View File

@@ -1,43 +1,364 @@
from esphome.components import climate
import logging
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
from esphome.components.climate import ClimateSwingMode
from esphome.const import CONF_ID, CONF_SUPPORTED_SWING_MODES
import esphome.final_validate as fv
from esphome.components import uart, sensor, climate, logger
from esphome import automation
from esphome.const import (
CONF_BEEPER,
CONF_ID,
CONF_LEVEL,
CONF_LOGGER,
CONF_LOGS,
CONF_MAX_TEMPERATURE,
CONF_MIN_TEMPERATURE,
CONF_PROTOCOL,
CONF_SUPPORTED_MODES,
CONF_SUPPORTED_SWING_MODES,
CONF_VISUAL,
CONF_WIFI,
DEVICE_CLASS_TEMPERATURE,
ICON_THERMOMETER,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
from esphome.components.climate import (
ClimateSwingMode,
ClimateMode,
)
DEPENDENCIES = ["uart"]
_LOGGER = logging.getLogger(__name__)
PROTOCOL_MIN_TEMPERATURE = 16.0
PROTOCOL_MAX_TEMPERATURE = 30.0
PROTOCOL_TEMPERATURE_STEP = 1.0
CODEOWNERS = ["@paveldn"]
AUTO_LOAD = ["sensor"]
DEPENDENCIES = ["climate", "uart"]
CONF_WIFI_SIGNAL = "wifi_signal"
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
CONF_VERTICAL_AIRFLOW = "vertical_airflow"
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
PROTOCOL_HON = "HON"
PROTOCOL_SMARTAIR2 = "SMARTAIR2"
PROTOCOLS_SUPPORTED = [PROTOCOL_HON, PROTOCOL_SMARTAIR2]
haier_ns = cg.esphome_ns.namespace("haier")
HaierClimate = haier_ns.class_(
"HaierClimate", climate.Climate, cg.PollingComponent, uart.UARTDevice
HaierClimateBase = haier_ns.class_(
"HaierClimateBase", uart.UARTDevice, climate.Climate, cg.Component
)
HonClimate = haier_ns.class_("HonClimate", HaierClimateBase)
Smartair2Climate = haier_ns.class_("Smartair2Climate", HaierClimateBase)
ALLOWED_CLIMATE_SWING_MODES = {
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
AirflowVerticalDirection = haier_ns.enum("AirflowVerticalDirection")
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = {
"UP": AirflowVerticalDirection.UP,
"CENTER": AirflowVerticalDirection.CENTER,
"DOWN": AirflowVerticalDirection.DOWN,
}
validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True)
AirflowHorizontalDirection = haier_ns.enum("AirflowHorizontalDirection")
AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = {
"LEFT": AirflowHorizontalDirection.LEFT,
"CENTER": AirflowHorizontalDirection.CENTER,
"RIGHT": AirflowHorizontalDirection.RIGHT,
}
CONFIG_SCHEMA = cv.All(
SUPPORTED_SWING_MODES_OPTIONS = {
"OFF": ClimateSwingMode.CLIMATE_SWING_OFF, # always available
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, # always available
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
}
SUPPORTED_CLIMATE_MODES_OPTIONS = {
"OFF": ClimateMode.CLIMATE_MODE_OFF, # always available
"AUTO": ClimateMode.CLIMATE_MODE_AUTO, # always available
"COOL": ClimateMode.CLIMATE_MODE_COOL,
"HEAT": ClimateMode.CLIMATE_MODE_HEAT,
"DRY": ClimateMode.CLIMATE_MODE_DRY,
"FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
}
def validate_visual(config):
if CONF_VISUAL in config:
visual_config = config[CONF_VISUAL]
if CONF_MIN_TEMPERATURE in visual_config:
min_temp = visual_config[CONF_MIN_TEMPERATURE]
if min_temp < PROTOCOL_MIN_TEMPERATURE:
raise cv.Invalid(
f"Configured visual minimum temperature {min_temp} is lower than supported by Haier protocol is {PROTOCOL_MIN_TEMPERATURE}"
)
else:
config[CONF_VISUAL][CONF_MIN_TEMPERATURE] = PROTOCOL_MIN_TEMPERATURE
if CONF_MAX_TEMPERATURE in visual_config:
max_temp = visual_config[CONF_MAX_TEMPERATURE]
if max_temp > PROTOCOL_MAX_TEMPERATURE:
raise cv.Invalid(
f"Configured visual maximum temperature {max_temp} is higher than supported by Haier protocol is {PROTOCOL_MAX_TEMPERATURE}"
)
else:
config[CONF_VISUAL][CONF_MAX_TEMPERATURE] = PROTOCOL_MAX_TEMPERATURE
else:
config[CONF_VISUAL] = {
CONF_MIN_TEMPERATURE: PROTOCOL_MIN_TEMPERATURE,
CONF_MAX_TEMPERATURE: PROTOCOL_MAX_TEMPERATURE,
}
return config
BASE_CONFIG_SCHEMA = (
climate.CLIMATE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(HaierClimate),
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
validate_swing_modes
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True)
),
cv.Optional(
CONF_SUPPORTED_SWING_MODES,
default=[
"OFF",
"VERTICAL",
"HORIZONTAL",
"BOTH",
],
): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)),
}
)
.extend(cv.polling_component_schema("5s"))
.extend(uart.UART_DEVICE_SCHEMA),
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(Smartair2Climate),
}
),
PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(HonClimate),
cv.Optional(CONF_WIFI_SIGNAL, default=True): cv.boolean,
cv.Optional(CONF_BEEPER, default=True): cv.boolean,
cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
}
),
},
key=CONF_PROTOCOL,
default_type=PROTOCOL_SMARTAIR2,
upper=True,
),
validate_visual,
)
# Actions
DisplayOnAction = haier_ns.class_("DisplayOnAction", automation.Action)
DisplayOffAction = haier_ns.class_("DisplayOffAction", automation.Action)
BeeperOnAction = haier_ns.class_("BeeperOnAction", automation.Action)
BeeperOffAction = haier_ns.class_("BeeperOffAction", automation.Action)
StartSelfCleaningAction = haier_ns.class_("StartSelfCleaningAction", automation.Action)
StartSteriCleaningAction = haier_ns.class_(
"StartSteriCleaningAction", automation.Action
)
VerticalAirflowAction = haier_ns.class_("VerticalAirflowAction", automation.Action)
HorizontalAirflowAction = haier_ns.class_("HorizontalAirflowAction", automation.Action)
HealthOnAction = haier_ns.class_("HealthOnAction", automation.Action)
HealthOffAction = haier_ns.class_("HealthOffAction", automation.Action)
PowerOnAction = haier_ns.class_("PowerOnAction", automation.Action)
PowerOffAction = haier_ns.class_("PowerOffAction", automation.Action)
PowerToggleAction = haier_ns.class_("PowerToggleAction", automation.Action)
HAIER_BASE_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(HaierClimateBase),
}
)
HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(HonClimate),
}
)
@automation.register_action(
"climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA
)
@automation.register_action(
"climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA
)
async def display_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_action(
"climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA
)
@automation.register_action(
"climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA
)
async def beeper_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
# Start self cleaning or steri-cleaning action action
@automation.register_action(
"climate.haier.start_self_cleaning",
StartSelfCleaningAction,
HAIER_HON_BASE_ACTION_SCHEMA,
)
@automation.register_action(
"climate.haier.start_steri_cleaning",
StartSteriCleaningAction,
HAIER_HON_BASE_ACTION_SCHEMA,
)
async def start_cleaning_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
# Set vertical airflow direction action
@automation.register_action(
"climate.haier.set_vertical_airflow",
VerticalAirflowAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(HonClimate),
cv.Required(CONF_VERTICAL_AIRFLOW): cv.templatable(
cv.enum(AIRFLOW_VERTICAL_DIRECTION_OPTIONS, upper=True)
),
}
),
)
async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(
config[CONF_VERTICAL_AIRFLOW], args, AirflowVerticalDirection
)
cg.add(var.set_direction(template_))
return var
# Set horizontal airflow direction action
@automation.register_action(
"climate.haier.set_horizontal_airflow",
HorizontalAirflowAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(HonClimate),
cv.Required(CONF_HORIZONTAL_AIRFLOW): cv.templatable(
cv.enum(AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, upper=True)
),
}
),
)
async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(
config[CONF_HORIZONTAL_AIRFLOW], args, AirflowHorizontalDirection
)
cg.add(var.set_direction(template_))
return var
@automation.register_action(
"climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA
)
@automation.register_action(
"climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA
)
async def health_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_action(
"climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA
)
@automation.register_action(
"climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA
)
@automation.register_action(
"climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA
)
async def power_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
def _final_validate(config):
full_config = fv.full_config.get()
if CONF_LOGGER in full_config:
_level = "NONE"
logger_config = full_config[CONF_LOGGER]
if CONF_LOGS in logger_config:
if "haier.protocol" in logger_config[CONF_LOGS]:
_level = logger_config[CONF_LOGS]["haier.protocol"]
else:
_level = logger_config[CONF_LEVEL]
_LOGGER.info("Detected log level for Haier protocol: %s", _level)
if _level not in logger.LOG_LEVEL_SEVERITY:
raise cv.Invalid("Unknown log level for Haier protocol")
_severity = logger.LOG_LEVEL_SEVERITY.index(_level)
cg.add_build_flag(f"-DHAIER_LOG_LEVEL={_severity}")
else:
_LOGGER.info(
"No logger component found, logging for Haier protocol is disabled"
)
cg.add_build_flag("-DHAIER_LOG_LEVEL=0")
if (
(CONF_WIFI_SIGNAL in config)
and (config[CONF_WIFI_SIGNAL])
and CONF_WIFI not in full_config
):
raise cv.Invalid(
f"No WiFi configured, if you want to use haier climate without WiFi add {CONF_WIFI_SIGNAL}: false to climate configuration"
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
cg.add(haier_ns.init_haier_protocol_logging())
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await climate.register_climate(var, config)
await uart.register_uart_device(var, config)
await climate.register_climate(var, config)
if (CONF_WIFI_SIGNAL in config) and (config[CONF_WIFI_SIGNAL]):
cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL]))
if CONF_BEEPER in config:
cg.add(var.set_beeper_state(config[CONF_BEEPER]))
if CONF_OUTDOOR_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
cg.add(var.set_outdoor_temperature_sensor(sens))
if CONF_SUPPORTED_MODES in config:
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
if CONF_SUPPORTED_SWING_MODES in config:
cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
# https://github.com/paveldn/HaierProtocol
cg.add_library("pavlodn/HaierProtocol", "0.9.18")

View File

@@ -1,302 +0,0 @@
#include <cmath>
#include "haier.h"
#include "esphome/core/macros.h"
namespace esphome {
namespace haier {
static const char *const TAG = "haier";
static const uint8_t TEMPERATURE = 13;
static const uint8_t HUMIDITY = 15;
static const uint8_t MODE = 23;
static const uint8_t FAN_SPEED = 25;
static const uint8_t SWING = 27;
static const uint8_t POWER = 29;
static const uint8_t POWER_MASK = 1;
static const uint8_t SET_TEMPERATURE = 35;
static const uint8_t DECIMAL_MASK = (1 << 5);
static const uint8_t CRC = 36;
static const uint8_t COMFORT_PRESET_MASK = (1 << 3);
static const uint8_t MIN_VALID_TEMPERATURE = 16;
static const uint8_t MAX_VALID_TEMPERATURE = 50;
static const float TEMPERATURE_STEP = 0.5f;
static const uint8_t POLL_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 1, 90};
static const uint8_t OFF_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 3, 92};
void HaierClimate::dump_config() {
ESP_LOGCONFIG(TAG, "Haier:");
ESP_LOGCONFIG(TAG, " Update interval: %u", this->get_update_interval());
this->dump_traits_(TAG);
this->check_uart_settings(9600);
}
void HaierClimate::loop() {
if (this->available() >= sizeof(this->data_)) {
this->read_array(this->data_, sizeof(this->data_));
if (this->data_[0] != 255 || this->data_[1] != 255)
return;
read_state_(this->data_, sizeof(this->data_));
}
}
void HaierClimate::update() {
this->write_array(POLL_REQ, sizeof(POLL_REQ));
dump_message_("Poll sent", POLL_REQ, sizeof(POLL_REQ));
}
climate::ClimateTraits HaierClimate::traits() {
auto traits = climate::ClimateTraits();
traits.set_visual_min_temperature(MIN_VALID_TEMPERATURE);
traits.set_visual_max_temperature(MAX_VALID_TEMPERATURE);
traits.set_visual_temperature_step(TEMPERATURE_STEP);
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL, climate::CLIMATE_MODE_COOL,
climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY});
traits.set_supported_fan_modes({
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH,
});
traits.set_supported_swing_modes(this->supported_swing_modes_);
traits.set_supports_current_temperature(true);
traits.set_supports_two_point_target_temperature(false);
traits.add_supported_preset(climate::CLIMATE_PRESET_NONE);
traits.add_supported_preset(climate::CLIMATE_PRESET_COMFORT);
return traits;
}
void HaierClimate::read_state_(const uint8_t *data, uint8_t size) {
dump_message_("Received state", data, size);
uint8_t check = data[CRC];
uint8_t crc = get_checksum_(data, size);
if (check != crc) {
ESP_LOGW(TAG, "Invalid checksum");
return;
}
this->current_temperature = data[TEMPERATURE];
this->target_temperature = data[SET_TEMPERATURE] + MIN_VALID_TEMPERATURE;
if (data[POWER] & DECIMAL_MASK) {
this->target_temperature += 0.5f;
}
switch (data[MODE]) {
case MODE_SMART:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
case MODE_COOL:
this->mode = climate::CLIMATE_MODE_COOL;
break;
case MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
case MODE_ONLY_FAN:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
case MODE_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
break;
default: // other modes are unsupported
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
}
switch (data[FAN_SPEED]) {
case FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
case FAN_MIN:
this->fan_mode = climate::CLIMATE_FAN_LOW;
break;
case FAN_MIDDLE:
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
break;
case FAN_MAX:
this->fan_mode = climate::CLIMATE_FAN_HIGH;
break;
}
switch (data[SWING]) {
case SWING_OFF:
this->swing_mode = climate::CLIMATE_SWING_OFF;
break;
case SWING_VERTICAL:
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
break;
case SWING_HORIZONTAL:
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
break;
case SWING_BOTH:
this->swing_mode = climate::CLIMATE_SWING_BOTH;
break;
}
if (data[POWER] & COMFORT_PRESET_MASK) {
this->preset = climate::CLIMATE_PRESET_COMFORT;
} else {
this->preset = climate::CLIMATE_PRESET_NONE;
}
if ((data[POWER] & POWER_MASK) == 0) {
this->mode = climate::CLIMATE_MODE_OFF;
}
this->publish_state();
}
void HaierClimate::control(const climate::ClimateCall &call) {
if (call.get_mode().has_value()) {
switch (call.get_mode().value()) {
case climate::CLIMATE_MODE_OFF:
send_data_(OFF_REQ, sizeof(OFF_REQ));
break;
case climate::CLIMATE_MODE_HEAT_COOL:
case climate::CLIMATE_MODE_AUTO:
data_[POWER] |= POWER_MASK;
data_[MODE] = MODE_SMART;
break;
case climate::CLIMATE_MODE_HEAT:
data_[POWER] |= POWER_MASK;
data_[MODE] = MODE_HEAT;
break;
case climate::CLIMATE_MODE_COOL:
data_[POWER] |= POWER_MASK;
data_[MODE] = MODE_COOL;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
data_[POWER] |= POWER_MASK;
data_[MODE] = MODE_ONLY_FAN;
break;
case climate::CLIMATE_MODE_DRY:
data_[POWER] |= POWER_MASK;
data_[MODE] = MODE_DRY;
break;
}
}
if (call.get_preset().has_value()) {
if (call.get_preset().value() == climate::CLIMATE_PRESET_COMFORT) {
data_[POWER] |= COMFORT_PRESET_MASK;
} else {
data_[POWER] &= ~COMFORT_PRESET_MASK;
}
}
if (call.get_target_temperature().has_value()) {
float target = call.get_target_temperature().value() - MIN_VALID_TEMPERATURE;
data_[SET_TEMPERATURE] = (uint8_t) target;
if ((int) target == std::lroundf(target)) {
data_[POWER] &= ~DECIMAL_MASK;
} else {
data_[POWER] |= DECIMAL_MASK;
}
}
if (call.get_fan_mode().has_value()) {
switch (call.get_fan_mode().value()) {
case climate::CLIMATE_FAN_AUTO:
data_[FAN_SPEED] = FAN_AUTO;
break;
case climate::CLIMATE_FAN_LOW:
data_[FAN_SPEED] = FAN_MIN;
break;
case climate::CLIMATE_FAN_MEDIUM:
data_[FAN_SPEED] = FAN_MIDDLE;
break;
case climate::CLIMATE_FAN_HIGH:
data_[FAN_SPEED] = FAN_MAX;
break;
default: // other modes are unsupported
break;
}
}
if (call.get_swing_mode().has_value()) {
switch (call.get_swing_mode().value()) {
case climate::CLIMATE_SWING_OFF:
data_[SWING] = SWING_OFF;
break;
case climate::CLIMATE_SWING_VERTICAL:
data_[SWING] = SWING_VERTICAL;
break;
case climate::CLIMATE_SWING_HORIZONTAL:
data_[SWING] = SWING_HORIZONTAL;
break;
case climate::CLIMATE_SWING_BOTH:
data_[SWING] = SWING_BOTH;
break;
}
}
// Parts of the message that must have specific values for "send" command.
// The meaning of those values is unknown at the moment.
data_[9] = 1;
data_[10] = 77;
data_[11] = 95;
data_[17] = 0;
// Compute checksum
uint8_t crc = get_checksum_(data_, sizeof(data_));
data_[CRC] = crc;
send_data_(data_, sizeof(data_));
}
void HaierClimate::send_data_(const uint8_t *message, uint8_t size) {
this->write_array(message, size);
dump_message_("Sent message", message, size);
}
void HaierClimate::dump_message_(const char *title, const uint8_t *message, uint8_t size) {
ESP_LOGV(TAG, "%s:", title);
for (int i = 0; i < size; i++) {
ESP_LOGV(TAG, " byte %02d - %d", i, message[i]);
}
}
uint8_t HaierClimate::get_checksum_(const uint8_t *message, size_t size) {
uint8_t position = size - 1;
uint8_t crc = 0;
for (int i = 2; i < position; i++)
crc += message[i];
return crc;
}
} // namespace haier
} // namespace esphome

View File

@@ -1,37 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace haier {
enum Mode : uint8_t { MODE_SMART = 0, MODE_COOL = 1, MODE_HEAT = 2, MODE_ONLY_FAN = 3, MODE_DRY = 4 };
enum FanSpeed : uint8_t { FAN_MAX = 0, FAN_MIDDLE = 1, FAN_MIN = 2, FAN_AUTO = 3 };
enum SwingMode : uint8_t { SWING_OFF = 0, SWING_VERTICAL = 1, SWING_HORIZONTAL = 2, SWING_BOTH = 3 };
class HaierClimate : public climate::Climate, public uart::UARTDevice, public PollingComponent {
public:
void loop() override;
void update() override;
void dump_config() override;
void control(const climate::ClimateCall &call) override;
void set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
this->supported_swing_modes_ = modes;
}
protected:
climate::ClimateTraits traits() override;
void read_state_(const uint8_t *data, uint8_t size);
void send_data_(const uint8_t *message, uint8_t size);
void dump_message_(const char *title, const uint8_t *message, uint8_t size);
uint8_t get_checksum_(const uint8_t *message, size_t size);
private:
uint8_t data_[37];
std::set<climate::ClimateSwingMode> supported_swing_modes_{};
};
} // namespace haier
} // namespace esphome

View File

@@ -0,0 +1,311 @@
#include <chrono>
#include <string>
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#include "haier_base.h"
using namespace esphome::climate;
using namespace esphome::uart;
namespace esphome {
namespace haier {
static const char *const TAG = "haier.climate";
constexpr size_t COMMUNICATION_TIMEOUT_MS = 60000;
constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000;
constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000;
constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000;
constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400;
constexpr size_t CONTROL_TIMEOUT_MS = 7000;
constexpr size_t NO_COMMAND = 0xFF; // Indicate that there is no command supplied
#if (HAIER_LOG_LEVEL > 4)
// To reduce size of binary this function only available when log level is Verbose
const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
static const char *phase_names[] = {
"SENDING_INIT_1",
"WAITING_ANSWER_INIT_1",
"SENDING_INIT_2",
"WAITING_ANSWER_INIT_2",
"SENDING_FIRST_STATUS_REQUEST",
"WAITING_FIRST_STATUS_ANSWER",
"SENDING_ALARM_STATUS_REQUEST",
"WAITING_ALARM_STATUS_ANSWER",
"IDLE",
"SENDING_STATUS_REQUEST",
"WAITING_STATUS_ANSWER",
"SENDING_UPDATE_SIGNAL_REQUEST",
"WAITING_UPDATE_SIGNAL_ANSWER",
"SENDING_SIGNAL_LEVEL",
"WAITING_SIGNAL_LEVEL_ANSWER",
"SENDING_CONTROL",
"WAITING_CONTROL_ANSWER",
"SENDING_POWER_ON_COMMAND",
"WAITING_POWER_ON_ANSWER",
"SENDING_POWER_OFF_COMMAND",
"WAITING_POWER_OFF_ANSWER",
"UNKNOWN" // Should be the last!
};
int phase_index = (int) phase;
if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0))
phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES;
return phase_names[phase_index];
}
#endif
HaierClimateBase::HaierClimateBase()
: haier_protocol_(*this),
protocol_phase_(ProtocolPhases::SENDING_INIT_1),
action_request_(ActionRequest::NO_ACTION),
display_status_(true),
health_mode_(false),
force_send_control_(false),
forced_publish_(false),
forced_request_status_(false),
first_control_attempt_(false),
reset_protocol_request_(false) {
this->traits_ = climate::ClimateTraits();
this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY,
climate::CLIMATE_MODE_AUTO});
this->traits_.set_supported_fan_modes(
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
this->traits_.set_supports_current_temperature(true);
}
HaierClimateBase::~HaierClimateBase() {}
void HaierClimateBase::set_phase_(ProtocolPhases phase) {
if (this->protocol_phase_ != phase) {
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase));
#else
ESP_LOGV(TAG, "Phase transition: %d => %d", (int) this->protocol_phase_, (int) phase);
#endif
this->protocol_phase_ = phase;
}
}
bool HaierClimateBase::check_timeout_(std::chrono::steady_clock::time_point now,
std::chrono::steady_clock::time_point tpoint, size_t timeout) {
return std::chrono::duration_cast<std::chrono::milliseconds>(now - tpoint).count() > timeout;
}
bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS);
}
bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS);
}
bool HaierClimateBase::is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->control_request_timestamp_, CONTROL_TIMEOUT_MS);
}
bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS);
}
bool HaierClimateBase::is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL);
}
bool HaierClimateBase::get_display_state() const { return this->display_status_; }
void HaierClimateBase::set_display_state(bool state) {
if (this->display_status_ != state) {
this->display_status_ = state;
this->set_force_send_control_(true);
}
}
bool HaierClimateBase::get_health_mode() const { return this->health_mode_; }
void HaierClimateBase::set_health_mode(bool state) {
if (this->health_mode_ != state) {
this->health_mode_ = state;
this->set_force_send_control_(true);
}
}
void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; }
void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; }
void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; }
void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
this->traits_.set_supported_swing_modes(modes);
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); // Always available
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); // Always available
}
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) {
this->traits_.set_supported_modes(modes);
this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available
this->traits_.add_supported_mode(climate::CLIMATE_MODE_AUTO); // Always available
}
haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type,
uint8_t expected_request_message_type,
uint8_t answer_message_type,
uint8_t expected_answer_message_type,
ProtocolPhases expected_phase) {
haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK;
if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type))
result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type))
result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_))
result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
if (is_message_invalid(answer_message_type))
result = haier_protocol::HandlerError::INVALID_ANSWER;
return result;
}
haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) {
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_));
#else
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_);
#endif
if (this->protocol_phase_ > ProtocolPhases::IDLE) {
this->set_phase_(ProtocolPhases::IDLE);
} else {
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
}
return haier_protocol::HandlerError::HANDLER_OK;
}
void HaierClimateBase::setup() {
ESP_LOGI(TAG, "Haier initialization...");
// Set timestamp here to give AC time to boot
this->last_request_timestamp_ = std::chrono::steady_clock::now();
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
this->set_answers_handlers();
this->haier_protocol_.set_default_timeout_handler(
std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1));
}
void HaierClimateBase::dump_config() {
LOG_CLIMATE("", "Haier Climate", this);
ESP_LOGCONFIG(TAG, " Device communication status: %s",
(this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none");
}
void HaierClimateBase::loop() {
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
if ((std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_valid_status_timestamp_).count() >
COMMUNICATION_TIMEOUT_MS) ||
(this->reset_protocol_request_)) {
if (this->protocol_phase_ >= ProtocolPhases::IDLE) {
// No status too long, reseting protocol
if (this->reset_protocol_request_) {
this->reset_protocol_request_ = false;
ESP_LOGW(TAG, "Protocol reset requested");
} else {
ESP_LOGW(TAG, "Communication timeout, reseting protocol");
}
this->last_valid_status_timestamp_ = now;
this->set_force_send_control_(false);
if (this->hvac_settings_.valid)
this->hvac_settings_.reset();
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
return;
} else {
// No need to reset protocol if we didn't pass initialization phase
this->last_valid_status_timestamp_ = now;
}
};
if ((this->protocol_phase_ == ProtocolPhases::IDLE) ||
(this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) ||
(this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) ||
(this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL)) {
// If control message or action is pending we should send it ASAP unless we are in initialisation
// procedure or waiting for an answer
if (this->action_request_ != ActionRequest::NO_ACTION) {
this->process_pending_action();
} else if (this->hvac_settings_.valid || this->force_send_control_) {
ESP_LOGV(TAG, "Control packet is pending...");
this->set_phase_(ProtocolPhases::SENDING_CONTROL);
}
}
this->process_phase(now);
this->haier_protocol_.loop();
}
void HaierClimateBase::process_pending_action() {
ActionRequest request = this->action_request_;
if (this->action_request_ == ActionRequest::TOGGLE_POWER) {
request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF;
}
switch (request) {
case ActionRequest::TURN_POWER_ON:
this->set_phase_(ProtocolPhases::SENDING_POWER_ON_COMMAND);
break;
case ActionRequest::TURN_POWER_OFF:
this->set_phase_(ProtocolPhases::SENDING_POWER_OFF_COMMAND);
break;
case ActionRequest::TOGGLE_POWER:
case ActionRequest::NO_ACTION:
// shouldn't get here, do nothing
break;
default:
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_);
break;
}
this->action_request_ = ActionRequest::NO_ACTION;
}
ClimateTraits HaierClimateBase::traits() { return traits_; }
void HaierClimateBase::control(const ClimateCall &call) {
ESP_LOGD("Control", "Control call");
if (this->protocol_phase_ < ProtocolPhases::IDLE) {
ESP_LOGW(TAG, "Can't send control packet, first poll answer not received");
return; // cancel the control, we cant do it without a poll answer.
}
if (this->hvac_settings_.valid) {
ESP_LOGW(TAG, "Overriding old valid settings before they were applied!");
}
{
if (call.get_mode().has_value())
this->hvac_settings_.mode = call.get_mode();
if (call.get_fan_mode().has_value())
this->hvac_settings_.fan_mode = call.get_fan_mode();
if (call.get_swing_mode().has_value())
this->hvac_settings_.swing_mode = call.get_swing_mode();
if (call.get_target_temperature().has_value())
this->hvac_settings_.target_temperature = call.get_target_temperature();
if (call.get_preset().has_value())
this->hvac_settings_.preset = call.get_preset();
this->hvac_settings_.valid = true;
}
this->first_control_attempt_ = true;
}
void HaierClimateBase::HvacSettings::reset() {
this->valid = false;
this->mode.reset();
this->fan_mode.reset();
this->swing_mode.reset();
this->target_temperature.reset();
this->preset.reset();
}
void HaierClimateBase::set_force_send_control_(bool status) {
this->force_send_control_ = status;
if (status) {
this->first_control_attempt_ = true;
}
}
void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) {
this->haier_protocol_.send_message(command, use_crc);
this->last_request_timestamp_ = std::chrono::steady_clock::now();
}
} // namespace haier
} // namespace esphome

View File

@@ -0,0 +1,142 @@
#pragma once
#include <chrono>
#include <set>
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
// HaierProtocol
#include <protocol/haier_protocol.h>
namespace esphome {
namespace haier {
enum class ActionRequest : uint8_t {
NO_ACTION = 0,
TURN_POWER_ON = 1,
TURN_POWER_OFF = 2,
TOGGLE_POWER = 3,
START_SELF_CLEAN = 4, // only hOn
START_STERI_CLEAN = 5, // only hOn
};
class HaierClimateBase : public esphome::Component,
public esphome::climate::Climate,
public esphome::uart::UARTDevice,
public haier_protocol::ProtocolStream {
public:
HaierClimateBase();
HaierClimateBase(const HaierClimateBase &) = delete;
HaierClimateBase &operator=(const HaierClimateBase &) = delete;
~HaierClimateBase();
void setup() override;
void loop() override;
void control(const esphome::climate::ClimateCall &call) override;
void dump_config() override;
float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; }
void set_fahrenheit(bool fahrenheit);
void set_display_state(bool state);
bool get_display_state() const;
void set_health_mode(bool state);
bool get_health_mode() const;
void send_power_on_command();
void send_power_off_command();
void toggle_power();
void reset_protocol() { this->reset_protocol_request_ = true; };
void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes);
void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes);
size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
size_t read_array(uint8_t *data, size_t len) noexcept override {
return esphome::uart::UARTDevice::read_array(data, len) ? len : 0;
};
void write_array(const uint8_t *data, size_t len) noexcept override {
esphome::uart::UARTDevice::write_array(data, len);
};
bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; };
protected:
enum class ProtocolPhases {
UNKNOWN = -1,
// INITIALIZATION
SENDING_INIT_1 = 0,
WAITING_ANSWER_INIT_1 = 1,
SENDING_INIT_2 = 2,
WAITING_ANSWER_INIT_2 = 3,
SENDING_FIRST_STATUS_REQUEST = 4,
WAITING_FIRST_STATUS_ANSWER = 5,
SENDING_ALARM_STATUS_REQUEST = 6,
WAITING_ALARM_STATUS_ANSWER = 7,
// FUNCTIONAL STATE
IDLE = 8,
SENDING_STATUS_REQUEST = 9,
WAITING_STATUS_ANSWER = 10,
SENDING_UPDATE_SIGNAL_REQUEST = 11,
WAITING_UPDATE_SIGNAL_ANSWER = 12,
SENDING_SIGNAL_LEVEL = 13,
WAITING_SIGNAL_LEVEL_ANSWER = 14,
SENDING_CONTROL = 15,
WAITING_CONTROL_ANSWER = 16,
SENDING_POWER_ON_COMMAND = 17,
WAITING_POWER_ON_ANSWER = 18,
SENDING_POWER_OFF_COMMAND = 19,
WAITING_POWER_OFF_ANSWER = 20,
NUM_PROTOCOL_PHASES
};
#if (HAIER_LOG_LEVEL > 4)
const char *phase_to_string_(ProtocolPhases phase);
#endif
virtual void set_answers_handlers() = 0;
virtual void process_phase(std::chrono::steady_clock::time_point now) = 0;
virtual haier_protocol::HaierMessage get_control_message() = 0;
virtual bool is_message_invalid(uint8_t message_type) = 0;
virtual void process_pending_action();
esphome::climate::ClimateTraits traits() override;
// Answers handlers
haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type,
uint8_t answer_message_type, uint8_t expected_answer_message_type,
ProtocolPhases expected_phase);
// Timeout handler
haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type);
// Helper functions
void set_force_send_control_(bool status);
void send_message_(const haier_protocol::HaierMessage &command, bool use_crc);
void set_phase_(ProtocolPhases phase);
bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint,
size_t timeout);
bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now);
bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now);
bool is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now);
bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now);
bool is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now);
struct HvacSettings {
esphome::optional<esphome::climate::ClimateMode> mode;
esphome::optional<esphome::climate::ClimateFanMode> fan_mode;
esphome::optional<esphome::climate::ClimateSwingMode> swing_mode;
esphome::optional<float> target_temperature;
esphome::optional<esphome::climate::ClimatePreset> preset;
bool valid;
HvacSettings() : valid(false){};
void reset();
};
haier_protocol::ProtocolHandler haier_protocol_;
ProtocolPhases protocol_phase_;
ActionRequest action_request_;
uint8_t fan_mode_speed_;
uint8_t other_modes_fan_speed_;
bool display_status_;
bool health_mode_;
bool force_send_control_;
bool forced_publish_;
bool forced_request_status_;
bool first_control_attempt_;
bool reset_protocol_request_;
esphome::climate::ClimateTraits traits_;
HvacSettings hvac_settings_;
std::chrono::steady_clock::time_point last_request_timestamp_; // For interval between messages
std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout
std::chrono::steady_clock::time_point last_status_request_; // To request AC status
std::chrono::steady_clock::time_point control_request_timestamp_; // To send control message
};
} // namespace haier
} // namespace esphome

View File

@@ -0,0 +1,857 @@
#include <chrono>
#include <string>
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#ifdef USE_WIFI
#include "esphome/components/wifi/wifi_component.h"
#endif
#include "hon_climate.h"
#include "hon_packet.h"
using namespace esphome::climate;
using namespace esphome::uart;
namespace esphome {
namespace haier {
static const char *const TAG = "haier.climate";
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) {
switch (direction) {
case AirflowVerticalDirection::HEALTH_UP:
return hon_protocol::VerticalSwingMode::HEALTH_UP;
case AirflowVerticalDirection::MAX_UP:
return hon_protocol::VerticalSwingMode::MAX_UP;
case AirflowVerticalDirection::UP:
return hon_protocol::VerticalSwingMode::UP;
case AirflowVerticalDirection::DOWN:
return hon_protocol::VerticalSwingMode::DOWN;
case AirflowVerticalDirection::HEALTH_DOWN:
return hon_protocol::VerticalSwingMode::HEALTH_DOWN;
default:
return hon_protocol::VerticalSwingMode::CENTER;
}
}
hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDirection direction) {
switch (direction) {
case AirflowHorizontalDirection::MAX_LEFT:
return hon_protocol::HorizontalSwingMode::MAX_LEFT;
case AirflowHorizontalDirection::LEFT:
return hon_protocol::HorizontalSwingMode::LEFT;
case AirflowHorizontalDirection::RIGHT:
return hon_protocol::HorizontalSwingMode::RIGHT;
case AirflowHorizontalDirection::MAX_RIGHT:
return hon_protocol::HorizontalSwingMode::MAX_RIGHT;
default:
return hon_protocol::HorizontalSwingMode::CENTER;
}
}
HonClimate::HonClimate()
: last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]),
cleaning_status_(CleaningState::NO_CLEANING),
got_valid_outdoor_temp_(false),
hvac_hardware_info_available_(false),
hvac_functions_{false, false, false, false, false},
use_crc_(hvac_functions_[2]),
active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
outdoor_sensor_(nullptr),
send_wifi_signal_(true) {
this->traits_.set_supported_presets({
climate::CLIMATE_PRESET_NONE,
climate::CLIMATE_PRESET_ECO,
climate::CLIMATE_PRESET_BOOST,
climate::CLIMATE_PRESET_SLEEP,
});
this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID;
this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
}
HonClimate::~HonClimate() {}
void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; }
bool HonClimate::get_beeper_state() const { return this->beeper_status_; }
void HonClimate::set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; }
AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this->vertical_direction_; };
void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) {
if (direction > AirflowVerticalDirection::DOWN) {
this->vertical_direction_ = AirflowVerticalDirection::CENTER;
} else {
this->vertical_direction_ = direction;
}
this->set_force_send_control_(true);
}
AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; }
void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) {
if (direction > AirflowHorizontalDirection::RIGHT) {
this->horizontal_direction_ = AirflowHorizontalDirection::CENTER;
} else {
this->horizontal_direction_ = direction;
}
this->set_force_send_control_(true);
}
std::string HonClimate::get_cleaning_status_text() const {
switch (this->cleaning_status_) {
case CleaningState::SELF_CLEAN:
return "Self clean";
case CleaningState::STERI_CLEAN:
return "56°C Steri-Clean";
default:
return "No cleaning";
}
}
CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_status_; }
void HonClimate::start_self_cleaning() {
if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
ESP_LOGI(TAG, "Sending self cleaning start request");
this->action_request_ = ActionRequest::START_SELF_CLEAN;
this->set_force_send_control_(true);
}
}
void HonClimate::start_steri_cleaning() {
if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
ESP_LOGI(TAG, "Sending steri cleaning start request");
this->action_request_ = ActionRequest::START_STERI_CLEAN;
this->set_force_send_control_(true);
}
}
void HonClimate::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; }
haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type,
const uint8_t *data, size_t data_size) {
haier_protocol::HandlerError result = this->answer_preprocess_(
request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type,
(uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_1);
if (result == haier_protocol::HandlerError::HANDLER_OK) {
if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) {
// Wrong structure
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
}
// All OK
hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data;
char tmp[9];
tmp[8] = 0;
strncpy(tmp, answr->protocol_version, 8);
this->hvac_protocol_version_ = std::string(tmp);
strncpy(tmp, answr->software_version, 8);
this->hvac_software_version_ = std::string(tmp);
strncpy(tmp, answr->hardware_version, 8);
this->hvac_hardware_version_ = std::string(tmp);
strncpy(tmp, answr->device_name, 8);
this->hvac_device_name_ = std::string(tmp);
this->hvac_functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support
this->hvac_functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support
this->hvac_functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support
this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support
this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support
this->hvac_hardware_info_available_ = true;
this->set_phase_(ProtocolPhases::SENDING_INIT_2);
return result;
} else {
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
: ProtocolPhases::SENDING_INIT_1);
return result;
}
}
haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type,
const uint8_t *data, size_t data_size) {
haier_protocol::HandlerError result = this->answer_preprocess_(
request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type,
(uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_2);
if (result == haier_protocol::HandlerError::HANDLER_OK) {
this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
return result;
} else {
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
: ProtocolPhases::SENDING_INIT_1);
return result;
}
}
haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type,
const uint8_t *data, size_t data_size) {
haier_protocol::HandlerError result =
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type,
(uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
if (result == haier_protocol::HandlerError::HANDLER_OK) {
result = this->process_status_message_(data, data_size);
if (result != haier_protocol::HandlerError::HANDLER_OK) {
ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
: ProtocolPhases::SENDING_INIT_1);
} else {
if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) {
memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl));
} else {
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
sizeof(hon_protocol::HaierPacketControl));
}
if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) {
ESP_LOGI(TAG, "First HVAC status received");
this->set_phase_(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
} else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) ||
(this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) ||
(this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) {
this->set_phase_(ProtocolPhases::IDLE);
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) {
this->set_phase_(ProtocolPhases::IDLE);
this->set_force_send_control_(false);
if (this->hvac_settings_.valid)
this->hvac_settings_.reset();
}
}
return result;
} else {
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
: ProtocolPhases::SENDING_INIT_1);
return result;
}
}
haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type,
uint8_t message_type,
const uint8_t *data,
size_t data_size) {
haier_protocol::HandlerError result =
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION,
message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE,
ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
if (result == haier_protocol::HandlerError::HANDLER_OK) {
this->set_phase_(ProtocolPhases::SENDING_SIGNAL_LEVEL);
return result;
} else {
this->set_phase_(ProtocolPhases::IDLE);
return result;
}
}
haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(uint8_t request_type,
uint8_t message_type,
const uint8_t *data, size_t data_size) {
haier_protocol::HandlerError result =
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, message_type,
(uint8_t) hon_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
this->set_phase_(ProtocolPhases::IDLE);
return result;
}
haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type,
const uint8_t *data, size_t data_size) {
if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) {
if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) {
// Unexpected answer to request
this->set_phase_(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
}
if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) {
// Don't expect this answer now
this->set_phase_(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
}
memcpy(this->active_alarms_, data + 2, 8);
this->set_phase_(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::HANDLER_OK;
} else {
this->set_phase_(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
}
}
void HonClimate::set_answers_handlers() {
// Set handlers
this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION),
std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::GET_DEVICE_ID),
std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::CONTROL),
std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3,
std::placeholders::_4));
this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION),
std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::GET_ALARM_STATUS),
std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::REPORT_NETWORK_STATUS),
std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
}
void HonClimate::dump_config() {
HaierClimateBase::dump_config();
ESP_LOGCONFIG(TAG, " Protocol version: hOn");
if (this->hvac_hardware_info_available_) {
ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_protocol_version_.c_str());
ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_software_version_.c_str());
ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_version_.c_str());
ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_device_name_.c_str());
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""),
(this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""),
(this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : ""));
ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
}
}
void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_INIT_1:
if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) {
this->hvac_hardware_info_available_ = false;
// Indicate device capabilities:
// bit 0 - if 1 module support interactive mode
// bit 1 - if 1 module support controller-device mode
// bit 2 - if 1 module support crc
// bit 3 - if 1 module support multiple devices
// bit 4..bit 15 - not used
uint8_t module_capabilities[2] = {0b00000000, 0b00000111};
static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST(
(uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities));
this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_);
this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_1);
}
break;
case ProtocolPhases::SENDING_INIT_2:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage DEVICEID_REQUEST((uint8_t) hon_protocol::FrameType::GET_DEVICE_ID);
this->send_message_(DEVICEID_REQUEST, this->use_crc_);
this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_2);
}
break;
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
case ProtocolPhases::SENDING_STATUS_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage STATUS_REQUEST(
(uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcomandsControl::GET_USER_DATA);
this->send_message_(STATUS_REQUEST, this->use_crc_);
this->last_status_request_ = now;
this->set_phase_((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1));
}
break;
#ifdef USE_WIFI
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST(
(uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION);
this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_);
this->last_signal_request_ = now;
this->set_phase_(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
}
break;
case ProtocolPhases::SENDING_SIGNAL_LEVEL:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00};
if (wifi::global_wifi_component->is_connected()) {
wifi_status_data[1] = 0;
int8_t rssi = wifi::global_wifi_component->wifi_rssi();
wifi_status_data[3] = uint8_t((128 + rssi) / 1.28f);
ESP_LOGD(TAG, "WiFi signal is: %ddBm => %d%%", rssi, wifi_status_data[3]);
} else {
ESP_LOGD(TAG, "WiFi is not connected");
wifi_status_data[1] = 1;
wifi_status_data[3] = 0;
}
haier_protocol::HaierMessage wifi_status_request((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS,
wifi_status_data, sizeof(wifi_status_data));
this->send_message_(wifi_status_request, this->use_crc_);
this->set_phase_(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
}
break;
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
break;
#else
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
case ProtocolPhases::SENDING_SIGNAL_LEVEL:
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
this->set_phase_(ProtocolPhases::IDLE);
break;
#endif
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(
(uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS);
this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_);
this->set_phase_(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER);
}
break;
case ProtocolPhases::SENDING_CONTROL:
if (this->first_control_attempt_) {
this->control_request_timestamp_ = now;
this->first_control_attempt_ = false;
}
if (this->is_control_message_timeout_exceeded_(now)) {
ESP_LOGW(TAG, "Sending control packet timeout!");
this->set_force_send_control_(false);
if (this->hvac_settings_.valid)
this->hvac_settings_.reset();
this->forced_request_status_ = true;
this->forced_publish_ = true;
this->set_phase_(ProtocolPhases::IDLE);
} else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
haier_protocol::HaierMessage control_message = get_control_message();
this->send_message_(control_message, this->use_crc_);
ESP_LOGI(TAG, "Control packet sent");
this->set_phase_(ProtocolPhases::WAITING_CONTROL_ANSWER);
}
break;
case ProtocolPhases::SENDING_POWER_ON_COMMAND:
case ProtocolPhases::SENDING_POWER_OFF_COMMAND:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
uint8_t pwr_cmd_buf[2] = {0x00, 0x00};
if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND)
pwr_cmd_buf[1] = 0x01;
haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL,
((uint16_t) hon_protocol::SubcomandsControl::SET_SINGLE_PARAMETER) + 1,
pwr_cmd_buf, sizeof(pwr_cmd_buf));
this->send_message_(power_cmd, this->use_crc_);
this->set_phase_(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND
? ProtocolPhases::WAITING_POWER_ON_ANSWER
: ProtocolPhases::WAITING_POWER_OFF_ANSWER);
}
break;
case ProtocolPhases::WAITING_ANSWER_INIT_1:
case ProtocolPhases::WAITING_ANSWER_INIT_2:
case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER:
case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER:
case ProtocolPhases::WAITING_STATUS_ANSWER:
case ProtocolPhases::WAITING_CONTROL_ANSWER:
case ProtocolPhases::WAITING_POWER_ON_ANSWER:
case ProtocolPhases::WAITING_POWER_OFF_ANSWER:
break;
case ProtocolPhases::IDLE: {
if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST);
this->forced_request_status_ = false;
}
#ifdef USE_WIFI
else if (this->send_wifi_signal_ &&
(std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
SIGNAL_LEVEL_UPDATE_INTERVAL_MS))
this->set_phase_(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
#endif
} break;
default:
// Shouldn't get here
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
#else
ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_);
#endif
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
break;
}
}
haier_protocol::HaierMessage HonClimate::get_control_message() {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
bool has_hvac_settings = false;
if (this->hvac_settings_.valid) {
has_hvac_settings = true;
HvacSettings climate_control;
climate_control = this->hvac_settings_;
if (climate_control.mode.has_value()) {
switch (climate_control.mode.value()) {
case CLIMATE_MODE_OFF:
out_data->ac_power = 0;
break;
case CLIMATE_MODE_AUTO:
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::AUTO;
out_data->fan_mode = this->other_modes_fan_speed_;
break;
case CLIMATE_MODE_HEAT:
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::HEAT;
out_data->fan_mode = this->other_modes_fan_speed_;
break;
case CLIMATE_MODE_DRY:
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
out_data->fan_mode = this->other_modes_fan_speed_;
break;
case CLIMATE_MODE_FAN_ONLY:
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::FAN;
out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode
// Disabling boost and eco mode for Fan only
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
break;
case CLIMATE_MODE_COOL:
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::COOL;
out_data->fan_mode = this->other_modes_fan_speed_;
break;
default:
ESP_LOGE("Control", "Unsupported climate mode");
break;
}
}
// Set fan speed, if we are in fan mode, reject auto in fan mode
if (climate_control.fan_mode.has_value()) {
switch (climate_control.fan_mode.value()) {
case CLIMATE_FAN_LOW:
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_LOW;
break;
case CLIMATE_FAN_MEDIUM:
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_MID;
break;
case CLIMATE_FAN_HIGH:
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_HIGH;
break;
case CLIMATE_FAN_AUTO:
if (mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
break;
default:
ESP_LOGE("Control", "Unsupported fan mode");
break;
}
}
// Set swing mode
if (climate_control.swing_mode.has_value()) {
switch (climate_control.swing_mode.value()) {
case CLIMATE_SWING_OFF:
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
break;
case CLIMATE_SWING_VERTICAL:
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO;
break;
case CLIMATE_SWING_HORIZONTAL:
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO;
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
break;
case CLIMATE_SWING_BOTH:
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO;
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO;
break;
}
}
if (climate_control.target_temperature.has_value()) {
out_data->set_point =
climate_control.target_temperature.value() - 16; // set the temperature at our offset, subtract 16.
}
if (out_data->ac_power == 0) {
// If AC is off - no presets alowed
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
} else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
break;
case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode
out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
break;
case CLIMATE_PRESET_BOOST:
out_data->quiet_mode = 0;
// Boost is not supported in Fan only mode
out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->sleep_mode = 0;
break;
case CLIMATE_PRESET_AWAY:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
break;
case CLIMATE_PRESET_SLEEP:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 1;
break;
default:
ESP_LOGE("Control", "Unsupported preset");
break;
}
}
} else {
if (out_data->vertical_swing_mode != (uint8_t) hon_protocol::VerticalSwingMode::AUTO)
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
if (out_data->horizontal_swing_mode != (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
}
out_data->beeper_status = ((!this->beeper_status_) || (!has_hvac_settings)) ? 1 : 0;
control_out_buffer[4] = 0; // This byte should be cleared before setting values
out_data->display_status = this->display_status_ ? 1 : 0;
out_data->health_mode = this->health_mode_ ? 1 : 0;
switch (this->action_request_) {
case ActionRequest::START_SELF_CLEAN:
this->action_request_ = ActionRequest::NO_ACTION;
out_data->self_cleaning_status = 1;
out_data->steri_clean = 0;
out_data->set_point = 0x06;
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
out_data->light_status = 0;
break;
case ActionRequest::START_STERI_CLEAN:
this->action_request_ = ActionRequest::NO_ACTION;
out_data->self_cleaning_status = 0;
out_data->steri_clean = 1;
out_data->set_point = 0x06;
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
out_data->light_status = 0;
break;
default:
// No change
break;
}
return haier_protocol::HaierMessage((uint8_t) hon_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcomandsControl::SET_GROUP_PARAMETERS,
control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
}
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
if (size < sizeof(hon_protocol::HaierStatus))
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
hon_protocol::HaierStatus packet;
if (size < sizeof(hon_protocol::HaierStatus))
size = sizeof(hon_protocol::HaierStatus);
memcpy(&packet, packet_buffer, size);
if (packet.sensors.error_status != 0) {
ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status);
}
if ((this->outdoor_sensor_ != nullptr) && (got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) {
got_valid_outdoor_temp_ = true;
float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET);
if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp))
this->outdoor_sensor_->publish_state(otemp);
}
bool should_publish = false;
{
// Extra modes/presets
optional<ClimatePreset> old_preset = this->preset;
if (packet.control.quiet_mode != 0) {
this->preset = CLIMATE_PRESET_ECO;
} else if (packet.control.fast_mode != 0) {
this->preset = CLIMATE_PRESET_BOOST;
} else if (packet.control.sleep_mode != 0) {
this->preset = CLIMATE_PRESET_SLEEP;
} else {
this->preset = CLIMATE_PRESET_NONE;
}
should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value());
}
{
// Target temperature
float old_target_temperature = this->target_temperature;
this->target_temperature = packet.control.set_point + 16.0f;
should_publish = should_publish || (old_target_temperature != this->target_temperature);
}
{
// Current temperature
float old_current_temperature = this->current_temperature;
this->current_temperature = packet.sensors.room_temperature / 2.0f;
should_publish = should_publish || (old_current_temperature != this->current_temperature);
}
{
// Fan mode
optional<ClimateFanMode> old_fan_mode = this->fan_mode;
// remember the fan speed we last had for climate vs fan
if (packet.control.ac_mode == (uint8_t) hon_protocol::ConditioningMode::FAN) {
if (packet.control.fan_mode != (uint8_t) hon_protocol::FanMode::FAN_AUTO)
this->fan_mode_speed_ = packet.control.fan_mode;
} else {
this->other_modes_fan_speed_ = packet.control.fan_mode;
}
switch (packet.control.fan_mode) {
case (uint8_t) hon_protocol::FanMode::FAN_AUTO:
if (packet.control.ac_mode != (uint8_t) hon_protocol::ConditioningMode::FAN) {
this->fan_mode = CLIMATE_FAN_AUTO;
} else {
// Shouldn't accept fan speed auto in fan-only mode even if AC reports it
ESP_LOGI(TAG, "Fan speed Auto is not supported in Fan only AC mode, ignoring");
}
break;
case (uint8_t) hon_protocol::FanMode::FAN_MID:
this->fan_mode = CLIMATE_FAN_MEDIUM;
break;
case (uint8_t) hon_protocol::FanMode::FAN_LOW:
this->fan_mode = CLIMATE_FAN_LOW;
break;
case (uint8_t) hon_protocol::FanMode::FAN_HIGH:
this->fan_mode = CLIMATE_FAN_HIGH;
break;
}
should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
}
{
// Display status
// should be before "Climate mode" because it is changing this->mode
if (packet.control.ac_power != 0) {
// if AC is off display status always ON so process it only when AC is on
bool disp_status = packet.control.display_status != 0;
if (disp_status != this->display_status_) {
// Do something only if display status changed
if (this->mode == CLIMATE_MODE_OFF) {
// AC just turned on from remote need to turn off display
this->set_force_send_control_(true);
} else {
this->display_status_ = disp_status;
}
}
}
}
{
// Health mode
bool old_health_mode = this->health_mode_;
this->health_mode_ = packet.control.health_mode == 1;
should_publish = should_publish || (old_health_mode != this->health_mode_);
}
{
CleaningState new_cleaning;
if (packet.control.steri_clean == 1) {
// Steri-cleaning
new_cleaning = CleaningState::STERI_CLEAN;
} else if (packet.control.self_cleaning_status == 1) {
// Self-cleaning
new_cleaning = CleaningState::SELF_CLEAN;
} else {
// No cleaning
new_cleaning = CleaningState::NO_CLEANING;
}
if (new_cleaning != this->cleaning_status_) {
ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning);
if (new_cleaning == CleaningState::NO_CLEANING) {
// Turnuin AC off after cleaning
this->action_request_ = ActionRequest::TURN_POWER_OFF;
}
this->cleaning_status_ = new_cleaning;
}
}
{
// Climate mode
ClimateMode old_mode = this->mode;
if (packet.control.ac_power == 0) {
this->mode = CLIMATE_MODE_OFF;
} else {
// Check current hvac mode
switch (packet.control.ac_mode) {
case (uint8_t) hon_protocol::ConditioningMode::COOL:
this->mode = CLIMATE_MODE_COOL;
break;
case (uint8_t) hon_protocol::ConditioningMode::HEAT:
this->mode = CLIMATE_MODE_HEAT;
break;
case (uint8_t) hon_protocol::ConditioningMode::DRY:
this->mode = CLIMATE_MODE_DRY;
break;
case (uint8_t) hon_protocol::ConditioningMode::FAN:
this->mode = CLIMATE_MODE_FAN_ONLY;
break;
case (uint8_t) hon_protocol::ConditioningMode::AUTO:
this->mode = CLIMATE_MODE_AUTO;
break;
}
}
should_publish = should_publish || (old_mode != this->mode);
}
{
// Swing mode
ClimateSwingMode old_swing_mode = this->swing_mode;
if (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) {
if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
this->swing_mode = CLIMATE_SWING_BOTH;
} else {
this->swing_mode = CLIMATE_SWING_HORIZONTAL;
}
} else {
if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
this->swing_mode = CLIMATE_SWING_VERTICAL;
} else {
this->swing_mode = CLIMATE_SWING_OFF;
}
}
should_publish = should_publish || (old_swing_mode != this->swing_mode);
}
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
if (this->forced_publish_ || should_publish) {
#if (HAIER_LOG_LEVEL > 4)
std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now();
#endif
this->publish_state();
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGV(TAG, "Publish delay: %lld ms",
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() -
_publish_start)
.count());
#endif
this->forced_publish_ = false;
}
if (should_publish) {
ESP_LOGI(TAG, "HVAC values changed");
}
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
"HVAC Mode = 0x%X", packet.control.ac_mode);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
"Fan speed Status = 0x%X", packet.control.fan_mode);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
"Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
"Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
"Set Point Status = 0x%X", packet.control.set_point);
return haier_protocol::HandlerError::HANDLER_OK;
}
bool HonClimate::is_message_invalid(uint8_t message_type) {
return message_type == (uint8_t) hon_protocol::FrameType::INVALID;
}
void HonClimate::process_pending_action() {
switch (this->action_request_) {
case ActionRequest::START_SELF_CLEAN:
case ActionRequest::START_STERI_CLEAN:
// Will reset action with control message sending
this->set_phase_(ProtocolPhases::SENDING_CONTROL);
break;
default:
HaierClimateBase::process_pending_action();
break;
}
}
} // namespace haier
} // namespace esphome

View File

@@ -0,0 +1,95 @@
#pragma once
#include <chrono>
#include "esphome/components/sensor/sensor.h"
#include "haier_base.h"
namespace esphome {
namespace haier {
enum class AirflowVerticalDirection : uint8_t {
HEALTH_UP = 0,
MAX_UP = 1,
UP = 2,
CENTER = 3,
DOWN = 4,
HEALTH_DOWN = 5,
};
enum class AirflowHorizontalDirection : uint8_t {
MAX_LEFT = 0,
LEFT = 1,
CENTER = 2,
RIGHT = 3,
MAX_RIGHT = 4,
};
enum class CleaningState : uint8_t {
NO_CLEANING = 0,
SELF_CLEAN = 1,
STERI_CLEAN = 2,
};
class HonClimate : public HaierClimateBase {
public:
HonClimate();
HonClimate(const HonClimate &) = delete;
HonClimate &operator=(const HonClimate &) = delete;
~HonClimate();
void dump_config() override;
void set_beeper_state(bool state);
bool get_beeper_state() const;
void set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor);
AirflowVerticalDirection get_vertical_airflow() const;
void set_vertical_airflow(AirflowVerticalDirection direction);
AirflowHorizontalDirection get_horizontal_airflow() const;
void set_horizontal_airflow(AirflowHorizontalDirection direction);
std::string get_cleaning_status_text() const;
CleaningState get_cleaning_status() const;
void start_self_cleaning();
void start_steri_cleaning();
void set_send_wifi(bool send_wifi);
protected:
void set_answers_handlers() override;
void process_phase(std::chrono::steady_clock::time_point now) override;
haier_protocol::HaierMessage get_control_message() override;
bool is_message_invalid(uint8_t message_type) override;
void process_pending_action() override;
// Answers handlers
haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type,
const uint8_t *data, size_t data_size);
haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type,
const uint8_t *data, size_t data_size);
haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data,
size_t data_size);
haier_protocol::HandlerError get_management_information_answer_handler_(uint8_t request_type, uint8_t message_type,
const uint8_t *data, size_t data_size);
haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type,
const uint8_t *data, size_t data_size);
haier_protocol::HandlerError get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type,
const uint8_t *data, size_t data_size);
// Helper functions
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
std::unique_ptr<uint8_t[]> last_status_message_;
bool beeper_status_;
CleaningState cleaning_status_;
bool got_valid_outdoor_temp_;
AirflowVerticalDirection vertical_direction_;
AirflowHorizontalDirection horizontal_direction_;
bool hvac_hardware_info_available_;
std::string hvac_protocol_version_;
std::string hvac_software_version_;
std::string hvac_hardware_version_;
std::string hvac_device_name_;
bool hvac_functions_[5];
bool &use_crc_;
uint8_t active_alarms_[8];
esphome::sensor::Sensor *outdoor_sensor_;
bool send_wifi_signal_;
std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level
};
} // namespace haier
} // namespace esphome

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