Compare commits

...

386 Commits

Author SHA1 Message Date
Jesse Hills
9ff893881c Merge pull request #3560 from esphome/bump-2022.6.0b4
2022.6.0b4
2022-06-14 22:42:06 +12:00
Jesse Hills
94f6c6861a Bump version to 2022.6.0b4 2022-06-14 20:41:46 +12:00
Martin
b1d614e6c4 Bm3xx: Fix typo (#3559) 2022-06-14 20:41:46 +12:00
André Klitzing
7fceb070e5 Fix compilation with ESP32-S3 (#3543)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-06-14 20:41:46 +12:00
Jesse Hills
5c7c0834c0 Merge pull request #3554 from esphome/bump-2022.6.0b3
2022.6.0b3
2022-06-13 17:06:45 +12:00
Jesse Hills
f3a25de11d Bump version to 2022.6.0b3 2022-06-13 13:32:43 +12:00
Jesse Hills
041bef8bcd Implement media player volume actions (#3551) 2022-06-13 13:32:43 +12:00
Jesse Hills
6e83790308 Merge pull request #3540 from esphome/bump-2022.6.0b2
2022.6.0b2
2022-06-09 21:16:23 +12:00
Jesse Hills
d2d4eb4eae Bump version to 2022.6.0b2 2022-06-09 20:27:22 +12:00
Viktor Nagy
5942a3898c Nextion brightness setting requires an assignment (#3533) 2022-06-09 20:27:22 +12:00
Samuel Sieb
93421f0fa7 publish fan speed count for discovery (#3537)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2022-06-09 20:27:22 +12:00
Jesse Hills
6cb5cd48c2 Merge pull request #3535 from esphome/bump-2022.6.0b1
2022.6.0b1
2022-06-08 23:26:21 +12:00
Jesse Hills
746fd1122f Bump version to 2022.6.0b1 2022-06-08 22:46:20 +12:00
Jesse Hills
9663760ec5 Merge branch 'dev' into bump-2022.6.0b1 2022-06-08 22:46:20 +12:00
Wolfgang Tremmel
a3d73d1e23 RG15 data is float/double, not int (#3512)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-06-08 22:34:50 +12:00
Jesse Hills
d63e14a4b6 Implement the media player actions (#3534) 2022-06-08 22:33:21 +12:00
DAVe3283
03944e6cd8 Fix bogus reading on no communication with MAX31865 (#3505) 2022-06-08 09:58:32 +12:00
Maurice Makaay
0d1028be2e Cleanup deprecated EntityBase::hash_base() (#3525)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
2022-06-08 09:13:11 +12:00
VitaliyKurokhtin
6a85259e4d Block Tuya light from reacting to dp changes if transitioning (#3076) 2022-06-07 23:07:08 +12:00
Maurice Makaay
ebca936b7e Fix percentage validation for wrong data type input (#3524)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
2022-06-07 23:00:27 +12:00
Nicholas Peters
31c4551890 Fix sdp3x error checking (#3531) 2022-06-07 22:43:46 +12:00
Samuel Sieb
dd470d4197 support rotated ILI9341 (ILI9342) (#3526)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2022-06-07 22:42:13 +12:00
Maurice Makaay
612822490b Fix endless 'WiFi Unknown connection status 0' loop (#3530)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
2022-06-07 12:08:29 +12:00
Maurice Makaay
f8969605e8 Suppress first rotary encoder event (#3532)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
2022-06-07 11:36:54 +12:00
Carlos Garcia Saura
dd24ffa24e Correct ADC auto-range for ESP32-S2 variant (13 bit adc) (#3158)
Co-authored-by: Otto Winter <otto@otto-winter.com>
2022-06-03 16:07:35 +12:00
guillempages
d0dda48932 Add display_type property to DisplayBuffer (#3430)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-06-03 15:39:04 +12:00
Eric van Blokland
6349b5f654 Added RC6 protocol support (#3514) 2022-06-03 15:37:04 +12:00
Joe
a6ff02a3cf Refactor clock syncing (#3503)
* Expose `send_local_time()` as public, for use in lambdas.
  This will send the current time configured in `time_id`.
* Add a new `set_clock()` public method, separate from time_id.
  This allows setting the clock manually, without syncing from a Time
  Component. Again this can only be called from ESPHome; i.e.,
  generally from a lambda.
2022-06-03 13:53:20 +12:00
jimtng
4f57bf786b Add mqtt.on_connect and mqtt.on_disconnect triggers (#3520) 2022-06-03 13:51:50 +12:00
Jesse Hills
6221f6d47d Implement Media Player and I2S Media player (#3487) 2022-06-02 17:00:17 +12:00
Wolfgang Tremmel
a922efeafa Change rain intensity sensor string (#3511) 2022-05-31 16:49:18 +12:00
jimtng
5aa42e5e66 Add variable substitutions for !include (#3510) 2022-05-31 16:45:18 +12:00
Joe
708672ec7e [BedJet] Add configurable heating strategy (#3519)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-05-31 15:45:01 +12:00
Jan Grewe
d2cefbf224 Allow Prometheus component to export internal components (#3508)
Co-authored-by: Jan Grewe <jan.grewe@flixbus.com>
2022-05-31 07:29:57 +12:00
Michael Davidson
adb7aa6950 Thermostat preset with modes (#3298)
* Rework HOME/AWAY support to being driven via a map of ClimatePreset/ThermostatClimateTargetTempConfig
This opens up to theoretically being able to support other presets (ECO, SLEEP, etc)

* Add support for additional presets
Configuration takes the form;
```
climate:
  platform: preset
  ...
  preset:
    [eco | away | boost | comfort | home | sleep | activity]:
      default_target_temperature_low: 20
      default_target_temperature_high: 24
```

These will be available in the Home Assistant UI and, like the existing Home/Away config will reset the temperature in line with these defaults when selected. The existing away_config/home_config is still respected (although preset->home/preset->away will be applied after them and override them if both styles are specified)

* Add support for specifying MODE, FAN_MODE and SWING_MODE on a preset
When switching presets these will implicitly flow through to the controller. However calls to climate.control which specify any of these will take precedence even when changing the mode (think of the preset version as the default for that preset)

* Add `preset_change` mode trigger
When defined this trigger will fire when the preset for the thermostat has been changed. The intent of this is similar to `auto_mode` - it's not intended to be used to control the preset's state (eg. communicate with the physical thermostat) but instead might be used to update a visual indicator, for instance.

* Apply lint, clang-format, and clang-tidy fixes

* Additional clang-format fixes

* Wrap log related strings in LOG_STR_ARG

* Add support for custom presets
This also changes the configuration syntax to;
```yaml
  preset:
    # Standard preset
    - name: [eco | away | boost | comfort | home | sleep | activity]
      default_target_temperature_low: 20
      ...
    # Custom preset
    - name: My custom preset
      default_target_temperature_low: 18
```

For the end user there is no difference between a custom and built in preset. For developers custom presets are set via `climate.control` `custom_preset` property instead of the `preset`

* Lint/clang-format/clang-tidy fixes

* Additional lint/clang-format/clang-tidy fixes

* Clang-tidy changes

* Sort imports

* Improve configuration validation for presets
- Unify temperature validation across default, away, and preset configuration
- Validate modes for presets have the required actions

* Trigger a refresh after changing internals of the thermostat

* Apply formatting fixes

* Validate mode, fan_mode, and swing_mode on presets

* Add preset temperature validation against visual min/max configuration

* Apply code formatting fixes

* Fix preset temperature validation
2022-05-24 22:44:26 -05:00
Wumpf
cd35ead890 [scd4x] Fix not passing arguments to templatable value for perform_forced_calibration (#3495) 2022-05-24 13:00:06 +12:00
joseph douce
9dc804ee27 Output a true RMS voltage % (#3494) 2022-05-24 12:52:54 +12:00
Martin
a8ceeaa7b0 esp32: fix NVS (#3497) 2022-05-23 20:56:26 +12:00
Sergey Dudanov
7092f7663e midea: New power_toggle action. Auto-use remote transmitter. (#3496) 2022-05-23 20:51:45 +12:00
Jesse Hills
d9d2edeb08 Fix compile issues on windows (#3491) 2022-05-19 21:21:42 +12:00
Jesse Hills
dda1ddcb26 Add missing import to bedjet (#3490) 2022-05-19 16:23:40 +12:00
Keilin Bickar
f0c890f160 Remove deprecated fan speeds (#3397)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-05-19 12:50:44 +12:00
gazoodle
4f52d43347 add support user-defined modbus functions (#3461) 2022-05-19 12:49:12 +12:00
Martin
0ed7db979b Add support for SGP41 (#3382)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-05-19 12:47:33 +12:00
myml
9c78049359 feat: esp32-camera add stream event (#3285) 2022-05-19 12:23:50 +12:00
user897943
7882105661 Update bedjet_const.h to remove blank spaces before speed steps, fixes Unknown Error when using climate.set_fan_mode in HA (#3476) 2022-05-19 10:25:42 +12:00
Dave T
c000e1d6dd Ili9341 8bit indexed mode pt1 (#2490) 2022-05-19 10:23:00 +12:00
Jesse Hills
c5069edc78 Merge pull request #3484 from esphome/bump-2022.5.0b4
2022.5.0b4
2022-05-17 23:42:51 +12:00
Jesse Hills
282d9e138c Revert adding spaces 2022-05-17 23:31:55 +12:00
Jesse Hills
72fcf2cbe1 Bump version to 2022.5.0b4 2022-05-17 23:23:37 +12:00
Samuel Sieb
6f49f5465b Retry Tuya init commands (#3482)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2022-05-17 23:23:33 +12:00
Martin
17b8bd8316 ESP32: Only save to NVS if data was changed (#3479) 2022-05-17 23:16:33 +12:00
Samuel Sieb
9b6b9c1fa2 Retry Tuya init commands (#3482)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2022-05-17 20:15:02 +12:00
Martin
609a2ca592 ESP32: Only save to NVS if data was changed (#3479) 2022-05-17 10:59:36 +12:00
[pʲɵs]
6dabf24bf3 MQTT cover: send state even if position is available (#3473) 2022-05-16 15:35:27 +12:00
Jesse Hills
7e88938932 Merge pull request #3478 from esphome/bump-2022.5.0b3
2022.5.0b3
2022-05-16 13:42:05 +12:00
Jesse Hills
c707e64685 Bump version to 2022.5.0b3 2022-05-16 13:07:12 +12:00
Jesse Hills
a639690716 Mark improv_serial and ESP-IDF usb based serial on c3/s2/s3 unsupported (#3477) 2022-05-16 13:07:12 +12:00
[pʲɵs]
01222dbab7 Increase JSON buffer size on overflow (#3475) 2022-05-16 13:07:12 +12:00
Jesse Hills
93e2506279 Mark improv_serial and ESP-IDF usb based serial on c3/s2/s3 unsupported (#3477) 2022-05-16 13:05:20 +12:00
Maxim Ocheretianko
f62d5d3b9d Add Tuya select (#3469) 2022-05-16 07:49:40 +12:00
Maxim Ocheretianko
0665acd190 Tuya status gpio support (#3466) 2022-05-16 07:44:14 +12:00
[pʲɵs]
fea05e9d33 Increase JSON buffer size on overflow (#3475) 2022-05-15 19:53:43 +12:00
dependabot[bot]
7a03c7d56f Bump pylint from 2.13.8 to 2.13.9 (#3470)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.13.8 to 2.13.9.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.13.8...v2.13.9)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-15 19:46:36 +12:00
dependabot[bot]
2dc2aec954 Bump esptool from 3.3 to 3.3.1 (#3468)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-13 13:44:24 +12:00
Dave T
39c6c2417a Remove duplicate convert_to_8bit_color_ function. (#2469)
Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl>
2022-05-12 22:18:51 +12:00
Jesse Hills
ff72d6a146 Merge pull request #3465 from esphome/bump-2022.5.0b2
2022.5.0b2
2022-05-12 22:15:21 +12:00
Jesse Hills
603d0d0c7c Bump version to 2022.5.0b2 2022-05-12 17:00:14 +12:00
Brian Kaufman
28883f711b Update captive portal canHandle function (#3360) 2022-05-12 17:00:13 +12:00
Michael Davidson
e914828add Make custom_fan and custom_preset templatable as per documentation (#3330) 2022-05-12 17:00:13 +12:00
James Szalay
c1480029fb Use heat mode for heat. Move EXT HT to custom presets. (#3437)
* Use heat mode for heat. Move EXT HT to custom presets.

* Fix syntax error.
2022-05-12 17:00:13 +12:00
Niclas Larsson
40f622949e Shelly dimmer: Use unique_ptr to handle the lifetime of stm32_t (#3400)
Co-authored-by: Martin <25747549+martgras@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-05-12 17:00:13 +12:00
Maurice Makaay
63096ac2bc On epoch sync, restore local TZ (#3462)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
2022-05-12 17:00:13 +12:00
Brian Kaufman
03d5a0ec1d Update captive portal canHandle function (#3360) 2022-05-12 16:57:50 +12:00
Michael Davidson
1c873e0034 Make custom_fan and custom_preset templatable as per documentation (#3330) 2022-05-12 16:54:45 +12:00
swifty99
bcb47c306c Tcs34725 automatic sampling settings for improved dynamics and accuracy (#3258)
Co-authored-by: Daniel Cousens <413395+dcousens@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-05-12 16:53:33 +12:00
James Szalay
01c4d3c225 Use heat mode for heat. Move EXT HT to custom presets. (#3437)
* Use heat mode for heat. Move EXT HT to custom presets.

* Fix syntax error.
2022-05-12 15:26:14 +12:00
Niclas Larsson
c2aaae4818 Shelly dimmer: Use unique_ptr to handle the lifetime of stm32_t (#3400)
Co-authored-by: Martin <25747549+martgras@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-05-12 10:26:51 +12:00
Maurice Makaay
3f678e218d On epoch sync, restore local TZ (#3462)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
2022-05-12 09:25:00 +12:00
Jesse Hills
c2a59cb476 Merge pull request #3460 from esphome/bump-2022.5.0b1
2022.5.0b1
2022-05-11 15:51:44 +12:00
Jesse Hills
f8a1bd4e79 Bump version to 2022.6.0-dev 2022-05-11 12:50:42 +12:00
Jesse Hills
d6e039a1d1 Bump version to 2022.5.0b1 2022-05-11 12:50:42 +12:00
Jesse Hills
0f1a7c2b69 Merge branch 'dev' into bump-2022.5.0b1 2022-05-11 12:50:41 +12:00
Jesse Hills
40ad9f4911 Add deep_sleep.allow YAML action (#3459) 2022-05-11 12:47:50 +12:00
Ruben De Smet
4116caff6a Implement allow_deep_sleep (#3282) 2022-05-11 11:44:52 +12:00
Otto Winter
0b69f72315 Enable api transport encryption for new projects (#3142)
* Enable api transport encryption for new projects

* Format
2022-05-11 11:38:05 +12:00
Maurice Makaay
c569f5ddcf Code cleanup fixes for the number component (#3458)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
2022-05-11 11:02:49 +12:00
Maurice Makaay
62f9e181e0 Code cleanup fixes for the select component (#3457)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
2022-05-11 10:58:28 +12:00
Otto Winter
235a97ea10 Make retry scheduler efficient (#3225) 2022-05-11 07:54:00 +12:00
MFlasskamp
e541ae400c Esp32c3 deepsleep fix (#3454) 2022-05-10 22:03:59 +12:00
Massimo Cetra
4822abde86 Fix BLE280 setup when the sensor is marked as failed. (#3396) 2022-05-10 22:03:40 +12:00
Jesse Hills
b7e52812f8 Fix tests (#3455) 2022-05-10 22:02:58 +12:00
LuBeDa
69118120d9 added prev_frame for animation (#3427) 2022-05-10 21:56:29 +12:00
Dennis
7cba0c6fb0 Fix cover set position by force pushing position_id datapoint (simila… (#3435) 2022-05-10 21:42:31 +12:00
Felix Storm
5fac67ce15 CAN bus: on_frame remote_transmission_request (#3376) 2022-05-10 21:39:18 +12:00
Matthew Garrett
98c733108e PMSX003: Add support for specifying the update interval and spinning down (#3053)
Co-authored-by: Otto Winter <otto@otto-winter.com>
2022-05-10 21:35:43 +12:00
Martin
782186e13d extend scd4x (#3409)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-05-10 21:25:44 +12:00
George
4e1f6518e8 Delonghi Penguino PAC W120HP ir support (#3124) 2022-05-10 21:22:22 +12:00
Andre Lengwenus
53e0fe8e51 Add SML (Smart Message Language) platform for energy meters (#2396)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-05-10 21:05:49 +12:00
Martin
0e547390da add support for Sen5x sensor series (#3383)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-05-10 20:15:02 +12:00
Martin
86b52df839 tca9548a fix channel selection (#3417) 2022-05-10 17:17:55 +12:00
MFlasskamp
d685fdf54a mask deprecated adc_gpio_init() for esp32-s2 (#3445) 2022-05-10 17:16:16 +12:00
Maurice Makaay
d9caab4108 Number enhancement (#3429)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-05-10 16:58:56 +12:00
Maurice Makaay
44b68f140e Select enhancement (#3423)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
2022-05-10 16:41:16 +12:00
Unai
3a3d97dfa7 Add SERIAL_JTAG/CDC logger option for ESP-IDF platform for ESP32-S2/S3/C3 (#3105)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-05-10 13:28:22 +12:00
MFlasskamp
47898b527c Esp32c3 deepsleep fix (#3433) 2022-05-09 20:32:14 +12:00
dependabot[bot]
a35f36ad39 Bump pylint from 2.13.5 to 2.13.8 (#3432)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-09 20:28:21 +12:00
dependabot[bot]
d13a397f8e Bump pyupgrade from 2.32.0 to 2.32.1 (#3452)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-09 19:44:54 +12:00
Jesse Hills
df999723f8 Force using name substitution when adopting a device (#3451) 2022-05-09 19:43:09 +12:00
Jesse Hills
8236e840a7 Fix spi transfer with miso pin defined on espidf (#3450) 2022-05-09 19:24:27 +12:00
dependabot[bot]
e5b3625f73 Bump click from 8.1.2 to 8.1.3 (#3426)
Bumps [click](https://github.com/pallets/click) from 8.1.2 to 8.1.3.
- [Release notes](https://github.com/pallets/click/releases)
- [Changelog](https://github.com/pallets/click/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/click/compare/8.1.2...8.1.3)

---
updated-dependencies:
- dependency-name: click
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-09 19:22:47 +12:00
Jesse Hills
2e4645310b Also rename yaml filename with rename command (#3447) 2022-05-09 19:16:46 +12:00
Ingo Theiss
50a32b387e Add ENS210 Humidity & Temperature sensor component (#2942)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-05-09 17:23:38 +12:00
rainero84
2059283707 Early pin init (#3439)
* Added early_pin_init configuration parameter for ESP8266 platform

* Added #include to core

* Updated test3.yaml to include early_pin_init parameter

Co-authored-by: Rainer Oellermann <ro@playplaycode.com>
2022-05-09 17:21:43 +12:00
Patrick van der Leer
8e3af515c9 Waveshare epaper 7in5 v2alt (#3276)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-05-09 17:17:36 +12:00
Paulus Schoutsen
6f88f0ea3f Bump dashboard to 20220508.0 (#3448) 2022-05-09 17:17:21 +12:00
Jens-Christian Skibakk
d2f37cf3f9 Support for Arduino 2 and serial port on ESP32-S2 and ESP32-C3 (#3436) 2022-05-09 16:17:22 +12:00
Paulus Schoutsen
7c30d6254e Add rename command handler (#3443) 2022-05-09 13:53:34 +12:00
Jesse Hills
64fb39a653 Add help text to rename command (#3442) 2022-05-09 10:18:24 +12:00
Dan Jackson
91895aa70c Allow wifi output_power down to 8.5dB (#3405) 2022-05-03 19:09:06 +12:00
LuBeDa
68dfaf238b added RGB565 image type (#3229)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-04-27 08:41:10 +12:00
Trevor North
ebf13a0ba0 Queue sensor publishes so we don't block for too long (#3422) 2022-04-27 07:51:22 +12:00
code-review-doctor
2bff9937b7 Fix issue probably-meant-fstring found at https://codereview.doctor (#3415) 2022-04-27 07:43:35 +12:00
Jesse Hills
256395c28d Add duration device class for sensors (#3421) 2022-04-26 21:02:08 +12:00
quentin9696
3346bc8bba feat: add openssh-client on docker image (#1681) (#3319)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-04-26 10:09:49 +12:00
Martin
6fe22a7e62 SPS30: Add fan action (#3410)
* Add fan action to SPS30

* add codeowner
2022-04-26 09:50:36 +12:00
Jesse Hills
41d9059a2f Merge pull request #3407 from esphome/bump-2022.4.0b4
2022.4.0b4
2022-04-20 16:55:11 +12:00
Jesse Hills
e26e0d7c01 Bump version to 2022.4.0b4 2022-04-20 16:35:43 +12:00
Jesse Hills
ad41c07a1f Dont require {} for wifi ap with defaults (#3404) 2022-04-20 16:35:42 +12:00
Jesse Hills
5732f3b044 Merge pull request #3402 from esphome/bump-2022.4.0b3
2022.4.0b3
2022-04-19 15:31:05 +12:00
Jesse Hills
712115b6ce Bump version to 2022.4.0b3 2022-04-19 12:33:38 +12:00
rnauber
9283559c6b Shelly Dimmer: Delete obsolete LICENSE.txt (#3394) 2022-04-19 12:33:38 +12:00
Michel van de Wetering
6b393438e9 Fix power_delivered/produced_phase sensor deviceclass in DSMR (#3395) 2022-04-19 12:33:38 +12:00
Jesse Hills
343b9ab455 Merge pull request #3390 from esphome/bump-2022.4.0b2
2022.4.0b2
2022-04-14 15:37:31 +12:00
Jesse Hills
dcb226b202 Bump version to 2022.4.0b2 2022-04-14 13:48:35 +12:00
Janez Troha
2243021b58 Allocate smaller amount of buffer for JSON (#3384) 2022-04-14 13:48:35 +12:00
rnauber
d5134e88b1 Add support for Shelly Dimmer 2 (#2954)
Co-authored-by: Niclas Larsson <niclas@edgesystems.se>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Jernej Kos <jernej@kos.mx>
Co-authored-by: Richard Nauber <richard@nauber.dev>
2022-04-14 13:48:35 +12:00
matthias882
c59adf612f Changes accuracy of single cell voltage (#3387) 2022-04-14 13:48:35 +12:00
Jesse Hills
a82d8ea0c3 Merge pull request #3381 from esphome/bump-2022.4.0b1
2022.4.0b1
2022-04-13 16:14:08 +12:00
Jesse Hills
ad57faa9a9 Bump version to 2022.4.0b1 2022-04-13 13:42:28 +12:00
Jesse Hills
a9b5e8d036 Merge branch 'dev' into bump-2022.4.0b1 2022-04-13 13:42:27 +12:00
Jesse Hills
59e6e798dd Merge pull request #3302 from esphome/bump-2022.3.0b2
2022.3.0b2
2022-03-16 15:38:32 +13:00
Jesse Hills
e5c2dbc7ec Bump version to 2022.3.0b2 2022-03-16 14:06:00 +13:00
Jesse Hills
756f71c382 Allow custom register type for modbus number (#3202) 2022-03-16 14:06:00 +13:00
Jesse Hills
b7535693fa Add helper overloads for hex print 16-bit (#3297) 2022-03-16 14:06:00 +13:00
stegm
06a3505698 Add optimistic config flag to modbus select. (#3267) 2022-03-16 14:06:00 +13:00
Jesse Hills
efa8f0730d Merge pull request #3278 from esphome/bump-2022.3.0b1
2022.3.0b1
2022-03-09 20:57:07 +13:00
Jesse Hills
023d26f521 Bump version to 2022.3.0b1 2022-03-09 20:07:50 +13:00
Jesse Hills
5068619f1b Merge branch 'dev' into bump-2022.3.0b1 2022-03-09 20:07:49 +13:00
Jesse Hills
b528f48417 Merge pull request #3201 from esphome/bump-2022.2.0b3
2022.2.0b3
2022-02-16 11:10:31 +13:00
Jesse Hills
ec7a79049a Bump version to 2022.2.0b3 2022-02-16 09:45:05 +13:00
Jesse Hills
6ddad6b299 Update HA addon token (#3200) 2022-02-16 09:45:05 +13:00
Maurice Makaay
16dc7762f9 Fix strlcpy() uses to make long SSIDs and passwords work (#3199)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
2022-02-16 09:45:05 +13:00
Jesse Hills
dc0ed8857f Merge pull request #3198 from esphome/bump-2022.2.0b2
2022.2.0b2
2022-02-15 12:48:34 +13:00
Jesse Hills
bb6b77bd98 Bump version to 2022.2.0b2 2022-02-15 12:00:12 +13:00
Jesse Hills
dcc80f9032 Allow framework version validator to be maximum version (#3197) 2022-02-15 12:00:12 +13:00
Otto Winter
dd554bcdf4 Make generating combined binary output verbose (#3127) 2022-02-15 12:00:12 +13:00
Jesse Hills
f376a39e55 Clamp rotary_encoder restored value to min and max (#3184) 2022-02-15 12:00:12 +13:00
dependabot[bot]
8dcc9d6b66 Bump aioesphomeapi from 10.8.1 to 10.8.2 (#3182) 2022-02-15 12:00:11 +13:00
Jesse Hills
a576c9f21f Merge pull request #3180 from esphome/bump-2022.2.0b1
2022.2.0b1
2022-02-10 00:20:46 +13:00
Jesse Hills
71a438e2cb Bump version to 2022.2.0b1 2022-02-09 23:47:36 +13:00
Jesse Hills
272d6f2a8b Merge branch 'dev' into bump-2022.2.0b1 2022-02-09 23:47:36 +13:00
Jesse Hills
5dc776e55f Merge pull request #3068 from esphome/bump-2022.1.0b4
2022.1.0b4
2022-01-19 07:40:37 +13:00
Jesse Hills
72d60f30f7 Bump version to 2022.1.0b4 2022-01-18 15:49:31 +13:00
Oxan van Leeuwen
869743a742 Fail hard if no random bytes available for encryption (#3067) 2022-01-18 15:49:31 +13:00
Martin
7b03e07908 [modbus_controller] add missing skip_updates (#3063) 2022-01-18 15:49:31 +13:00
Paulus Schoutsen
348f880e15 bump dashboard to 20220116.0 (#3061) 2022-01-18 15:49:31 +13:00
Jesse Hills
ead597d0fb Merge pull request #3060 from esphome/bump-2022.1.0b3
2022.1.0b3
2022-01-17 13:13:40 +13:00
Jesse Hills
afbf989715 Bump version to 2022.1.0b3 2022-01-17 12:40:07 +13:00
Jesse Hills
01b62a16c3 Add number setting to web_server/rest_api (#3055) 2022-01-17 12:40:07 +13:00
Oxan van Leeuwen
c5eba04517 Remove deprecated attribute from virtual entity methods (#3056) 2022-01-17 12:40:07 +13:00
Oxan van Leeuwen
282313ab52 Rename post_build scripts to fix codeowners script (#3057) 2022-01-17 12:40:07 +13:00
Ohad Lutzky
d274545e77 Disable caching for binary download (#3054) 2022-01-17 12:40:07 +13:00
Jesse Hills
d3fda37615 Merge pull request #3042 from esphome/bump-2022.1.0b2
2022.1.0b2
2022-01-13 22:21:45 +13:00
Jesse Hills
cbe3092404 Bump version to 2022.1.0b2 2022-01-13 21:28:45 +13:00
Paulus Schoutsen
6dfe3039d0 Bump dashboard to 20220113.2 (#3041) 2022-01-13 21:28:45 +13:00
Paulus Schoutsen
d6009453df Add factory to download name (#3040) 2022-01-13 21:28:45 +13:00
Jesse Hills
c81323ef91 Merge pull request #3039 from esphome/bump-2022.1.0b1
2022.1.0b1
2022-01-13 12:12:07 +13:00
Jesse Hills
961c27f1c2 Bump version to 2022.1.0b1 2022-01-13 11:02:07 +13:00
Jesse Hills
fe4a14e6cc Merge branch 'dev' into bump-2022.1.0b1 2022-01-13 11:02:07 +13:00
Jesse Hills
50848c2f4d Merge pull request #2966 from esphome/bump-2021.12.3
2021.12.3
2021-12-30 14:54:49 +13:00
Jesse Hills
d32633b3c7 Update curl package version in docker (#2939) 2021-12-30 14:32:29 +13:00
Jesse Hills
b37739eec2 Bump version to 2021.12.3 2021-12-30 13:58:47 +13:00
Jesse Hills
28f87dc804 Remove -e for hassio images (#2964) 2021-12-30 13:58:47 +13:00
Jesse Hills
41879e41e6 Workaround installing as editable package not working (#2936) 2021-12-30 13:58:47 +13:00
Jesse Hills
fc0a6546a2 Only allow internal pins for dht sensor (#2940) 2021-12-30 13:58:47 +13:00
Jesse Hills
ffd4280d6c Require arduino in webserver for better validation (#2941) 2021-12-30 13:58:46 +13:00
Jesse Hills
db3b955b0f Merge pull request #2932 from esphome/bump-2021.12.2
2021.12.2
2021-12-21 12:45:27 +13:00
Jesse Hills
5516f65971 Bump version to 2021.12.2 2021-12-21 08:24:08 +13:00
Oxan van Leeuwen
9471df0a1b Fix MQTT button press action (#2917) 2021-12-21 08:24:07 +13:00
Oxan van Leeuwen
6d39f64be7 Don't disable idle task WDT when it's not enabled (#2856) 2021-12-21 08:24:07 +13:00
Jesse Hills
b89d0a9a73 Merge pull request #2915 from esphome/bump-2021.12.1
2021.12.1
2021-12-15 16:36:39 +13:00
Jesse Hills
4bb779d9a5 Bump version to 2021.12.1 2021-12-15 14:57:32 +13:00
wilberforce
386a5b6362 Allow button POST on press from web server (#2913) 2021-12-15 14:57:32 +13:00
Oxan van Leeuwen
e32a999cd0 Set text sensor state property to filter output (#2893) 2021-12-15 14:57:32 +13:00
Jesse Hills
bfbc6a4bad Merge pull request #2907 from esphome/bump-2021.12.0
2021.12.0
2021-12-12 07:59:37 +13:00
Jesse Hills
8c9e0e552d Bump version to 2021.12.0 2021-12-12 07:10:51 +13:00
Jesse Hills
8aaf9fd83f Merge pull request #2905 from esphome/bump-2021.12.0b6
2021.12.0b6
2021-12-11 21:54:49 +13:00
Jesse Hills
08057720b8 Bump version to 2021.12.0b6 2021-12-11 21:07:07 +13:00
Jesse Hills
bfaa648837 Bump esphome-dashboard to 20211211.0 (#2904) 2021-12-11 21:07:07 +13:00
Keith Burzinski
d504daef91 Fix for two points setting when fan_only_cooling is disabled (#2903)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Keith Burzinski <kburzinski@kbx-mbp2021.ad.kbx81.net>
2021-12-11 21:07:07 +13:00
Jesse Hills
b8d3ef2f49 Merge pull request #2899 from esphome/bump-2021.12.0b5
2021.12.0b5
2021-12-10 10:55:55 +13:00
Jesse Hills
3bf6320030 Bump version to 2021.12.0b5 2021-12-10 09:55:48 +13:00
Guillermo Ruffino
708b928c73 Modbus number/output use write single (#2896)
Co-authored-by: Martin <25747549+martgras@users.noreply.github.com>
2021-12-10 09:55:48 +13:00
Jesse Hills
649366ff44 Fix published state for modbus number (#2894) 2021-12-10 09:55:47 +13:00
Jesse Hills
e5c9e87fad Merge pull request #2890 from esphome/bump-2021.12.0b4
2021.12.0b4
2021-12-10 09:50:29 +13:00
Jesse Hills
f3d9d707b6 Bump version to 2021.12.0b4 2021-12-08 12:58:14 +13:00
Jesse Hills
090e10730c Bump esphome-dashboard to 20211208.0 (#2887) 2021-12-08 12:58:14 +13:00
Jesse Hills
fbc84861c7 Use new platform component config blocks for wizard (#2885) 2021-12-08 12:58:14 +13:00
Carlos Garcia Saura
e763469af8 Feed watchdog while setting up OTA (#2876) 2021-12-08 12:58:14 +13:00
Jesse Hills
3c0c514e44 Merge pull request #2880 from esphome/bump-2021.12.0b3
2021.12.0b3
2021-12-07 15:27:08 +13:00
Jesse Hills
ed5e2dd332 Bump version to 2021.12.0b3 2021-12-07 07:47:48 +13:00
Jesse Hills
09b7c6f550 Bump esphome-dashboard to 20211207.0 (#2877) 2021-12-07 07:47:48 +13:00
Oxan van Leeuwen
df315a1f51 Feed watchdog when no component loops (#2857) 2021-12-07 07:47:48 +13:00
Jesse Hills
7ee4bb621c Allow wizard to specify secrets (#2875) 2021-12-07 07:47:48 +13:00
Jesse Hills
24874f4c3c Adopt using wifi secrets that should exist at this point (#2874) 2021-12-07 07:47:48 +13:00
Jesse Hills
c128880033 Add endpoint to fetch secrets keys (#2873) 2021-12-07 07:47:48 +13:00
Massimiliano Ravelli
a66e94a0b0 Ignore already stopped dhcp for ethernet (#2862)
Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2021-12-07 07:47:48 +13:00
Oxan van Leeuwen
56870ed4a8 Fix MCP23x17 not disabling pullup after config change (#2855) 2021-12-07 07:47:48 +13:00
Martin
3ac720df47 SPS30 : fix i2c read size (#2866) 2021-12-07 07:47:48 +13:00
Carlos Garcia Saura
1bc757ad06 ADC: Turn verbose the debugging "got voltage" (#2863) 2021-12-07 07:47:48 +13:00
Martin
f72abc6f3d tlc59208f : fix compilation error (#2867) 2021-12-07 07:47:48 +13:00
Jesse Hills
5ac88de985 Bump esphome-dashboard to 20211206.0 (#2870) 2021-12-07 07:47:48 +13:00
Jesse Hills
0826b367d6 Merge pull request #2853 from esphome/bump-2021.12.0b2
2021.12.0b2
2021-12-03 08:07:30 +13:00
Jesse Hills
329bf861d6 Bump version to 2021.12.0b2 2021-12-03 07:54:34 +13:00
Oxan van Leeuwen
9dcd3d18a0 Update ota_component.cpp (#2852) 2021-12-03 07:54:34 +13:00
Jesse Hills
db66cd88b6 Merge pull request #2851 from esphome/bump-2021.12.0b1
2021.12.0b1
2021-12-02 21:32:43 +13:00
Jesse Hills
86c205fe43 Remove blank line 2021-12-02 21:08:11 +13:00
Jesse Hills
c6414138c7 Bump version to 2021.12.0b1 2021-12-02 19:38:49 +13:00
Jesse Hills
36b355eb82 Merge branch 'dev' into bump-2021.12.0b1 2021-12-02 19:38:44 +13:00
Jesse Hills
7be9291b13 Merge pull request #2821 from esphome/bump-2021.11.4
2021.11.4
2021-11-29 13:23:47 +13:00
Jesse Hills
ea9e75039b Bump version to 2021.11.4 2021-11-29 10:18:49 +13:00
Conclusio
a5fb036011 Add delay to improve stability (#2793) 2021-11-29 10:18:48 +13:00
Dave T
e55506f9db Correct bitmask for third color (blue) scaling. (#2817) 2021-11-29 10:18:48 +13:00
Carlos Garcia Saura
50ec1d0445 Fix compilation error for WPA enterprise in ESP-IDF (#2815) 2021-11-29 10:18:48 +13:00
Oxan van Leeuwen
3d5e1d8d91 Fix parsing of multiple values in EZO sensor (#2814)
Co-authored-by: Lydia Sevelt <LydiaSevelt@gmail.com>
2021-11-29 10:18:48 +13:00
Oxan van Leeuwen
db2128a344 Fix parsing numbers in Anova (#2816) 2021-11-29 10:18:48 +13:00
Jesse Hills
21db43db06 Merge pull request #2808 from esphome/bump-2021.11.3
2021.11.3
2021-11-28 00:01:16 +13:00
Jesse Hills
5009b3029f Bump version to 2021.11.3 2021-11-27 21:13:01 +13:00
Maurice Makaay
57a029189c Add missing nvs_flash_init() to ESP32 preferences code (#2805)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
2021-11-27 21:13:01 +13:00
Jesse Hills
0cb715bb76 Merge pull request #2799 from esphome/bump-2021.11.2
2021.11.2
2021-11-26 09:25:37 +13:00
Jesse Hills
7d03823afd Bump version to 2021.11.2 2021-11-26 09:02:54 +13:00
Oxan van Leeuwen
8e1c9f5042 Fix parsing numbers from null-terminated buffers (#2755) 2021-11-26 09:02:54 +13:00
Samuel Sieb
980b7cda8f Remove floating point ops from the ISR (#2751)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2021-11-26 09:02:53 +13:00
Kamil Trzciński
3a72dd5cb6 esp32_camera_web_server: Improve support for MotionEye (#2777) 2021-11-26 09:02:53 +13:00
Dave T
3178243811 Fix frame scaling for animated gifs (#2750) 2021-11-26 09:02:53 +13:00
Maurice Makaay
d30e2f2a4f Allow UART debug configuration with no after: definition (#2753) 2021-11-26 09:02:53 +13:00
Jesse Hills
6226dae05c Merge pull request #2744 from esphome/bump-2021.11.1
2021.11.1
2021-11-17 23:45:43 +13:00
Jesse Hills
9c6a475a6e Bump version to 2021.11.1 2021-11-17 23:31:38 +13:00
Franck Nijhof
8294d10d5b Re-instate device class update for binary sensors (#2743) 2021-11-17 23:31:38 +13:00
Evgeny
67558bec47 Fix HM3301 AQI index calculator (#2739) 2021-11-17 23:31:38 +13:00
Jesse Hills
84873d4074 Merge pull request #2742 from esphome/bump-2021.11.0
2021.11.0
2021-11-17 22:18:29 +13:00
Jesse Hills
58a0b28a39 Bump version to 2021.11.0 2021-11-17 21:58:30 +13:00
Jesse Hills
b37d3a66cc Merge pull request #2738 from esphome/bump-2021.11.0b9
2021.11.0b9
2021-11-17 10:27:41 +13:00
Jesse Hills
7e495a5e27 Bump version to 2021.11.0b9 2021-11-17 08:00:26 +13:00
rotarykite
c41547fd4a Fix senseair component uart read timeout (#2658)
Co-authored-by: DAVe3283 <DAVe3283+GitHub@gmail.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Chua Jun Chieh <junchieh.chua@softspace.com.my>
2021-11-17 08:00:26 +13:00
Ryan Hoffman
0d47d41c85 Use as_reversed_hex_array in ble_sensor to fix UUID parsing (#2737)
#1627 renamed as_hex_array to as_reversed_hex_array but forgot to rename these users.
2021-11-17 08:00:26 +13:00
Jesse Hills
41a3a17456 Merge pull request #2734 from esphome/bump-2021.11.0b8
2021.11.0b8
2021-11-16 13:50:10 +13:00
Jesse Hills
cbbafbcca2 Bump version to 2021.11.0b8 2021-11-16 12:53:56 +13:00
Jesse Hills
c75566b374 Fix zeroconf time comparisons (#2733)
Co-authored-by: J. Nick Koston <nick@koston.org>
2021-11-16 12:53:56 +13:00
Jesse Hills
7279f1fcc1 Merge pull request #2732 from esphome/bump-2021.11.0b7
2021.11.0b7
2021-11-16 12:19:07 +13:00
Jesse Hills
d7432f7c10 Bump version to 2021.11.0b7 2021-11-16 11:05:51 +13:00
Jesse Hills
b0a0a153f3 Improv serial/checksum changes (#2731)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-11-16 11:05:51 +13:00
Jesse Hills
024632dbd0 Merge pull request #2730 from esphome/bump-2021.11.0b6
2021.11.0b6
2021-11-16 10:53:11 +13:00
Jesse Hills
0a545a28b9 Bump version to 2021.11.0b6 2021-11-16 09:59:00 +13:00
Jesse Hills
0f2df59998 Add zeroconf as a direct dependency and lock the version (#2729) 2021-11-16 09:58:59 +13:00
Jesse Hills
29a7d32f77 Merge pull request #2725 from esphome/bump-2021.11.0b5
2021.11.0b5
2021-11-15 13:42:59 +13:00
Jesse Hills
687a7e9b2f Bump version to 2021.11.0b5 2021-11-15 12:02:18 +13:00
Alexandre-Jacques St-Jacques
09e8782318 Remove unnecessary duplicate touch_pad_filter_start (#2724) 2021-11-15 12:02:18 +13:00
Jesse Hills
f2aea02210 Merge pull request #2723 from esphome/bump-2021.11.0b4
2021.11.0b4
2021-11-15 11:42:59 +13:00
Jesse Hills
194f922312 Bump version to 2021.11.0b4 2021-11-15 11:03:40 +13:00
Jesse Hills
fea3c48098 Fix indentation of write_lambda for modbus_controller number (#2722) 2021-11-15 11:03:39 +13:00
Sergey V. DUDANOV
c2f57baec2 RemoteTransmitter fix. Bug from version 2021.10. Some changes. (#2706) 2021-11-15 11:03:39 +13:00
Oxan van Leeuwen
f4a140e126 Feed WDT between doing ESP32 touchpad measurements (#2720) 2021-11-15 11:03:39 +13:00
Oxan van Leeuwen
ab506b09fe Restore InterruptLock on wifi-less ESP8266 (#2712) 2021-11-15 11:03:39 +13:00
Krzysztof Białek
87e1cdeedb Allow setting custom command_topic for Select and Number components (#2714) 2021-11-15 11:03:39 +13:00
Jesse Hills
81a36146ef Bump ESPAsyncWebServer to 2.1.0 (#2686) 2021-11-15 11:03:39 +13:00
Jesse Hills
7fa4a68a27 Merge pull request #2704 from esphome/bump-2021.11.0b3
2021.11.0b3
2021-11-12 17:21:58 +13:00
Jesse Hills
f1c5e2ef81 Bump version to 2021.11.0b3 2021-11-12 16:12:31 +13:00
lcavalli
b526155cce Update device classes for binary sensors (#2703) 2021-11-12 16:12:31 +13:00
Jesse Hills
62c3f301e7 Only allow prometheus when using arduino (#2697) 2021-11-12 16:12:31 +13:00
Jesse Hills
38cb988809 Remove my.ha links from improv (#2695) 2021-11-12 16:12:31 +13:00
Jesse Hills
b976ac54c8 Merge pull request #2693 from esphome/bump-2021.11.0b2
2021.11.0b2
2021-11-11 12:52:35 +13:00
Jesse Hills
78026e766f Bump version to 2021.11.0b2 2021-11-11 12:25:41 +13:00
Oxan van Leeuwen
b4cd8d21a5 Enable addressable light power supply based on raw values (#2690) 2021-11-11 12:25:41 +13:00
Maurice Makaay
7552893311 Uart debugging support (#2478)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
Co-authored-by: Maurice Makaay <account-github@makaay.nl>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2021-11-11 12:25:41 +13:00
Carlos Garcia Saura
21c896d8f8 [remote_transmitter] accurate pulse timing for ESP8266 (#2476) 2021-11-11 12:25:40 +13:00
Jesse Hills
4b7fe202ec Fix template number initial value being NaN (#2692) 2021-11-11 12:25:40 +13:00
Jesse Hills
9f4519210f Merge pull request #2691 from esphome/bump-2021.11.0b1
2021.11.0b1
2021-11-11 11:05:45 +13:00
Jesse Hills
b0506afa5b Merge branch 'beta' into bump-2021.11.0b1 2021-11-11 10:48:23 +13:00
Jesse Hills
8cbb379898 Remove import (not sure how it got there) 2021-11-11 10:35:18 +13:00
Jesse Hills
b226215593 Bump version to 2021.11.0b1 2021-11-11 10:10:05 +13:00
Jesse Hills
19970729a9 Merge branch 'dev' into bump-2021.11.0b1 2021-11-11 10:10:04 +13:00
Jesse Hills
d2ebfd2833 Merge pull request #2634 from esphome/bump-2021.10.3
2021.10.3
2021-10-27 11:21:08 +13:00
Jesse Hills
bd782fc828 Bump version to 2021.10.3 2021-10-27 10:49:11 +13:00
Jesse Hills
23560e608c Fix select.set using lambda (#2633) 2021-10-27 10:49:10 +13:00
Jan Čermák
f1377b560e Fix pin number validation for sn74hc595 (#2621) 2021-10-27 10:49:10 +13:00
Martin
72108684ea fix modbus output (#2630) 2021-10-27 10:49:10 +13:00
Jesse Hills
c6adaaea97 Remove power and energy from sensors that are not true power (#2628) 2021-10-27 10:49:10 +13:00
Otto Winter
91999a38ca Fix glue code missing micros() (#2623) 2021-10-27 10:49:10 +13:00
0hax
b34eed125d Teleinfo ptec (#2599)
* teleinfo: handle historical mode correctly.

In historical mode, tags like PTEC leads to an issue where we detect a
timestamp wheras this is not possible in historical mode.

PTEC teleinfo tag looks like:
    PTEC HP..
Instead of the usual format
    IINST1 001 I

This make our data parsing fails.

While at here, make sure we continue parsing other tags even if parsing
one of the tag fails.

Signed-off-by: 0hax <0hax@protonmail.com>

* teleinfo: fix compilation with loglevel set to debug.

Signed-off-by: 0hax <0hax@protonmail.com>
2021-10-27 10:49:10 +13:00
Stefan Agner
2abe09529a Autodetect flash size (#2615) 2021-10-27 10:49:10 +13:00
Otto Winter
9aaaf4dd4b Bump platform-espressif8266 from 2.6.2 to 2.6.3 (#2620) 2021-10-27 10:49:09 +13:00
Andreas Hergert
cbfbcf7f1b fixed dependency for pca9685 component (#2614)
Co-authored-by: Otto Winter <otto@otto-winter.com>
Co-authored-by: Andreas <andreas.hergert@otrs.com>
2021-10-27 10:49:09 +13:00
Otto Winter
c7651dc40d Merge pull request #2613 from esphome/bump-2021.10.2
2021.10.2
2021-10-22 18:35:28 +02:00
Otto winter
eda1c471ad Bump version to 2021.10.2 2021-10-22 18:26:24 +02:00
Andreas Hergert
c7ef18fbc4 Bugfix tca9548a and idf refactor anh (#2612)
Co-authored-by: Andreas Hergert <andreas.hergert@otrs.com>
2021-10-22 18:26:23 +02:00
Otto Winter
901ec918b1 Fix ESP8266 OTA compression only starting framework v2.7.0 (#2610) 2021-10-22 18:26:23 +02:00
Otto Winter
6bdae55ee1 Fix compiler warnings and update platformio line filter (#2607) 2021-10-22 18:26:23 +02:00
Otto Winter
dfb96e4b7f Add owner to all libraries used (#2604) 2021-10-22 18:26:22 +02:00
Otto Winter
ff2c316b18 Re-raise keyboardinterrupt (#2603) 2021-10-22 18:26:22 +02:00
Otto Winter
5be52f71f9 Add OTA upload compression for ESP8266 (#2601) 2021-10-22 18:26:22 +02:00
Otto Winter
42873dd37c Bump noise-c from 0.1.3 to 0.1.4 (#2602) 2021-10-22 18:26:21 +02:00
Otto Winter
f93e7d4e3a Fix socket connection closed not detected (#2587) 2021-10-22 18:26:21 +02:00
Oxan van Leeuwen
bbcd523967 Fix validation of addressable light IDs (#2588) 2021-10-22 18:26:21 +02:00
dependabot[bot]
68cbe58d00 Bump esphome-dashboard from 20211021.0 to 20211021.1 (#2594)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-22 18:26:12 +02:00
Oxan van Leeuwen
115bca98f1 Fix old-style arduino_version on ESP8266 and with magic values (#2591) 2021-10-22 18:24:48 +02:00
Oxan van Leeuwen
ed0b34b2fe Fix pin/component switchup in SX1509 pin configuration (#2593) 2021-10-22 18:24:48 +02:00
Oxan van Leeuwen
ab34401421 Fix PlatformIO version for latest Arduino framework (#2590) 2021-10-22 18:24:48 +02:00
Otto Winter
eed0c18d65 Fix HeatpumpIR pin (#2585) 2021-10-22 18:24:47 +02:00
Otto Winter
e5a38ce748 Merge pull request #2580 from esphome/bump-2021.10.1
2021.10.1
2021-10-21 15:31:26 +02:00
Otto Winter
7d9d9fcf36 Fix platformio_install_deps no longer installing all lib_deps (#2584) 2021-10-21 15:29:53 +02:00
Otto Winter
f0aba6ceb2 Fix platformio version in Dockerfile doesn't match requirements (#2582) 2021-10-21 14:53:22 +02:00
Otto Winter
ab07ee57c6 Fix ESP8266 dallas GPIO16 INPUT_PULLUP (#2581) 2021-10-21 14:39:55 +02:00
Otto winter
eae3d72a4d Bump version to 2021.10.1 2021-10-21 14:23:08 +02:00
Otto Winter
7b8d826704 Fix ESP8266 OTA adds unnecessary Update library (#2579) 2021-10-21 14:23:07 +02:00
Otto Winter
e7baa42e63 Arduino global delay/millis/... symbols workaround (#2575) 2021-10-21 14:23:07 +02:00
Otto Winter
2f32833a22 Fix wifi ble coexistence check (#2573) 2021-10-21 14:23:07 +02:00
Otto Winter
f6935a4b4b Fix ESP8266 GPIO0 Pullup Validation (#2572) 2021-10-21 14:23:06 +02:00
Maurice Makaay
332c9e891b Fix MDNS for ESP8266 devices (#2571)
Co-authored-by: Maurice Makaay <account-github@makaay.nl>
Co-authored-by: Otto winter <otto@otto-winter.com>
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
2021-10-21 14:23:06 +02:00
Jesse Hills
b91ee4847f Merge pull request #2570 from esphome/bump-2021.10.0
2021.10.0
2021-10-21 08:37:26 +13:00
Jesse Hills
625463d871 Bump version to 2021.10.0 2021-10-21 07:57:10 +13:00
Jesse Hills
6433a01e07 Merge pull request #2567 from esphome/bump-2021.10.0b11
2021.10.0b11
2021-10-21 07:47:22 +13:00
Jesse Hills
56cc31e8e7 Bump version to 2021.10.0b11 2021-10-21 07:32:27 +13:00
Otto Winter
3af297aa76 Revert nextion clang-tidy changes (#2566) 2021-10-21 07:32:27 +13:00
Otto Winter
996ec59d28 Move running process log line to debug level (#2565) 2021-10-21 07:32:26 +13:00
Jesse Hills
95593eeeab Bump esphome-dashboard to 20211021.0 (#2564) 2021-10-21 07:32:20 +13:00
Jesse Hills
dad244fb7a A few esp32_ble_server/improv fixes (#2562) 2021-10-21 07:31:55 +13:00
Jesse Hills
adb5d27d95 Merge pull request #2561 from esphome/bump-2021.10.0b10
2021.10.0b10
2021-10-20 17:28:37 +13:00
Jesse Hills
8456a8cecb Bump version to 2021.10.0b10 2021-10-20 16:39:24 +13:00
Jesse Hills
b9f66373c1 Bump esphome-dashboard to 20211020.1 (#2559) 2021-10-20 16:39:16 +13:00
Jesse Hills
9ac365feef Fix HA addon so it does not have logout button (#2558) 2021-10-20 16:38:46 +13:00
Jesse Hills
43bbd58a44 Merge pull request #2557 from esphome/bump-2021.10.0b9
2021.10.0b9
2021-10-20 11:18:40 +13:00
Jesse Hills
7feffa64f3 Bump version to 2021.10.0b9 2021-10-20 11:00:02 +13:00
Jesse Hills
ea0977abb4 Bump dashboard to 20211020.0 (#2556) 2021-10-20 10:59:53 +13:00
Jesse Hills
4c83dc7c28 Merge pull request #2555 from esphome/bump-2021.10.0b8
2021.10.0b8
2021-10-20 10:30:59 +13:00
Jesse Hills
e10ab1da78 Bump version to 2021.10.0b8 2021-10-20 10:14:30 +13:00
Martin
1b0e60374b ignore exception when not waiting for a response (#2552) 2021-10-20 10:14:30 +13:00
Oxan van Leeuwen
3a760fbb44 Fix ADC pin validation on ESP32-C3 (#2551) 2021-10-20 10:14:30 +13:00
Jesse Hills
6ef57a2973 Merge pull request #2550 from esphome/bump-2021.10.0b7
2021.10.0b7
2021-10-19 16:22:29 +13:00
Jesse Hills
3e9c7f2e9f Bump version to 2021.10.0b7 2021-10-19 15:47:31 +13:00
Jesse Hills
430598b7a1 Bump dashboard to 20211019.0 (#2549) 2021-10-19 15:47:26 +13:00
Jesse Hills
91611b09b4 Merge pull request #2545 from esphome/bump-2021.10.0b6
2021.10.0b6
2021-10-18 21:41:22 +13:00
Jesse Hills
ecd115851f Bump version to 2021.10.0b6 2021-10-18 21:26:36 +13:00
Maurice Makaay
4a1e50fed1 OTA firmware MD5 check + password support for esp-idf (#2507)
Co-authored-by: Maurice Makaay <account-github@makaay.nl>
2021-10-18 21:26:36 +13:00
Jesse Hills
d6d037047b Merge pull request #2544 from esphome/bump-2021.10.0b5
2021.10.0b5
2021-10-18 16:37:43 +13:00
Jesse Hills
b5734c2b20 Bump version to 2021.10.0b5 2021-10-18 15:31:01 +13:00
Oxan van Leeuwen
723fb7eaac Autodetect ESP32 variant (#2530)
Co-authored-by: Otto winter <otto@otto-winter.com>
2021-10-18 15:31:01 +13:00
Jesse Hills
63a9acaa19 Fix Bluetooth setup_priorities (#2458)
Co-authored-by: Otto Winter <otto@otto-winter.com>
2021-10-18 15:31:00 +13:00
Jesse Hills
0524f8c677 Fix const used for IDF recommended version (#2542) 2021-10-18 15:31:00 +13:00
Otto Winter
70b62f272e Only show timestamp for dashboard access logs (#2540) 2021-10-18 15:31:00 +13:00
Jesse Hills
f0089b7940 Merge pull request #2539 from esphome/bump-2021.10.0b4
2021.10.0b4
2021-10-17 21:17:54 +13:00
Jesse Hills
4b44280d53 Bump version to 2021.10.0b4 2021-10-17 20:34:07 +13:00
Paulus Schoutsen
f045382d20 Bump dashboard to 20211015.0 (#2525)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2021-10-17 20:34:03 +13:00
Jesse Hills
db3fa1ade7 Allow downloading all bin files from backend in dashboard (#2514)
Co-authored-by: Otto Winter <otto@otto-winter.com>
2021-10-17 20:28:43 +13:00
Oxan van Leeuwen
f83950fd75 Fix bitshift on read in ADE7953 (#2537) 2021-10-17 20:28:43 +13:00
Oxan van Leeuwen
4dd1bf920d Replace framework version_hint with source option (#2529) 2021-10-17 20:28:43 +13:00
Martin
98755f3621 Fix bug in register name definition (#2526) 2021-10-17 20:28:43 +13:00
Keith Burzinski
c3a8a044b9 Fix Nextion HTTPClient error for ESP32 (#2524) 2021-10-17 20:28:42 +13:00
Martin
15b5ea43a7 Add pressure compensation during runtime (#2493)
Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl>
2021-10-17 20:28:42 +13:00
Jesse Hills
ec683fc227 Merge pull request #2523 from esphome/bump-2021.10.0b3
2021.10.0b3
2021-10-15 10:15:26 +13:00
Jesse Hills
d4e65eb82a Bump version to 2021.10.0b3 2021-10-15 09:42:44 +13:00
Paul Monigatti
10c6601b0a Revert "Added test for bme680_bsec" (#2518)
This reverts commit 7f6a50d291b14935b17802b4dce52135fad1e49e due to BSEC library license restrictions.
2021-10-15 09:42:44 +13:00
Oxan van Leeuwen
73940bc1bd Don't define UART_SELECTION_UART2 when UART2 is unavailable (#2512) 2021-10-15 09:42:43 +13:00
Paul Monigatti
9b7fb829f9 Fix: Color modes not being correctly used in light partitions (#2513) 2021-10-15 09:42:43 +13:00
Dmitriy Lopatko
c51d8c9021 add missing include in sgp30 (#2517) 2021-10-15 09:42:43 +13:00
Paul Monigatti
d8a6dfe5ce Fix BME680_BSEC compilation issue with ESP32 (#2516) 2021-10-15 09:42:43 +13:00
Oxan van Leeuwen
5f7cef0b06 Disallow using UART2 for logger on ESP-32 variants that lack it (#2510) 2021-10-15 09:42:43 +13:00
Paul Monigatti
48ff2ffc68 Fix: Light flash not restoring previous LightState (#2383)
* Update light state when transformer has finished

* Revert writing direct to output

* Correct handling of zero-length light transformers

* Allow transformers to handle zero-length transitions, and check more boundary conditions when transitioning back to start state

* Removed log.h

* Fixed race condition between LightFlashTransformer.apply() and is_finished()

* clang-format

* Step progress from 0.0f to 1.0f at t=start_time for zero-length transforms to avoid divide-by-zero
2021-10-15 09:42:43 +13:00
Oxan van Leeuwen
b3b9ccd314 Fix light state remaining on after turn off with transition (#2509) 2021-10-15 09:42:43 +13:00
Jesse Hills
e63c7b483b Merge pull request #2505 from esphome/bump-2021.10.0b2
2021.10.0b2
2021-10-13 22:45:29 +13:00
Jesse Hills
f57980b069 Bump version to 2021.10.0b2 2021-10-13 22:30:29 +13:00
Jesse Hills
7006aa0d2a Merge branch 'dev' into beta 2021-10-13 22:11:53 +13:00
Jesse Hills
8051c1ca99 Merge pull request #2504 from esphome/bump-2021.10.0b1
2021.10.0b1
2021-10-13 21:38:41 +13:00
Jesse Hills
a779592414 Bump version to 2021.10.0b1 2021-10-13 16:40:46 +13:00
Jesse Hills
112215848d Merge branch 'dev' into bump-2021.10.0b1 2021-10-13 16:40:46 +13:00
256 changed files with 7918 additions and 2329 deletions

View File

@@ -55,11 +55,13 @@ esphome/components/current_based/* @djwmarcx
esphome/components/daly_bms/* @s1lvi0
esphome/components/dashboard_import/* @esphome/core
esphome/components/debug/* @OttoWinter
esphome/components/delonghi/* @grob6000
esphome/components/dfplayer/* @glmnet
esphome/components/dht/* @OttoWinter
esphome/components/ds1307/* @badbadc0ffee
esphome/components/dsmr/* @glmnet @zuidwijk
esphome/components/ektf2232/* @jesserockz
esphome/components/ens210/* @itn3rd77
esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @jesserockz
esphome/components/esp32_ble_server/* @jesserockz
@@ -86,6 +88,7 @@ esphome/components/honeywellabp/* @RubyBailey
esphome/components/hrxl_maxsonar_wr/* @netmikey
esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/i2c/* @esphome/core
esphome/components/i2s_audio/* @jesserockz
esphome/components/improv_serial/* @esphome/core
esphome/components/ina260/* @MrEditor97
esphome/components/inkbird_ibsth1_mini/* @fkirill
@@ -100,6 +103,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/ltr390/* @sjtrny
esphome/components/max31865/* @DAVe3283
esphome/components/max44009/* @berfenger
esphome/components/max7219digit/* @rspaargaren
esphome/components/max9611/* @mckaymatthew
@@ -117,6 +121,7 @@ esphome/components/mcp47a1/* @jesserockz
esphome/components/mcp9808/* @k7hpn
esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz
esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov
esphome/components/mitsubishi/* @RubyBailey
@@ -166,23 +171,27 @@ esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz
esphome/components/rtttl/* @glmnet
esphome/components/safe_mode/* @jsuanet @paulmonigatti
esphome/components/scd4x/* @sjtrny
esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core
esphome/components/sdm_meter/* @jesserockz @polyfaces
esphome/components/sdp3x/* @Azimath
esphome/components/selec_meter/* @sourabhjaiswal
esphome/components/select/* @esphome/core
esphome/components/sen5x/* @martgras
esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core
esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sgp4x/* @SenexCrenshaw @martgras
esphome/components/shelly_dimmer/* @edge90 @rnauber
esphome/components/sht4x/* @sjtrny
esphome/components/shutdown/* @esphome/core @jsuanet
esphome/components/sim800l/* @glmnet
esphome/components/sm2135/* @BoukeHaarsma23
esphome/components/sml/* @alengwenus
esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/spi/* @esphome/core
esphome/components/sps30/* @martgras
esphome/components/ssd1322_base/* @kbx81
esphome/components/ssd1322_spi/* @kbx81
esphome/components/ssd1325_base/* @kbx81
@@ -217,6 +226,7 @@ esphome/components/tsl2591/* @wjcarpenter
esphome/components/tuya/binary_sensor/* @jesserockz
esphome/components/tuya/climate/* @jesserockz
esphome/components/tuya/number/* @frankiboy1
esphome/components/tuya/select/* @bearpawmaxim
esphome/components/tuya/sensor/* @jesserockz
esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra

View File

@@ -30,6 +30,7 @@ RUN \
iputils-ping=3:20210202-1 \
git=1:2.30.2-1 \
curl=7.74.0-1.3+deb11u1 \
openssh-client=1:8.4p1-5 \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \

View File

@@ -496,83 +496,85 @@ def command_rename(args, config):
)
)
return 1
# Load existing yaml file
with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file:
raw_contents = raw_file.read()
yaml = yaml_util.load_yaml(CORE.config_path)
if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
print(
color(
Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed."
yaml = yaml_util.load_yaml(CORE.config_path)
if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
print(
color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.")
)
return 1
old_name = yaml[CONF_ESPHOME][CONF_NAME]
match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name)
if match is None:
new_raw = re.sub(
rf"name:\s+[\"']?{old_name}[\"']?",
f'name: "{args.name}"',
raw_contents,
)
else:
old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)]
if (
len(
re.findall(
rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?",
raw_contents,
flags=re.MULTILINE,
)
)
return 1
old_name = yaml[CONF_ESPHOME][CONF_NAME]
match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name)
if match is None:
new_raw = re.sub(
rf"name:\s+[\"']?{old_name}[\"']?",
f'name: "{args.name}"',
raw_contents,
)
else:
old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)]
if (
len(
re.findall(
rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?",
raw_contents,
flags=re.MULTILINE,
)
)
> 1
):
print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename"))
return 1
new_raw = re.sub(
rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?",
f'\\1: "{args.name}"',
raw_contents,
flags=re.MULTILINE,
)
raw_file.seek(0)
raw_file.write(new_raw)
raw_file.flush()
print(f"Updating {color(Fore.CYAN, CORE.config_path)}")
print()
rc = run_external_process("esphome", "config", CORE.config_path)
if rc != 0:
raw_file.seek(0)
raw_file.write(raw_contents)
print(color(Fore.BOLD_RED, "Rename failed. Reverting changes."))
> 1
):
print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename"))
return 1
cli_args = [
"run",
CORE.config_path,
"--no-logs",
"--device",
CORE.address,
]
new_raw = re.sub(
rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?",
f'\\1: "{args.name}"',
raw_contents,
flags=re.MULTILINE,
)
if args.dashboard:
cli_args.insert(0, "--dashboard")
new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
print(
f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}"
)
print()
try:
rc = run_external_process("esphome", *cli_args)
except KeyboardInterrupt:
rc = 1
if rc != 0:
raw_file.seek(0)
raw_file.write(raw_contents)
return 1
with open(new_path, mode="w", encoding="utf-8") as new_file:
new_file.write(new_raw)
print(color(Fore.BOLD_GREEN, "SUCCESS"))
print()
return 0
rc = run_external_process("esphome", "config", new_path)
if rc != 0:
print(color(Fore.BOLD_RED, "Rename failed. Reverting changes."))
os.remove(new_path)
return 1
cli_args = [
"run",
new_path,
"--no-logs",
"--device",
CORE.address,
]
if args.dashboard:
cli_args.insert(0, "--dashboard")
try:
rc = run_external_process("esphome", *cli_args)
except KeyboardInterrupt:
rc = 1
if rc != 0:
os.remove(new_path)
return 1
os.remove(CORE.config_path)
print(color(Fore.BOLD_GREEN, "SUCCESS"))
print()
return 0
PRE_CONFIG_ACTIONS = {
@@ -776,7 +778,10 @@ def parse_args(argv):
"configuration", help="Your YAML configuration file(s).", nargs=1
)
parser_rename = subparsers.add_parser("rename")
parser_rename = subparsers.add_parser(
"rename",
help="Rename a device in YAML, compile the binary and upload it.",
)
parser_rename.add_argument(
"configuration", help="Your YAML configuration file.", nargs=1
)

View File

@@ -64,6 +64,7 @@ from esphome.cpp_types import ( # noqa
uint64,
int32,
int64,
size_t,
const_char_ptr,
NAN,
esphome_ns,

View File

@@ -121,7 +121,11 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
// calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic
// also take into account min_power
auto min_us = this->cycle_time_us * this->min_power / 1000;
this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535);
// calculate required value to provide a true RMS voltage output
this->enable_time_us =
std::max((uint32_t) 1, (uint32_t)((65535 - (acos(1 - (2 * this->value / 65535.0)) / 3.14159 * 65535)) *
(this->cycle_time_us - min_us)) /
65535);
if (this->method == DIM_METHOD_LEADING_PULSE) {
// Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone
// this is for brightness near 99%

View File

@@ -15,10 +15,21 @@ namespace esphome {
namespace adc {
static const char *const TAG = "adc";
// 13 bits for S3 / 12 bit for all other esp32 variants
// create a const to avoid the repated cast to enum
// 13bit for S2, and 12bit for all other esp32 variants
#ifdef USE_ESP32
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
#ifndef SOC_ADC_RTC_MAX_BITWIDTH
#if USE_ESP32_VARIANT_ESP32S2
static const int SOC_ADC_RTC_MAX_BITWIDTH = 13;
#else
static const int SOC_ADC_RTC_MAX_BITWIDTH = 12;
#endif
#endif
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; // 4095 (12 bit) or 8191 (13 bit)
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; // 2048 (12 bit) or 4096 (13 bit)
#endif
void ADCSensor::setup() {
@@ -51,8 +62,8 @@ void ADCSensor::setup() {
}
}
// adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2)
// adc_gpio_init doesn't exist on ESP32-S2, ESP32-C3 or ESP32-H2
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) && !defined(USE_ESP32_VARIANT_ESP32S2)
adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_);
#endif
#endif // USE_ESP32
@@ -75,16 +86,16 @@ void ADCSensor::dump_config() {
} else {
switch (this->attenuation_) {
case ADC_ATTEN_DB_0:
ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)");
ESP_LOGCONFIG(TAG, " Attenuation: 0db");
break;
case ADC_ATTEN_DB_2_5:
ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)");
ESP_LOGCONFIG(TAG, " Attenuation: 2.5db");
break;
case ADC_ATTEN_DB_6:
ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)");
ESP_LOGCONFIG(TAG, " Attenuation: 6db");
break;
case ADC_ATTEN_DB_11:
ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)");
ESP_LOGCONFIG(TAG, " Attenuation: 11db");
break;
default: // This is to satisfy the unused ADC_ATTEN_MAX
break;
@@ -129,16 +140,16 @@ float ADCSensor::sample() {
return mv / 1000.0f;
}
int raw11, raw6 = 4095, raw2 = 4095, raw0 = 4095;
int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11);
raw11 = adc1_get_raw(channel_);
if (raw11 < 4095) {
if (raw11 < ADC_MAX) {
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6);
raw6 = adc1_get_raw(channel_);
if (raw6 < 4095) {
if (raw6 < ADC_MAX) {
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5);
raw2 = adc1_get_raw(channel_);
if (raw2 < 4095) {
if (raw2 < ADC_MAX) {
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0);
raw0 = adc1_get_raw(channel_);
}
@@ -154,15 +165,15 @@ float ADCSensor::sample() {
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]);
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]);
// Contribution of each value, in range 0-2048
uint32_t c11 = std::min(raw11, 2048);
uint32_t c6 = 2048 - std::abs(raw6 - 2048);
uint32_t c2 = 2048 - std::abs(raw2 - 2048);
uint32_t c0 = std::min(4095 - raw0, 2048);
// max theoretical csum value is 2048*4 = 8192
// Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC)
uint32_t c11 = std::min(raw11, ADC_HALF);
uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF);
uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF);
uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF);
// max theoretical csum value is 4096*4 = 16384
uint32_t csum = c11 + c6 + c2 + c0;
// each mv is max 3900; so max value is 3900*2048*4, fits in unsigned
// each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32
uint32_t mv_scaled = (mv11 * c11) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0);
return mv_scaled / (float) (csum * 1000U);
}

View File

@@ -40,6 +40,8 @@ class AddressableLightDisplay : public display::DisplayBuffer, public PollingCom
void setup() override;
void display();
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
protected:
int get_width_internal() override;
int get_height_internal() override;

View File

@@ -76,6 +76,8 @@ async def to_code(config):
pos = 0
for frameIndex in range(frames):
image.seek(frameIndex)
if CONF_RESIZE in config:
image.thumbnail(config[CONF_RESIZE])
frame = image.convert("RGB")
if CONF_RESIZE in config:
frame = frame.resize([width, height])
@@ -92,6 +94,29 @@ async def to_code(config):
data[pos] = pix[2]
pos += 1
elif config[CONF_TYPE] == "RGB565":
data = [0 for _ in range(height * width * 2 * frames)]
pos = 0
for frameIndex in range(frames):
image.seek(frameIndex)
frame = image.convert("RGB")
if CONF_RESIZE in config:
frame = frame.resize([width, height])
pixels = list(frame.getdata())
if len(pixels) != height * width:
raise core.EsphomeError(
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
)
for pix in pixels:
R = pix[0] >> 3
G = pix[1] >> 2
B = pix[2] >> 3
rgb = (R << 11) | (G << 5) | B
data[pos] = rgb >> 8
pos += 1
data[pos] = rgb & 255
pos += 1
elif config[CONF_TYPE] == "BINARY":
width8 = ((width + 7) // 8) * 8
data = [0 for _ in range((height * width8 // 8) * frames)]

View File

@@ -42,6 +42,7 @@ service APIConnection {
rpc select_command (SelectCommandRequest) returns (void) {}
rpc button_command (ButtonCommandRequest) returns (void) {}
rpc lock_command (LockCommandRequest) returns (void) {}
rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
}
@@ -991,7 +992,7 @@ message ListEntitiesLockResponse {
bool supports_open = 9;
bool requires_code = 10;
# Not yet implemented:
// Not yet implemented:
string code_format = 11;
}
message LockStateResponse {
@@ -1010,7 +1011,7 @@ message LockCommandRequest {
fixed32 key = 1;
LockCommand command = 2;
# Not yet implemented:
// Not yet implemented:
bool has_code = 3;
string code = 4;
}
@@ -1040,3 +1041,60 @@ message ButtonCommandRequest {
fixed32 key = 1;
}
// ==================== MEDIA PLAYER ====================
enum MediaPlayerState {
MEDIA_PLAYER_STATE_NONE = 0;
MEDIA_PLAYER_STATE_IDLE = 1;
MEDIA_PLAYER_STATE_PLAYING = 2;
MEDIA_PLAYER_STATE_PAUSED = 3;
}
enum MediaPlayerCommand {
MEDIA_PLAYER_COMMAND_PLAY = 0;
MEDIA_PLAYER_COMMAND_PAUSE = 1;
MEDIA_PLAYER_COMMAND_STOP = 2;
MEDIA_PLAYER_COMMAND_MUTE = 3;
MEDIA_PLAYER_COMMAND_UNMUTE = 4;
}
message ListEntitiesMediaPlayerResponse {
option (id) = 63;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_MEDIA_PLAYER";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
bool supports_pause = 8;
}
message MediaPlayerStateResponse {
option (id) = 64;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_MEDIA_PLAYER";
option (no_delay) = true;
fixed32 key = 1;
MediaPlayerState state = 2;
float volume = 3;
bool muted = 4;
}
message MediaPlayerCommandRequest {
option (id) = 65;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_MEDIA_PLAYER";
option (no_delay) = true;
fixed32 key = 1;
bool has_command = 2;
MediaPlayerCommand command = 3;
bool has_volume = 4;
float volume = 5;
bool has_media_url = 6;
string media_url = 7;
}

View File

@@ -12,9 +12,6 @@
#ifdef USE_HOMEASSISTANT_TIME
#include "esphome/components/homeassistant/time/homeassistant_time.h"
#endif
#ifdef USE_FAN
#include "esphome/components/fan/fan_helpers.h"
#endif
namespace esphome {
namespace api {
@@ -253,9 +250,6 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
#endif
#ifdef USE_FAN
// Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
bool APIConnection::send_fan_state(fan::Fan *fan) {
if (!this->state_subscription_)
return false;
@@ -268,7 +262,6 @@ bool APIConnection::send_fan_state(fan::Fan *fan) {
resp.oscillating = fan->oscillating;
if (traits.supports_speed()) {
resp.speed_level = fan->speed;
resp.speed = static_cast<enums::FanSpeed>(fan::speed_level_to_enum(fan->speed, traits.supported_speed_count()));
}
if (traits.supports_direction())
resp.direction = static_cast<enums::FanDirection>(fan->direction);
@@ -295,8 +288,6 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
if (fan == nullptr)
return;
auto traits = fan->get_traits();
auto call = fan->make_call();
if (msg.has_state)
call.set_state(msg.state);
@@ -305,14 +296,11 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
if (msg.has_speed_level) {
// Prefer level
call.set_speed(msg.speed_level);
} else if (msg.has_speed) {
call.set_speed(fan::speed_enum_to_level(static_cast<fan::FanSpeed>(msg.speed), traits.supported_speed_count()));
}
if (msg.has_direction)
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
call.perform();
}
#pragma GCC diagnostic pop
#endif
#ifdef USE_LIGHT
@@ -745,6 +733,52 @@ void APIConnection::lock_command(const LockCommandRequest &msg) {
}
#endif
#ifdef USE_MEDIA_PLAYER
bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) {
if (!this->state_subscription_)
return false;
MediaPlayerStateResponse resp{};
resp.key = media_player->get_object_id_hash();
resp.state = static_cast<enums::MediaPlayerState>(media_player->state);
resp.volume = media_player->volume;
resp.muted = media_player->is_muted();
return this->send_media_player_state_response(resp);
}
bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) {
ListEntitiesMediaPlayerResponse msg;
msg.key = media_player->get_object_id_hash();
msg.object_id = media_player->get_object_id();
msg.name = media_player->get_name();
msg.unique_id = get_default_unique_id("media_player", media_player);
msg.icon = media_player->get_icon();
msg.disabled_by_default = media_player->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(media_player->get_entity_category());
auto traits = media_player->get_traits();
msg.supports_pause = traits.get_supports_pause();
return this->send_list_entities_media_player_response(msg);
}
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key);
if (media_player == nullptr)
return;
auto call = media_player->make_call();
if (msg.has_command) {
call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command));
}
if (msg.has_volume) {
call.set_volume(msg.volume);
}
if (msg.has_media_url) {
call.set_media_url(msg.media_url);
}
call.perform();
}
#endif
#ifdef USE_ESP32_CAMERA
void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
if (!this->state_subscription_)

View File

@@ -82,6 +82,11 @@ class APIConnection : public APIServerConnection {
bool send_lock_state(lock::Lock *a_lock, lock::LockState state);
bool send_lock_info(lock::Lock *a_lock);
void lock_command(const LockCommandRequest &msg) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool send_media_player_state(media_player::MediaPlayer *media_player);
bool send_media_player_info(media_player::MediaPlayer *media_player);
void media_player_command(const MediaPlayerCommandRequest &msg) override;
#endif
bool send_log_message(int level, const char *tag, const char *line);
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {

View File

@@ -308,6 +308,36 @@ template<> const char *proto_enum_to_string<enums::LockCommand>(enums::LockComma
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::MediaPlayerState>(enums::MediaPlayerState value) {
switch (value) {
case enums::MEDIA_PLAYER_STATE_NONE:
return "MEDIA_PLAYER_STATE_NONE";
case enums::MEDIA_PLAYER_STATE_IDLE:
return "MEDIA_PLAYER_STATE_IDLE";
case enums::MEDIA_PLAYER_STATE_PLAYING:
return "MEDIA_PLAYER_STATE_PLAYING";
case enums::MEDIA_PLAYER_STATE_PAUSED:
return "MEDIA_PLAYER_STATE_PAUSED";
default:
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::MediaPlayerCommand>(enums::MediaPlayerCommand value) {
switch (value) {
case enums::MEDIA_PLAYER_COMMAND_PLAY:
return "MEDIA_PLAYER_COMMAND_PLAY";
case enums::MEDIA_PLAYER_COMMAND_PAUSE:
return "MEDIA_PLAYER_COMMAND_PAUSE";
case enums::MEDIA_PLAYER_COMMAND_STOP:
return "MEDIA_PLAYER_COMMAND_STOP";
case enums::MEDIA_PLAYER_COMMAND_MUTE:
return "MEDIA_PLAYER_COMMAND_MUTE";
case enums::MEDIA_PLAYER_COMMAND_UNMUTE:
return "MEDIA_PLAYER_COMMAND_UNMUTE";
default:
return "UNKNOWN";
}
}
bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
@@ -4574,6 +4604,254 @@ void ButtonCommandRequest::dump_to(std::string &out) const {
out.append("}");
}
#endif
bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 6: {
this->disabled_by_default = value.as_bool();
return true;
}
case 7: {
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 8: {
this->supports_pause = value.as_bool();
return true;
}
default:
return false;
}
}
bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->object_id = value.as_string();
return true;
}
case 3: {
this->name = value.as_string();
return true;
}
case 4: {
this->unique_id = value.as_string();
return true;
}
case 5: {
this->icon = value.as_string();
return true;
}
default:
return false;
}
}
bool ListEntitiesMediaPlayerResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 2: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name);
buffer.encode_string(4, this->unique_id);
buffer.encode_string(5, this->icon);
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_bool(8, this->supports_pause);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("ListEntitiesMediaPlayerResponse {\n");
out.append(" object_id: ");
out.append("'").append(this->object_id).append("'");
out.append("\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" name: ");
out.append("'").append(this->name).append("'");
out.append("\n");
out.append(" unique_id: ");
out.append("'").append(this->unique_id).append("'");
out.append("\n");
out.append(" icon: ");
out.append("'").append(this->icon).append("'");
out.append("\n");
out.append(" disabled_by_default: ");
out.append(YESNO(this->disabled_by_default));
out.append("\n");
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" supports_pause: ");
out.append(YESNO(this->supports_pause));
out.append("\n");
out.append("}");
}
#endif
bool MediaPlayerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->state = value.as_enum<enums::MediaPlayerState>();
return true;
}
case 4: {
this->muted = value.as_bool();
return true;
}
default:
return false;
}
}
bool MediaPlayerStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
case 3: {
this->volume = value.as_float();
return true;
}
default:
return false;
}
}
void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_enum<enums::MediaPlayerState>(2, this->state);
buffer.encode_float(3, this->volume);
buffer.encode_bool(4, this->muted);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void MediaPlayerStateResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("MediaPlayerStateResponse {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" state: ");
out.append(proto_enum_to_string<enums::MediaPlayerState>(this->state));
out.append("\n");
out.append(" volume: ");
sprintf(buffer, "%g", this->volume);
out.append(buffer);
out.append("\n");
out.append(" muted: ");
out.append(YESNO(this->muted));
out.append("\n");
out.append("}");
}
#endif
bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->has_command = value.as_bool();
return true;
}
case 3: {
this->command = value.as_enum<enums::MediaPlayerCommand>();
return true;
}
case 4: {
this->has_volume = value.as_bool();
return true;
}
case 6: {
this->has_media_url = value.as_bool();
return true;
}
default:
return false;
}
}
bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 7: {
this->media_url = value.as_string();
return true;
}
default:
return false;
}
}
bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
case 5: {
this->volume = value.as_float();
return true;
}
default:
return false;
}
}
void MediaPlayerCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_bool(2, this->has_command);
buffer.encode_enum<enums::MediaPlayerCommand>(3, this->command);
buffer.encode_bool(4, this->has_volume);
buffer.encode_float(5, this->volume);
buffer.encode_bool(6, this->has_media_url);
buffer.encode_string(7, this->media_url);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void MediaPlayerCommandRequest::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("MediaPlayerCommandRequest {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" has_command: ");
out.append(YESNO(this->has_command));
out.append("\n");
out.append(" command: ");
out.append(proto_enum_to_string<enums::MediaPlayerCommand>(this->command));
out.append("\n");
out.append(" has_volume: ");
out.append(YESNO(this->has_volume));
out.append("\n");
out.append(" volume: ");
sprintf(buffer, "%g", this->volume);
out.append(buffer);
out.append("\n");
out.append(" has_media_url: ");
out.append(YESNO(this->has_media_url));
out.append("\n");
out.append(" media_url: ");
out.append("'").append(this->media_url).append("'");
out.append("\n");
out.append("}");
}
#endif
} // namespace api
} // namespace esphome

View File

@@ -141,6 +141,19 @@ enum LockCommand : uint32_t {
LOCK_LOCK = 1,
LOCK_OPEN = 2,
};
enum MediaPlayerState : uint32_t {
MEDIA_PLAYER_STATE_NONE = 0,
MEDIA_PLAYER_STATE_IDLE = 1,
MEDIA_PLAYER_STATE_PLAYING = 2,
MEDIA_PLAYER_STATE_PAUSED = 3,
};
enum MediaPlayerCommand : uint32_t {
MEDIA_PLAYER_COMMAND_PLAY = 0,
MEDIA_PLAYER_COMMAND_PAUSE = 1,
MEDIA_PLAYER_COMMAND_STOP = 2,
MEDIA_PLAYER_COMMAND_MUTE = 3,
MEDIA_PLAYER_COMMAND_UNMUTE = 4,
};
} // namespace enums
@@ -1146,6 +1159,60 @@ class ButtonCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
};
class ListEntitiesMediaPlayerResponse : public ProtoMessage {
public:
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
bool supports_pause{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class MediaPlayerStateResponse : public ProtoMessage {
public:
uint32_t key{0};
enums::MediaPlayerState state{};
float volume{0.0f};
bool muted{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class MediaPlayerCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
bool has_command{false};
enums::MediaPlayerCommand command{};
bool has_volume{false};
float volume{0.0f};
bool has_media_url{false};
std::string media_url{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
} // namespace api
} // namespace esphome

View File

@@ -310,6 +310,24 @@ bool APIServerConnectionBase::send_list_entities_button_response(const ListEntit
#endif
#ifdef USE_BUTTON
#endif
#ifdef USE_MEDIA_PLAYER
bool APIServerConnectionBase::send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_list_entities_media_player_response: %s", msg.dump().c_str());
#endif
return this->send_message_<ListEntitiesMediaPlayerResponse>(msg, 63);
}
#endif
#ifdef USE_MEDIA_PLAYER
bool APIServerConnectionBase::send_media_player_state_response(const MediaPlayerStateResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_media_player_state_response: %s", msg.dump().c_str());
#endif
return this->send_message_<MediaPlayerStateResponse>(msg, 64);
}
#endif
#ifdef USE_MEDIA_PLAYER
#endif
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
switch (msg_type) {
case 1: {
@@ -563,6 +581,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str());
#endif
this->on_button_command_request(msg);
#endif
break;
}
case 65: {
#ifdef USE_MEDIA_PLAYER
MediaPlayerCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str());
#endif
this->on_media_player_command_request(msg);
#endif
break;
}
@@ -813,6 +842,19 @@ void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg)
this->lock_command(msg);
}
#endif
#ifdef USE_MEDIA_PLAYER
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->media_player_command(msg);
}
#endif
} // namespace api
} // namespace esphome

View File

@@ -144,6 +144,15 @@ class APIServerConnectionBase : public ProtoService {
#endif
#ifdef USE_BUTTON
virtual void on_button_command_request(const ButtonCommandRequest &value){};
#endif
#ifdef USE_MEDIA_PLAYER
bool send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg);
#endif
#ifdef USE_MEDIA_PLAYER
bool send_media_player_state_response(const MediaPlayerStateResponse &msg);
#endif
#ifdef USE_MEDIA_PLAYER
virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){};
#endif
protected:
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
@@ -192,6 +201,9 @@ class APIServerConnection : public APIServerConnectionBase {
#endif
#ifdef USE_LOCK
virtual void lock_command(const LockCommandRequest &msg) = 0;
#endif
#ifdef USE_MEDIA_PLAYER
virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
#endif
protected:
void on_hello_request(const HelloRequest &msg) override;
@@ -236,6 +248,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_LOCK
void on_lock_command_request(const LockCommandRequest &msg) override;
#endif
#ifdef USE_MEDIA_PLAYER
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
#endif
};
} // namespace api

View File

@@ -255,7 +255,7 @@ void APIServer::on_number_update(number::Number *obj, float state) {
#endif
#ifdef USE_SELECT
void APIServer::on_select_update(select::Select *obj, const std::string &state) {
void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
@@ -272,6 +272,15 @@ void APIServer::on_lock_update(lock::Lock *obj) {
}
#endif
#ifdef USE_MEDIA_PLAYER
void APIServer::on_media_player_update(media_player::MediaPlayer *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_media_player_state(obj);
}
#endif
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void APIServer::set_port(uint16_t port) { this->port_ = port; }
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -64,10 +64,13 @@ class APIServer : public Component, public Controller {
void on_number_update(number::Number *obj, float state) override;
#endif
#ifdef USE_SELECT
void on_select_update(select::Select *obj, const std::string &state) override;
void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
#endif
#ifdef USE_LOCK
void on_lock_update(lock::Lock *obj) override;
#endif
#ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }

View File

@@ -64,5 +64,11 @@ bool ListEntitiesIterator::on_number(number::Number *number) { return this->clie
bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); }
#endif
#ifdef USE_MEDIA_PLAYER
bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) {
return this->client_->send_media_player_info(media_player);
}
#endif
} // namespace api
} // namespace esphome

View File

@@ -51,6 +51,9 @@ class ListEntitiesIterator : public ComponentIterator {
#endif
#ifdef USE_LOCK
bool on_lock(lock::Lock *a_lock) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool on_media_player(media_player::MediaPlayer *media_player) override;
#endif
bool on_end() override;

View File

@@ -50,6 +50,11 @@ bool InitialStateIterator::on_select(select::Select *select) {
#ifdef USE_LOCK
bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); }
#endif
#ifdef USE_MEDIA_PLAYER
bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) {
return this->client_->send_media_player_state(media_player);
}
#endif
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
} // namespace api

View File

@@ -48,6 +48,9 @@ class InitialStateIterator : public ComponentIterator {
#endif
#ifdef USE_LOCK
bool on_lock(lock::Lock *a_lock) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool on_media_player(media_player::MediaPlayer *media_player) override;
#endif
protected:
APIConnection *client_;

View File

@@ -36,6 +36,14 @@ static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
return -1;
}
static BedjetButton heat_button(BedjetHeatMode mode) {
BedjetButton btn = BTN_HEAT;
if (mode == HEAT_MODE_EXTENDED) {
btn = BTN_EXTHT;
}
return btn;
}
void Bedjet::upgrade_firmware() {
auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE);
auto status = this->write_bedjet_packet_(pkt);
@@ -117,7 +125,7 @@ void Bedjet::control(const ClimateCall &call) {
pkt = this->codec_->get_button_request(BTN_OFF);
break;
case climate::CLIMATE_MODE_HEAT:
pkt = this->codec_->get_button_request(BTN_EXTHT);
pkt = this->codec_->get_button_request(heat_button(this->heating_mode_));
break;
case climate::CLIMATE_MODE_FAN_ONLY:
pkt = this->codec_->get_button_request(BTN_COOL);
@@ -137,7 +145,7 @@ void Bedjet::control(const ClimateCall &call) {
} else {
this->force_refresh_ = true;
this->mode = mode;
// We're using (custom) preset for Turbo & M1-3 presets, so changing climate mode will clear those
// We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
this->custom_preset.reset();
this->preset.reset();
}
@@ -186,6 +194,10 @@ void Bedjet::control(const ClimateCall &call) {
pkt = this->codec_->get_button_request(BTN_M2);
} else if (preset == "M3") {
pkt = this->codec_->get_button_request(BTN_M3);
} else if (preset == "LTD HT") {
pkt = this->codec_->get_button_request(BTN_HEAT);
} else if (preset == "EXT HT") {
pkt = this->codec_->get_button_request(BTN_EXTHT);
} else {
ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
return;
@@ -296,7 +308,7 @@ void Bedjet::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
#ifdef USE_TIME
if (this->time_id_.has_value()) {
this->send_local_time_();
this->send_local_time();
}
#endif
break;
@@ -451,40 +463,47 @@ uint8_t Bedjet::write_notify_config_descriptor_(bool enable) {
#ifdef USE_TIME
/** Attempts to sync the local time (via `time_id`) to the BedJet device. */
void Bedjet::send_local_time_() {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str());
return;
}
auto *time_id = *this->time_id_;
time::ESPTime now = time_id->now();
if (now.is_valid()) {
uint8_t hour = now.hour;
uint8_t minute = now.minute;
BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute);
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status);
} else {
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute);
void Bedjet::send_local_time() {
if (this->time_id_.has_value()) {
auto *time_id = *this->time_id_;
time::ESPTime now = time_id->now();
if (now.is_valid()) {
this->set_clock(now.hour, now.minute);
ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute);
}
} else {
ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
}
}
/** Initializes time sync callbacks to support syncing current time to the BedJet. */
void Bedjet::setup_time_() {
if (this->time_id_.has_value()) {
this->send_local_time_();
this->send_local_time();
auto *time_id = *this->time_id_;
time_id->add_on_time_sync_callback([this] { this->send_local_time_(); });
time::ESPTime now = time_id->now();
ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute);
time_id->add_on_time_sync_callback([this] { this->send_local_time(); });
} else {
ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
}
}
#endif
/** Attempt to set the BedJet device's clock to the specified time. */
void Bedjet::set_clock(uint8_t hour, uint8_t minute) {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str());
return;
}
BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute);
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status);
} else {
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute);
}
}
/** Writes one BedjetPacket to the BLE client on the BEDJET_COMMAND_UUID. */
uint8_t Bedjet::write_bedjet_packet_(BedjetPacket *pkt) {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
@@ -555,11 +574,25 @@ bool Bedjet::update_status_() {
break;
case MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
this->action = climate::CLIMATE_ACTION_HEATING;
this->preset.reset();
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
this->set_custom_preset_("LTD HT");
} else {
this->custom_preset.reset();
}
break;
case MODE_EXTHT:
this->mode = climate::CLIMATE_MODE_HEAT;
this->action = climate::CLIMATE_ACTION_HEATING;
this->custom_preset.reset();
this->preset.reset();
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
this->custom_preset.reset();
} else {
this->set_custom_preset_("EXT HT");
}
break;
case MODE_COOL:

View File

@@ -4,6 +4,7 @@
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/climate/climate.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "bedjet_base.h"
@@ -37,8 +38,12 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
#ifdef USE_TIME
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
void send_local_time();
#endif
void set_clock(uint8_t hour, uint8_t minute);
void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; }
/** Sets the default strategy to use for climate::CLIMATE_MODE_HEAT. */
void set_heating_mode(BedjetHeatMode mode) { this->heating_mode_ = mode; }
/** Attempts to check for and apply firmware updates. */
void upgrade_firmware();
@@ -67,10 +72,17 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
// We could fetch biodata from bedjet and set these names that way.
// But then we have to invert the lookup in order to send the right preset.
// For now, we can leave them as M1-3 to match the remote buttons.
// EXT HT added to match remote button.
"EXT HT",
"M1",
"M2",
"M3",
});
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
traits.add_supported_custom_preset("LTD HT");
} else {
traits.add_supported_custom_preset("EXT HT");
}
traits.set_visual_min_temperature(19.0);
traits.set_visual_max_temperature(43.0);
traits.set_visual_temperature_step(1.0);
@@ -82,11 +94,11 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
#ifdef USE_TIME
void setup_time_();
void send_local_time_();
optional<time::RealTimeClock *> time_id_{};
#endif
uint32_t timeout_{DEFAULT_STATUS_TIMEOUT};
BedjetHeatMode heating_mode_ = HEAT_MODE_HEAT;
static const uint32_t MIN_NOTIFY_THROTTLE = 5000;
static const uint32_t NOTIFY_WARN_THRESHOLD = 300000;

View File

@@ -24,6 +24,14 @@ enum BedjetMode : uint8_t {
MODE_WAIT = 6,
};
/** Optional heating strategies to use for climate::CLIMATE_MODE_HEAT. */
enum BedjetHeatMode {
/// HVACMode.HEAT is handled using BTN_HEAT (default)
HEAT_MODE_HEAT,
/// HVACMode.HEAT is handled using BTN_EXTHT
HEAT_MODE_EXTENDED,
};
enum BedjetButton : uint8_t {
/// Turn BedJet off
BTN_OFF = 0x1,
@@ -66,8 +74,8 @@ enum BedjetCommand : uint8_t {
#define BEDJET_FAN_STEP_NAMES_ \
{ \
" 5%", " 10%", " 15%", " 20%", " 25%", " 30%", " 35%", " 40%", " 45%", " 50%", " 55%", " 60%", " 65%", " 70%", \
" 75%", " 80%", " 85%", " 90%", " 95%", "100%" \
"5%", "10%", "15%", "20%", "25%", "30%", "35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", \
"85%", "90%", "95%", "100%" \
}
static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_;

View File

@@ -2,6 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import climate, ble_client, time
from esphome.const import (
CONF_HEAT_MODE,
CONF_ID,
CONF_RECEIVE_TIMEOUT,
CONF_TIME_ID,
@@ -14,11 +15,19 @@ bedjet_ns = cg.esphome_ns.namespace("bedjet")
Bedjet = bedjet_ns.class_(
"Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent
)
BedjetHeatMode = bedjet_ns.enum("BedjetHeatMode")
BEDJET_HEAT_MODES = {
"heat": BedjetHeatMode.HEAT_MODE_HEAT,
"extended": BedjetHeatMode.HEAT_MODE_EXTENDED,
}
CONFIG_SCHEMA = (
climate.CLIMATE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(Bedjet),
cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum(
BEDJET_HEAT_MODES, lower=True
),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Optional(
CONF_RECEIVE_TIMEOUT, default="0s"
@@ -35,6 +44,7 @@ async def to_code(config):
await cg.register_component(var, config)
await climate.register_climate(var, config)
await ble_client.register_ble_node(var, config)
cg.add(var.set_heating_mode(config[CONF_HEAT_MODE]))
if CONF_TIME_ID in config:
time_ = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_time_id(time_))

View File

@@ -69,7 +69,6 @@ void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
}
}
bool BinarySensor::has_state() const { return this->has_state_; }
uint32_t BinarySensor::hash_base() { return 1210250844UL; }
bool BinarySensor::is_status_binary_sensor() const { return false; }
} // namespace binary_sensor

View File

@@ -76,8 +76,6 @@ class BinarySensor : public EntityBase {
virtual std::string device_class();
protected:
uint32_t hash_base() override;
CallbackManager<void(bool)> state_callback_{};
optional<std::string> device_class_{}; ///< Stores the override of the device class
Filter *filter_list_{nullptr};

View File

@@ -11,8 +11,6 @@ namespace ble_client {
static const char *const TAG = "ble_sensor";
uint32_t BLESensor::hash_base() { return 343459825UL; }
void BLESensor::loop() {}
void BLESensor::dump_config() {

View File

@@ -37,7 +37,6 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
uint16_t handle;
protected:
uint32_t hash_base() override;
float parse_data_(uint8_t *value, uint16_t value_len);
optional<data_to_value_t> data_to_value_func_{};
bool notify_;

View File

@@ -14,8 +14,6 @@ static const char *const TAG = "ble_text_sensor";
static const std::string EMPTY = "";
uint32_t BLETextSensor::hash_base() { return 193967603UL; }
void BLETextSensor::loop() {}
void BLETextSensor::dump_config() {

View File

@@ -35,7 +35,6 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p
uint16_t handle;
protected:
uint32_t hash_base() override;
bool notify_;
espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_;

View File

@@ -81,6 +81,11 @@ static const char *iir_filter_to_str(BME280IIRFilter filter) {
void BME280Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up BME280...");
uint8_t chip_id = 0;
// Mark as not failed before initializing. Some devices will turn off sensors to save on batteries
// and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component.
this->component_state_ &= ~COMPONENT_STATE_FAILED;
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();

View File

@@ -169,6 +169,14 @@ void BME680BSECComponent::loop() {
} else {
this->status_clear_warning();
}
// Process a single action from the queue. These are primarily sensor state publishes
// that in totality take too long to send in a single call.
if (this->queue_.size()) {
auto action = std::move(this->queue_.front());
this->queue_.pop();
action();
}
}
void BME680BSECComponent::run_() {
@@ -306,37 +314,39 @@ void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme
}
void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) {
ESP_LOGV(TAG, "Publishing sensor states");
ESP_LOGV(TAG, "Queuing sensor state publish actions");
for (uint8_t i = 0; i < num_outputs; i++) {
float signal = outputs[i].signal;
switch (outputs[i].sensor_id) {
case BSEC_OUTPUT_IAQ:
case BSEC_OUTPUT_STATIC_IAQ:
uint8_t accuracy;
accuracy = outputs[i].accuracy;
this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal);
this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true);
case BSEC_OUTPUT_STATIC_IAQ: {
uint8_t accuracy = outputs[i].accuracy;
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
this->queue_push_([this, accuracy]() {
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
});
this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
// Queue up an opportunity to save state
this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); });
break;
this->queue_push_([this, accuracy]() { this->save_state_(accuracy); });
} break;
case BSEC_OUTPUT_CO2_EQUIVALENT:
this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal);
this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); });
break;
case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal);
this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); });
break;
case BSEC_OUTPUT_RAW_PRESSURE:
this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f);
this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); });
break;
case BSEC_OUTPUT_RAW_GAS:
this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal);
this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); });
break;
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE:
this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal);
this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); });
break;
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY:
this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal);
this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); });
break;
}
}
@@ -352,14 +362,14 @@ int64_t BME680BSECComponent::get_time_ns_() {
return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000);
}
void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) {
void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, bool change_only) {
if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) {
return;
}
sensor->publish_state(value);
}
void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) {
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) {
if (!sensor || (sensor->has_state() && sensor->state == value)) {
return;
}

View File

@@ -70,12 +70,14 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
void publish_(const bsec_output_t *outputs, uint8_t num_outputs);
int64_t get_time_ns_();
void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false);
void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value);
void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false);
void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value);
void load_state_();
void save_state_(uint8_t accuracy);
void queue_push_(std::function<void()> &&f) { this->queue_.push(std::move(f)); }
struct bme680_dev bme680_;
bsec_library_return_t bsec_status_{BSEC_OK};
int8_t bme680_status_{BME680_OK};
@@ -84,6 +86,8 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
uint32_t millis_overflow_counter_{0};
int64_t next_call_ns_{0};
std::queue<std::function<void()>> queue_;
ESPPreferenceObject bsec_state_;
uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day
uint32_t last_state_save_ms_ = 0;

View File

@@ -25,7 +25,7 @@ OVERSAMPLING_OPTIONS = {
"4X": Oversampling.OVERSAMPLING_X4,
"8X": Oversampling.OVERSAMPLING_X8,
"16X": Oversampling.OVERSAMPLING_X16,
"32x": Oversampling.OVERSAMPLING_X32,
"32X": Oversampling.OVERSAMPLING_X32,
}
IIRFilter = bmp3xx_ns.enum("IIRFilter")

View File

@@ -15,7 +15,6 @@ void Button::press() {
this->press_callback_.call();
}
void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); }
uint32_t Button::hash_base() { return 1495763804UL; }
void Button::set_device_class(const std::string &device_class) { this->device_class_ = device_class; }
std::string Button::get_device_class() { return this->device_class_; }

View File

@@ -47,8 +47,6 @@ class Button : public EntityBase {
*/
virtual void press_action() = 0;
uint32_t hash_base() override;
CallbackManager<void()> press_callback_{};
std::string device_class_{};
};

View File

@@ -78,6 +78,7 @@ CANBUS_SCHEMA = cv.Schema(
min=0, max=0x1FFFFFFF
),
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST): cv.boolean,
},
validate_id,
),
@@ -100,10 +101,20 @@ async def setup_canbus_core_(var, config):
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id
)
if CONF_REMOTE_TRANSMISSION_REQUEST in conf:
cg.add(
trigger.set_remote_transmission_request(
conf[CONF_REMOTE_TRANSMISSION_REQUEST]
)
)
await cg.register_component(trigger, conf)
await automation.build_automation(
trigger,
[(cg.std_vector.template(cg.uint8), "x"), (cg.uint32, "can_id")],
[
(cg.std_vector.template(cg.uint8), "x"),
(cg.uint32, "can_id"),
(cg.bool_, "remote_transmission_request"),
],
conf,
)

View File

@@ -81,8 +81,10 @@ void Canbus::loop() {
// fire all triggers
for (auto *trigger : this->triggers_) {
if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) &&
(trigger->use_extended_id_ == can_message.use_extended_id)) {
trigger->trigger(data, can_message.can_id);
(trigger->use_extended_id_ == can_message.use_extended_id) &&
(!trigger->remote_transmission_request_.has_value() ||
trigger->remote_transmission_request_.value() == can_message.remote_transmission_request)) {
trigger->trigger(data, can_message.can_id, can_message.remote_transmission_request);
}
}
}

View File

@@ -126,13 +126,18 @@ template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public P
std::vector<uint8_t> data_static_{};
};
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t>, public Component {
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {
friend class Canbus;
public:
explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask,
const bool use_extended_id)
: parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id){};
void set_remote_transmission_request(bool remote_transmission_request) {
this->remote_transmission_request_ = remote_transmission_request;
}
void setup() override { this->parent_->add_trigger(this); }
protected:
@@ -140,6 +145,7 @@ class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t>, public Com
uint32_t can_id_;
uint32_t can_id_mask_;
bool use_extended_id_;
optional<bool> remote_transmission_request_{};
};
} // namespace canbus

View File

@@ -39,17 +39,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
if (request->method() == HTTP_GET) {
if (request->url() == "/")
return true;
if (request->url() == "/stylesheet.css")
return true;
if (request->url() == "/wifi-strength-1.svg")
return true;
if (request->url() == "/wifi-strength-2.svg")
return true;
if (request->url() == "/wifi-strength-3.svg")
return true;
if (request->url() == "/wifi-strength-4.svg")
return true;
if (request->url() == "/lock.svg")
if (request->url() == "/config.json")
return true;
if (request->url() == "/wifisave")
return true;

View File

@@ -287,9 +287,11 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
validate_climate_fan_mode
),
cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.string_strict,
cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.templatable(
cv.string_strict
),
cv.Exclusive(CONF_PRESET, "preset"): cv.templatable(validate_climate_preset),
cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.string_strict,
cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.templatable(cv.string_strict),
cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode),
}
)
@@ -324,13 +326,17 @@ async def climate_control_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
cg.add(var.set_fan_mode(template_))
if CONF_CUSTOM_FAN_MODE in config:
template_ = await cg.templatable(config[CONF_CUSTOM_FAN_MODE], args, str)
template_ = await cg.templatable(
config[CONF_CUSTOM_FAN_MODE], args, cg.std_string
)
cg.add(var.set_custom_fan_mode(template_))
if CONF_PRESET in config:
template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset)
cg.add(var.set_preset(template_))
if CONF_CUSTOM_PRESET in config:
template_ = await cg.templatable(config[CONF_CUSTOM_PRESET], args, str)
template_ = await cg.templatable(
config[CONF_CUSTOM_PRESET], args, cg.std_string
)
cg.add(var.set_custom_preset(template_))
if CONF_SWING_MODE in config:
template_ = await cg.templatable(

View File

@@ -419,7 +419,6 @@ void Climate::publish_state() {
// Save state
this->save_state_();
}
uint32_t Climate::hash_base() { return 3104134496UL; }
ClimateTraits Climate::get_traits() {
auto traits = this->traits();

View File

@@ -282,7 +282,6 @@ class Climate : public EntityBase {
*/
void save_state_();
uint32_t hash_base() override;
void dump_traits_(const char *tag);
CallbackManager<void()> state_callback_{};

View File

@@ -7,7 +7,7 @@ namespace copy {
static const char *const TAG = "copy.select";
void CopySelect::setup() {
source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); });
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
traits.set_options(source_->traits.get_options());

View File

@@ -33,8 +33,6 @@ const char *cover_operation_to_str(CoverOperation op) {
Cover::Cover(const std::string &name) : EntityBase(name), position{COVER_OPEN} {}
uint32_t Cover::hash_base() { return 1727367479UL; }
CoverCall::CoverCall(Cover *parent) : parent_(parent) {}
CoverCall &CoverCall::set_command(const char *command) {
if (strcasecmp(command, "OPEN") == 0) {

View File

@@ -177,7 +177,6 @@ class Cover : public EntityBase {
virtual std::string device_class();
optional<CoverRestoreState> restore_state_();
uint32_t hash_base() override;
CallbackManager<void()> state_callback_{};
optional<std::string> device_class_override_{};

View File

@@ -64,7 +64,10 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N
config = {
"substitutions": {"name": name},
"packages": {project_name: import_url},
"esphome": {"name_add_mac_suffix": False},
"esphome": {
"name": "${name}",
"name_add_mac_suffix": False,
},
}
p.write_text(
dump(config) + WIFI_CONFIG,

View File

@@ -93,7 +93,14 @@ deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component)
EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action)
PreventDeepSleepAction = deep_sleep_ns.class_(
"PreventDeepSleepAction", automation.Action
"PreventDeepSleepAction",
automation.Action,
cg.Parented.template(DeepSleepComponent),
)
AllowDeepSleepAction = deep_sleep_ns.class_(
"AllowDeepSleepAction",
automation.Action,
cg.Parented.template(DeepSleepComponent),
)
WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode")
@@ -208,28 +215,32 @@ async def to_code(config):
cg.add_define("USE_DEEP_SLEEP")
DEEP_SLEEP_ENTER_SCHEMA = cv.All(
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(DeepSleepComponent),
cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable(
cv.positive_time_period_milliseconds
),
# Only on ESP32 due to how long the RTC on ESP8266 can stay asleep
cv.Exclusive(CONF_UNTIL, "time"): cv.All(cv.only_on_esp32, cv.time_of_day),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
}
),
cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID),
)
DEEP_SLEEP_PREVENT_SCHEMA = automation.maybe_simple_id(
DEEP_SLEEP_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(DeepSleepComponent),
}
)
DEEP_SLEEP_ENTER_SCHEMA = cv.All(
automation.maybe_simple_id(
DEEP_SLEEP_ACTION_SCHEMA.extend(
cv.Schema(
{
cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable(
cv.positive_time_period_milliseconds
),
# Only on ESP32 due to how long the RTC on ESP8266 can stay asleep
cv.Exclusive(CONF_UNTIL, "time"): cv.All(
cv.only_on_esp32, cv.time_of_day
),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
}
)
)
),
cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID),
)
@automation.register_action(
"deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA
@@ -252,8 +263,16 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args):
@automation.register_action(
"deep_sleep.prevent", PreventDeepSleepAction, DEEP_SLEEP_PREVENT_SCHEMA
"deep_sleep.prevent",
PreventDeepSleepAction,
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
)
async def deep_sleep_prevent_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"deep_sleep.allow",
AllowDeepSleepAction,
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
)
async def deep_sleep_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var

View File

@@ -21,6 +21,7 @@ optional<uint32_t> DeepSleepComponent::get_run_duration_() const {
switch (wakeup_cause) {
case ESP_SLEEP_WAKEUP_EXT0:
case ESP_SLEEP_WAKEUP_EXT1:
case ESP_SLEEP_WAKEUP_GPIO:
return this->wakeup_cause_to_run_duration_->gpio_cause;
case ESP_SLEEP_WAKEUP_TOUCHPAD:
return this->wakeup_cause_to_run_duration_->touch_cause;
@@ -72,16 +73,27 @@ float DeepSleepComponent::get_loop_priority() const {
return -100.0f; // run after everything else is ready
}
void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; }
#ifdef USE_ESP32
#if defined(USE_ESP32)
void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) {
this->wakeup_pin_mode_ = wakeup_pin_mode;
}
#endif
#if defined(USE_ESP32)
#if !defined(USE_ESP32_VARIANT_ESP32C3)
void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; }
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
#endif
void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) {
wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration;
}
#endif
void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; }
void DeepSleepComponent::begin_sleep(bool manual) {
if (this->prevent_ && !manual) {
@@ -107,7 +119,8 @@ void DeepSleepComponent::begin_sleep(bool manual) {
App.run_safe_shutdown_hooks();
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
#if defined(USE_ESP32)
#if !defined(USE_ESP32_VARIANT_ESP32C3)
if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
if (this->wakeup_pin_ != nullptr) {
@@ -125,10 +138,7 @@ void DeepSleepComponent::begin_sleep(bool manual) {
esp_sleep_enable_touchpad_wakeup();
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
}
esp_deep_sleep_start();
#endif
#ifdef USE_ESP32_VARIANT_ESP32C3
if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
@@ -137,9 +147,12 @@ void DeepSleepComponent::begin_sleep(bool manual) {
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
level = !level;
}
esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level);
esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()),
static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level));
}
#endif
esp_deep_sleep_start();
#endif
#ifdef USE_ESP8266
ESP.deepSleep(*this->sleep_duration_); // NOLINT(readability-static-accessed-through-instance)
@@ -147,6 +160,7 @@ void DeepSleepComponent::begin_sleep(bool manual) {
}
float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; }
void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; }
void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; }
} // namespace deep_sleep
} // namespace esphome

View File

@@ -70,17 +70,19 @@ class DeepSleepComponent : public Component {
void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode);
#endif
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
#if defined(USE_ESP32)
#if !defined(USE_ESP32_VARIANT_ESP32C3)
void set_ext1_wakeup(Ext1Wakeup ext1_wakeup);
void set_touch_wakeup(bool touch_wakeup);
#endif
// Set the duration in ms for how long the code should run before entering
// deep sleep mode, according to the cause the ESP32 has woken.
void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration);
#endif
/// Set a duration in ms for how long the code should run before entering deep sleep mode.
void set_run_duration(uint32_t time_ms);
@@ -94,6 +96,7 @@ class DeepSleepComponent : public Component {
void begin_sleep(bool manual = false);
void prevent_deep_sleep();
void allow_deep_sleep();
protected:
// Returns nullopt if no run duration is set. Otherwise, returns the run
@@ -187,14 +190,14 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> {
#endif
};
template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...> {
template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
public:
PreventDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {}
void play(Ts... x) override { this->parent_->prevent_deep_sleep(); }
};
void play(Ts... x) override { this->deep_sleep_->prevent_deep_sleep(); }
protected:
DeepSleepComponent *deep_sleep_;
template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
public:
void play(Ts... x) override { this->parent_->allow_deep_sleep(); }
};
} // namespace deep_sleep

View File

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

View File

@@ -0,0 +1,20 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import climate_ir
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"]
delonghi_ns = cg.esphome_ns.namespace("delonghi")
DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(DelonghiClimate),
}
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)

View File

@@ -0,0 +1,186 @@
#include "delonghi.h"
#include "esphome/components/remote_base/remote_base.h"
namespace esphome {
namespace delonghi {
static const char *const TAG = "delonghi.climate";
void DelonghiClimate::transmit_state() {
uint8_t remote_state[DELONGHI_STATE_FRAME_SIZE] = {0};
remote_state[0] = DELONGHI_ADDRESS;
remote_state[1] = this->temperature_();
remote_state[1] |= (this->fan_speed_()) << 5;
remote_state[2] = this->operation_mode_();
// Calculate checksum
for (int i = 0; i < DELONGHI_STATE_FRAME_SIZE - 1; i++) {
remote_state[DELONGHI_STATE_FRAME_SIZE - 1] += remote_state[i];
}
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
data->set_carrier_frequency(DELONGHI_IR_FREQUENCY);
data->mark(DELONGHI_HEADER_MARK);
data->space(DELONGHI_HEADER_SPACE);
for (unsigned char b : remote_state) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DELONGHI_BIT_MARK);
bool bit = b & mask;
data->space(bit ? DELONGHI_ONE_SPACE : DELONGHI_ZERO_SPACE);
}
}
data->mark(DELONGHI_BIT_MARK);
data->space(0);
transmit.perform();
}
uint8_t DelonghiClimate::operation_mode_() {
uint8_t operating_mode = DELONGHI_MODE_ON;
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
operating_mode |= DELONGHI_MODE_COOL;
break;
case climate::CLIMATE_MODE_DRY:
operating_mode |= DELONGHI_MODE_DRY;
break;
case climate::CLIMATE_MODE_HEAT:
operating_mode |= DELONGHI_MODE_HEAT;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
operating_mode |= DELONGHI_MODE_AUTO;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
operating_mode |= DELONGHI_MODE_FAN;
break;
case climate::CLIMATE_MODE_OFF:
default:
operating_mode = DELONGHI_MODE_OFF;
break;
}
return operating_mode;
}
uint16_t DelonghiClimate::fan_speed_() {
uint16_t fan_speed;
switch (this->fan_mode.value()) {
case climate::CLIMATE_FAN_LOW:
fan_speed = DELONGHI_FAN_LOW;
break;
case climate::CLIMATE_FAN_MEDIUM:
fan_speed = DELONGHI_FAN_MEDIUM;
break;
case climate::CLIMATE_FAN_HIGH:
fan_speed = DELONGHI_FAN_HIGH;
break;
case climate::CLIMATE_FAN_AUTO:
default:
fan_speed = DELONGHI_FAN_AUTO;
}
return fan_speed;
}
uint8_t DelonghiClimate::temperature_() {
// Force special temperatures depending on the mode
uint8_t temperature = 0b0001;
switch (this->mode) {
case climate::CLIMATE_MODE_HEAT:
temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_HEAT;
break;
case climate::CLIMATE_MODE_COOL:
case climate::CLIMATE_MODE_DRY:
case climate::CLIMATE_MODE_HEAT_COOL:
case climate::CLIMATE_MODE_FAN_ONLY:
case climate::CLIMATE_MODE_OFF:
default:
temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_COOL;
}
if (temperature > 0x0F) {
temperature = 0x0F; // clamp maximum
}
return temperature;
}
bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) {
uint8_t checksum = 0;
for (int i = 0; i < (DELONGHI_STATE_FRAME_SIZE - 1); i++) {
checksum += frame[i];
}
if (frame[DELONGHI_STATE_FRAME_SIZE - 1] != checksum) {
return false;
}
uint8_t mode = frame[2] & 0x0F;
if (mode & DELONGHI_MODE_ON) {
switch (mode & 0x0E) {
case DELONGHI_MODE_COOL:
this->mode = climate::CLIMATE_MODE_COOL;
break;
case DELONGHI_MODE_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
break;
case DELONGHI_MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
case DELONGHI_MODE_AUTO:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
case DELONGHI_MODE_FAN:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
}
} else {
this->mode = climate::CLIMATE_MODE_OFF;
}
uint8_t temperature = frame[1] & 0x0F;
if (this->mode == climate::CLIMATE_MODE_HEAT) {
this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_HEAT;
} else {
this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_COOL;
}
uint8_t fan_mode = frame[1] >> 5;
switch (fan_mode) {
case DELONGHI_FAN_LOW:
this->fan_mode = climate::CLIMATE_FAN_LOW;
break;
case DELONGHI_FAN_MEDIUM:
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
break;
case DELONGHI_FAN_HIGH:
this->fan_mode = climate::CLIMATE_FAN_HIGH;
break;
case DELONGHI_FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
}
this->publish_state();
return true;
}
bool DelonghiClimate::on_receive(remote_base::RemoteReceiveData data) {
uint8_t state_frame[DELONGHI_STATE_FRAME_SIZE] = {};
if (!data.expect_item(DELONGHI_HEADER_MARK, DELONGHI_HEADER_SPACE)) {
return false;
}
for (uint8_t pos = 0; pos < DELONGHI_STATE_FRAME_SIZE; pos++) {
uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) {
if (data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ONE_SPACE)) {
byte |= 1 << bit;
} else if (!data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ZERO_SPACE)) {
return false;
}
}
state_frame[pos] = byte;
if (pos == 0) {
// frame header
if (byte != DELONGHI_ADDRESS) {
return false;
}
}
}
return this->parse_state_frame_(state_frame);
}
} // namespace delonghi
} // namespace esphome

View File

@@ -0,0 +1,64 @@
#pragma once
#include "esphome/components/climate_ir/climate_ir.h"
namespace esphome {
namespace delonghi {
// Values for DELONGHI ARC43XXX IR Controllers
const uint8_t DELONGHI_ADDRESS = 83;
// Temperature
const uint8_t DELONGHI_TEMP_MIN = 13; // Celsius
const uint8_t DELONGHI_TEMP_MAX = 32; // Celsius
const uint8_t DELONGHI_TEMP_OFFSET_COOL = 17; // Celsius
const uint8_t DELONGHI_TEMP_OFFSET_HEAT = 12; // Celsius
// Modes
const uint8_t DELONGHI_MODE_AUTO = 0b1000;
const uint8_t DELONGHI_MODE_COOL = 0b0000;
const uint8_t DELONGHI_MODE_HEAT = 0b0110;
const uint8_t DELONGHI_MODE_DRY = 0b0010;
const uint8_t DELONGHI_MODE_FAN = 0b0100;
const uint8_t DELONGHI_MODE_OFF = 0b0000;
const uint8_t DELONGHI_MODE_ON = 0b0001;
// Fan Speed
const uint8_t DELONGHI_FAN_AUTO = 0b00;
const uint8_t DELONGHI_FAN_HIGH = 0b01;
const uint8_t DELONGHI_FAN_MEDIUM = 0b10;
const uint8_t DELONGHI_FAN_LOW = 0b11;
// IR Transmission - similar to NEC1
const uint32_t DELONGHI_IR_FREQUENCY = 38000;
const uint32_t DELONGHI_HEADER_MARK = 9000;
const uint32_t DELONGHI_HEADER_SPACE = 4500;
const uint32_t DELONGHI_BIT_MARK = 465;
const uint32_t DELONGHI_ONE_SPACE = 1750;
const uint32_t DELONGHI_ZERO_SPACE = 670;
// State Frame size
const uint8_t DELONGHI_STATE_FRAME_SIZE = 8;
class DelonghiClimate : public climate_ir::ClimateIR {
public:
DelonghiClimate()
: climate_ir::ClimateIR(DELONGHI_TEMP_MIN, DELONGHI_TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
protected:
// Transmit via IR the state of this climate controller.
void transmit_state() override;
uint8_t operation_mode_();
uint16_t fan_speed_();
uint8_t temperature_();
// Handle received IR Buffer
bool on_receive(remote_base::RemoteReceiveData data) override;
bool parse_state_frame_(const uint8_t frame[]);
};
} // namespace delonghi
} // namespace esphome

View File

@@ -242,6 +242,13 @@ void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color colo
}
}
break;
case IMAGE_TYPE_RGB565:
for (int img_x = 0; img_x < image->get_width(); img_x++) {
for (int img_y = 0; img_y < image->get_height(); img_y++) {
this->draw_pixel_at(x + img_x, y + img_y, image->get_rgb565_pixel(img_x, img_y));
}
}
break;
}
}
@@ -497,6 +504,17 @@ Color Image::get_color_pixel(int x, int y) const {
(progmem_read_byte(this->data_start_ + pos + 0) << 16);
return Color(color32);
}
Color Image::get_rgb565_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK;
const uint32_t pos = (x + y * this->width_) * 2;
uint16_t rgb565 =
progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F;
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
}
Color Image::get_grayscale_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK;
@@ -532,6 +550,20 @@ Color Animation::get_color_pixel(int x, int y) const {
(progmem_read_byte(this->data_start_ + pos + 0) << 16);
return Color(color32);
}
Color Animation::get_rgb565_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK;
const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_;
if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_))
return Color::BLACK;
const uint32_t pos = (x + y * this->width_ + frame_index) * 2;
uint16_t rgb565 =
progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F;
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
}
Color Animation::get_grayscale_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK;
@@ -552,6 +584,12 @@ void Animation::next_frame() {
this->current_frame_ = 0;
}
}
void Animation::prev_frame() {
this->current_frame_--;
if (this->current_frame_ < 0) {
this->current_frame_ = this->animation_frame_count_ - 1;
}
}
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
void DisplayPage::show() { this->parent_->show_page(this); }

View File

@@ -82,6 +82,13 @@ enum ImageType {
IMAGE_TYPE_GRAYSCALE = 1,
IMAGE_TYPE_RGB24 = 2,
IMAGE_TYPE_TRANSPARENT_BINARY = 3,
IMAGE_TYPE_RGB565 = 4,
};
enum DisplayType {
DISPLAY_TYPE_BINARY = 1,
DISPLAY_TYPE_GRAYSCALE = 2,
DISPLAY_TYPE_COLOR = 3,
};
enum DisplayRotation {
@@ -360,6 +367,11 @@ class DisplayBuffer {
virtual int get_width_internal() = 0;
DisplayRotation get_rotation() const { return this->rotation_; }
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
* returns the type the display is currently configured to.
*/
virtual DisplayType get_display_type() = 0;
protected:
void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg);
@@ -453,6 +465,7 @@ class Image {
Image(const uint8_t *data_start, int width, int height, ImageType type);
virtual bool get_pixel(int x, int y) const;
virtual Color get_color_pixel(int x, int y) const;
virtual Color get_rgb565_pixel(int x, int y) const;
virtual Color get_grayscale_pixel(int x, int y) const;
int get_width() const;
int get_height() const;
@@ -470,11 +483,13 @@ class Animation : public Image {
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type);
bool get_pixel(int x, int y) const override;
Color get_color_pixel(int x, int y) const override;
Color get_rgb565_pixel(int x, int y) const override;
Color get_grayscale_pixel(int x, int y) const override;
int get_animation_frame_count() const;
int get_current_frame() const;
void next_frame();
void prev_frame();
protected:
int current_frame_;

View File

@@ -66,6 +66,9 @@ class ColorUtil {
}
return color_return;
}
static inline Color rgb332_to_color(uint8_t rgb332_color) {
return to_color((uint32_t) rgb332_color, COLOR_ORDER_RGB, COLOR_BITNESS_332);
}
static uint8_t color_to_332(Color color, ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) {
uint16_t red_color, green_color, blue_color;
@@ -100,11 +103,57 @@ class ColorUtil {
}
return 0;
}
static uint32_t color_to_grayscale4(Color color) {
uint32_t gs4 = esp_scale8(color.white, 15);
return gs4;
}
/***
* Converts a Color value to an 8bit index using a 24bit 888 palette.
* Uses euclidiean distance to calculate the linear distance between
* two points in an RGB cube, then iterates through the full palette
* returning the closest match.
* @param[in] color The target color.
* @param[in] palette The 256*3 byte RGB palette.
* @return The 8 bit index of the closest color (e.g. for display buffer).
*/
// static uint8_t color_to_index8_palette888(Color color, uint8_t *palette) {
static uint8_t color_to_index8_palette888(Color color, const uint8_t *palette) {
uint8_t closest_index = 0;
uint32_t minimum_dist2 = UINT32_MAX; // Smallest distance^2 to the target
// so far
// int8_t(*plt)[][3] = palette;
int16_t tgt_r = color.r;
int16_t tgt_g = color.g;
int16_t tgt_b = color.b;
uint16_t x, y, z;
// Loop through each row of the palette
for (uint16_t i = 0; i < 256; i++) {
// Get the pallet rgb color
int16_t plt_r = (int16_t) palette[i * 3 + 0];
int16_t plt_g = (int16_t) palette[i * 3 + 1];
int16_t plt_b = (int16_t) palette[i * 3 + 2];
// Calculate euclidian distance (linear distance in rgb cube).
x = (uint32_t) std::abs(tgt_r - plt_r);
y = (uint32_t) std::abs(tgt_g - plt_g);
z = (uint32_t) std::abs(tgt_b - plt_b);
uint32_t dist2 = x * x + y * y + z * z;
if (dist2 < minimum_dist2) {
minimum_dist2 = dist2;
closest_index = (uint8_t) i;
}
}
return closest_index;
}
/***
* Converts an 8bit palette index (e.g. from a display buffer) to a color.
* @param[in] index The index to look up.
* @param[in] palette The 256*3 byte RGB palette.
* @return The RGBW Color object looked up by the palette.
*/
static Color index8_to_color_palette888(uint8_t index, const uint8_t *palette) {
Color color = Color(palette[index * 3 + 0], palette[index * 3 + 1], palette[index * 3 + 2], 0);
return color;
}
};
} // namespace display
} // namespace esphome

View File

View File

@@ -0,0 +1,230 @@
// ENS210 relative humidity and temperature sensor with I2C interface from ScioSense
//
// Datasheet: https://www.sciosense.com/wp-content/uploads/2021/01/ENS210.pdf
//
// Implementation based on:
// https://github.com/maarten-pennings/ENS210
// https://github.com/sciosense/ENS210_driver
#include "ens210.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace ens210 {
static const char *const TAG = "ens210";
// ENS210 chip constants
static const uint8_t ENS210_BOOTING_MS = 2; // Booting time in ms (also after reset, or going to high power)
static const uint8_t ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS =
130; // Conversion time in ms for single shot T/H measurement
static const uint16_t ENS210_PART_ID = 0x0210; // The expected part id of the ENS210
// Addresses of the ENS210 registers
static const uint8_t ENS210_REGISTER_PART_ID = 0x00;
static const uint8_t ENS210_REGISTER_UID = 0x04;
static const uint8_t ENS210_REGISTER_SYS_CTRL = 0x10;
static const uint8_t ENS210_REGISTER_SYS_STAT = 0x11;
static const uint8_t ENS210_REGISTER_SENS_RUN = 0x21;
static const uint8_t ENS210_REGISTER_SENS_START = 0x22;
static const uint8_t ENS210_REGISTER_SENS_STOP = 0x23;
static const uint8_t ENS210_REGISTER_SENS_STAT = 0x24;
static const uint8_t ENS210_REGISTER_T_VAL = 0x30;
static const uint8_t ENS210_REGISTER_H_VAL = 0x33;
// CRC-7 constants
static const uint8_t CRC7_WIDTH = 7; // A 7 bits CRC has polynomial of 7th order, which has 8 terms
static const uint8_t CRC7_POLY = 0x89; // The 8 coefficients of the polynomial
static const uint8_t CRC7_IVEC = 0x7F; // Initial vector has all 7 bits high
// Payload data constants
static const uint8_t DATA7_WIDTH = 17;
static const uint32_t DATA7_MASK = ((1UL << DATA7_WIDTH) - 1); // 0b 0 1111 1111 1111 1111
static const uint32_t DATA7_MSB = (1UL << (DATA7_WIDTH - 1)); // 0b 1 0000 0000 0000 0000
// Converts a status to a human readable string
static const LogString *ens210_status_to_human(int status) {
switch (status) {
case ENS210Component::ENS210_STATUS_I2C_ERROR:
return LOG_STR("I2C error - communication with ENS210 failed!");
case ENS210Component::ENS210_STATUS_CRC_ERROR:
return LOG_STR("CRC error");
case ENS210Component::ENS210_STATUS_INVALID:
return LOG_STR("Invalid data");
case ENS210Component::ENS210_STATUS_OK:
return LOG_STR("Status OK");
case ENS210Component::ENS210_WRONG_CHIP_ID:
return LOG_STR("ENS210 has wrong chip ID! Is it a ENS210?");
default:
return LOG_STR("Unknown");
}
}
// Compute the CRC-7 of 'value' (should only have 17 bits)
// https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Computation
static uint32_t crc7(uint32_t value) {
// Setup polynomial
uint32_t polynomial = CRC7_POLY;
// Align polynomial with data
polynomial = polynomial << (DATA7_WIDTH - CRC7_WIDTH - 1);
// Loop variable (indicates which bit to test, start with highest)
uint32_t bit = DATA7_MSB;
// Make room for CRC value
value = value << CRC7_WIDTH;
bit = bit << CRC7_WIDTH;
polynomial = polynomial << CRC7_WIDTH;
// Insert initial vector
value |= CRC7_IVEC;
// Apply division until all bits done
while (bit & (DATA7_MASK << CRC7_WIDTH)) {
if (bit & value)
value ^= polynomial;
bit >>= 1;
polynomial >>= 1;
}
return value;
}
void ENS210Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up ENS210...");
uint8_t data[2];
uint16_t part_id = 0;
// Reset
if (!this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80)) {
this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80);
this->error_code_ = ENS210_STATUS_I2C_ERROR;
this->mark_failed();
return;
}
// Wait to boot after reset
delay(ENS210_BOOTING_MS);
// Must disable low power to read PART_ID
if (!set_low_power_(false)) {
// Try to go back to default mode (low power enabled)
set_low_power_(true);
this->error_code_ = ENS210_STATUS_I2C_ERROR;
this->mark_failed();
return;
}
// Read the PART_ID
if (!this->read_bytes(ENS210_REGISTER_PART_ID, data, 2)) {
// Try to go back to default mode (low power enabled)
set_low_power_(true);
this->error_code_ = ENS210_STATUS_I2C_ERROR;
this->mark_failed();
return;
}
// Pack bytes into partid
part_id = data[1] * 256U + data[0] * 1U;
// Check expected part id of the ENS210
if (part_id != ENS210_PART_ID) {
this->error_code_ = ENS210_WRONG_CHIP_ID;
this->mark_failed();
}
// Set default power mode (low power enabled)
set_low_power_(true);
}
void ENS210Component::dump_config() {
ESP_LOGCONFIG(TAG, "ENS210:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "%s", LOG_STR_ARG(ens210_status_to_human(this->error_code_)));
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
}
float ENS210Component::get_setup_priority() const { return setup_priority::DATA; }
void ENS210Component::update() {
// Execute a single measurement
if (!this->write_byte(ENS210_REGISTER_SENS_RUN, 0x00)) {
ESP_LOGE(TAG, "Starting single measurement failed!");
this->status_set_warning();
return;
}
// Trigger measurement
if (!this->write_byte(ENS210_REGISTER_SENS_START, 0x03)) {
ESP_LOGE(TAG, "Trigger of measurement failed!");
this->status_set_warning();
return;
}
// Wait for measurement to complete
this->set_timeout("data", uint32_t(ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS), [this]() {
int temperature_data, temperature_status, humidity_data, humidity_status;
uint8_t data[6];
uint32_t h_val_data, t_val_data;
// Set default status for early bail out
temperature_status = ENS210_STATUS_I2C_ERROR;
humidity_status = ENS210_STATUS_I2C_ERROR;
// Read T_VAL and H_VAL
if (!this->read_bytes(ENS210_REGISTER_T_VAL, data, 6)) {
ESP_LOGE(TAG, "Communication with ENS210 failed!");
this->status_set_warning();
return;
}
// Pack bytes for humidity
h_val_data = (uint32_t)((uint32_t) data[5] << 16 | (uint32_t) data[4] << 8 | (uint32_t) data[3]);
// Extract humidity data and update the status
extract_measurement_(h_val_data, &humidity_data, &humidity_status);
if (humidity_status == ENS210_STATUS_OK) {
if (this->humidity_sensor_ != nullptr) {
float humidity = (humidity_data & 0xFFFF) / 512.0;
this->humidity_sensor_->publish_state(humidity);
}
} else {
ESP_LOGW(TAG, "Humidity status failure: %s", LOG_STR_ARG(ens210_status_to_human(humidity_status)));
this->status_set_warning();
return;
}
// Pack bytes for temperature
t_val_data = (uint32_t)((uint32_t) data[2] << 16 | (uint32_t) data[1] << 8 | (uint32_t) data[0]);
// Extract temperature data and update the status
extract_measurement_(t_val_data, &temperature_data, &temperature_status);
if (temperature_status == ENS210_STATUS_OK) {
if (this->temperature_sensor_ != nullptr) {
// Temperature in Celsius
float temperature = (temperature_data & 0xFFFF) / 64.0 - 27315L / 100.0;
this->temperature_sensor_->publish_state(temperature);
}
} else {
ESP_LOGW(TAG, "Temperature status failure: %s", LOG_STR_ARG(ens210_status_to_human(temperature_status)));
}
});
}
// Extracts measurement 'data' and 'status' from a 'val' obtained from measurment.
void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status) {
*data = (val >> 0) & 0xffff;
int valid = (val >> 16) & 0x1;
uint32_t crc = (val >> 17) & 0x7f;
uint32_t payload = (val >> 0) & 0x1ffff;
// Check CRC
uint8_t crc_ok = crc7(payload) == crc;
if (!crc_ok) {
*status = ENS210_STATUS_CRC_ERROR;
} else if (!valid) {
*status = ENS210_STATUS_INVALID;
} else {
*status = ENS210_STATUS_OK;
}
}
// Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems.
bool ENS210Component::set_low_power_(bool enable) {
uint8_t low_power_cmd = enable ? 0x01 : 0x00;
ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false");
bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd);
delay(ENS210_BOOTING_MS);
return result;
}
} // namespace ens210
} // namespace esphome

View File

@@ -0,0 +1,39 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace ens210 {
/// This class implements support for the ENS210 relative humidity and temperature i2c sensor.
class ENS210Component : public PollingComponent, public i2c::I2CDevice {
public:
float get_setup_priority() const override;
void dump_config() override;
void setup() override;
void update() override;
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
enum ErrorCode {
ENS210_STATUS_OK = 0, // The value was read, the CRC matches, and data is valid
ENS210_STATUS_INVALID, // The value was read, the CRC matches, but the data is invalid (e.g. the measurement was
// not yet finished)
ENS210_STATUS_CRC_ERROR, // The value was read, but the CRC over the payload (valid and data) does not match
ENS210_STATUS_I2C_ERROR, // There was an I2C communication error
ENS210_WRONG_CHIP_ID // The read PART_ID is not the expected part id of the ENS210
} error_code_{ENS210_STATUS_OK};
protected:
bool set_low_power_(bool enable);
void extract_measurement_(uint32_t val, int *data, int *status);
sensor::Sensor *temperature_sensor_;
sensor::Sensor *humidity_sensor_;
};
} // namespace ens210
} // namespace esphome

View File

@@ -0,0 +1,58 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_HUMIDITY,
CONF_ID,
CONF_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PERCENT,
)
CODEOWNERS = ["@itn3rd77"]
DEPENDENCIES = ["i2c"]
ens210_ns = cg.esphome_ns.namespace("ens210")
ENS210Component = ens210_ns.class_(
"ENS210Component", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ENS210Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x43))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature_sensor(sens))
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity_sensor(sens))

View File

@@ -107,7 +107,7 @@ def validate_gpio_pin(value):
value = _translate_pin(value)
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
if variant not in _esp32_validations:
raise cv.Invalid("Unsupported ESP32 variant {variant}")
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
return _esp32_validations[variant].pin_validation(value)
@@ -121,7 +121,7 @@ def validate_supports(value):
is_pulldown = mode[CONF_PULLDOWN]
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
if variant not in _esp32_validations:
raise cv.Invalid("Unsupported ESP32 variant {variant}")
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
if is_open_drain and not is_output:
raise cv.Invalid(

View File

@@ -118,12 +118,17 @@ class ESP32Preferences : public ESPPreferences {
// go through vector from back to front (makes erase easier/more efficient)
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
const auto &save = s_pending_save[i];
esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size());
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(),
esp_err_to_name(err));
any_failed = true;
continue;
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str());
if (is_changed(nvs_handle, save)) {
esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size());
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(),
esp_err_to_name(err));
any_failed = true;
continue;
}
} else {
ESP_LOGD(TAG, "NVS data not changed skipping %s len=%u", save.key.c_str(), save.data.size());
}
s_pending_save.erase(s_pending_save.begin() + i);
}
@@ -137,6 +142,22 @@ class ESP32Preferences : public ESPPreferences {
return !any_failed;
}
bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) {
NVSData stored_data{};
size_t actual_len;
esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err));
return true;
}
stored_data.data.reserve(actual_len);
err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err));
return true;
}
return to_save.data != stored_data.data;
}
};
void setup_preferences() {

View File

@@ -1,5 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome import pins
from esphome.const import (
CONF_FREQUENCY,
@@ -12,6 +13,7 @@ from esphome.const import (
CONF_RESOLUTION,
CONF_BRIGHTNESS,
CONF_CONTRAST,
CONF_TRIGGER_ID,
)
from esphome.core import CORE
from esphome.components.esp32 import add_idf_sdkconfig_option
@@ -23,7 +25,14 @@ AUTO_LOAD = ["psram"]
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
ESP32CameraStreamStartTrigger = esp32_camera_ns.class_(
"ESP32CameraStreamStartTrigger",
automation.Trigger.template(),
)
ESP32CameraStreamStopTrigger = esp32_camera_ns.class_(
"ESP32CameraStreamStopTrigger",
automation.Trigger.template(),
)
ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize")
FRAME_SIZES = {
"160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120,
@@ -111,6 +120,10 @@ CONF_TEST_PATTERN = "test_pattern"
CONF_MAX_FRAMERATE = "max_framerate"
CONF_IDLE_FRAMERATE = "idle_framerate"
# stream trigger
CONF_ON_STREAM_START = "on_stream_start"
CONF_ON_STREAM_STOP = "on_stream_stop"
camera_range_param = cv.int_range(min=-2, max=2)
CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
@@ -178,6 +191,20 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All(
cv.framerate, cv.Range(min=0, max=1)
),
cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32CameraStreamStartTrigger
),
}
),
cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32CameraStreamStopTrigger
),
}
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -238,3 +265,11 @@ async def to_code(config):
if CORE.using_esp_idf:
cg.add_library("espressif/esp32-camera", "1.0.0")
add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True)
for conf in config.get(CONF_ON_STREAM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_STREAM_STOP, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)

View File

@@ -282,8 +282,20 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) {
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f) {
this->new_image_callback_.add(std::move(f));
}
void ESP32Camera::start_stream(CameraRequester requester) { this->stream_requesters_ |= (1U << requester); }
void ESP32Camera::stop_stream(CameraRequester requester) { this->stream_requesters_ &= ~(1U << requester); }
void ESP32Camera::add_stream_start_callback(std::function<void()> &&callback) {
this->stream_start_callback_.add(std::move(callback));
}
void ESP32Camera::add_stream_stop_callback(std::function<void()> &&callback) {
this->stream_stop_callback_.add(std::move(callback));
}
void ESP32Camera::start_stream(CameraRequester requester) {
this->stream_start_callback_.call();
this->stream_requesters_ |= (1U << requester);
}
void ESP32Camera::stop_stream(CameraRequester requester) {
this->stream_stop_callback_.call();
this->stream_requesters_ &= ~(1U << requester);
}
void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= (1U << requester); }
void ESP32Camera::update_camera_parameters() {
sensor_t *s = esp_camera_sensor_get();
@@ -310,7 +322,6 @@ void ESP32Camera::update_camera_parameters() {
}
/* ---------------- Internal methods ---------------- */
uint32_t ESP32Camera::hash_base() { return 3010542557UL; }
bool ESP32Camera::has_requested_image_() const { return this->single_requesters_ || this->stream_requesters_; }
bool ESP32Camera::can_return_image_() const { return this->current_image_.use_count() == 1; }
void ESP32Camera::framebuffer_task(void *pv) {

View File

@@ -2,6 +2,7 @@
#ifdef USE_ESP32
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
@@ -145,9 +146,11 @@ class ESP32Camera : public Component, public EntityBase {
void request_image(CameraRequester requester);
void update_camera_parameters();
void add_stream_start_callback(std::function<void()> &&callback);
void add_stream_stop_callback(std::function<void()> &&callback);
protected:
/* internal methods */
uint32_t hash_base() override;
bool has_requested_image_() const;
bool can_return_image_() const;
@@ -187,6 +190,8 @@ class ESP32Camera : public Component, public EntityBase {
QueueHandle_t framebuffer_get_queue_;
QueueHandle_t framebuffer_return_queue_;
CallbackManager<void(std::shared_ptr<CameraImage>)> new_image_callback_;
CallbackManager<void()> stream_start_callback_{};
CallbackManager<void()> stream_stop_callback_{};
uint32_t last_idle_request_{0};
uint32_t last_update_{0};
@@ -195,6 +200,23 @@ class ESP32Camera : public Component, public EntityBase {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern ESP32Camera *global_esp32_camera;
class ESP32CameraStreamStartTrigger : public Trigger<> {
public:
explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) {
parent->add_stream_start_callback([this]() { this->trigger(); });
}
protected:
};
class ESP32CameraStreamStopTrigger : public Trigger<> {
public:
explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) {
parent->add_stream_stop_callback([this]() { this->trigger(); });
}
protected:
};
} // namespace esp32_camera
} // namespace esphome

View File

@@ -19,6 +19,7 @@ from esphome.helpers import copy_file_if_changed
from .const import (
CONF_RESTORE_FROM_FLASH,
CONF_EARLY_PIN_INIT,
KEY_BOARD,
KEY_ESP8266,
KEY_PIN_INITIAL_STATES,
@@ -148,6 +149,7 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_BOARD): cv.string_strict,
cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA,
cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean,
cv.Optional(CONF_EARLY_PIN_INIT, default=True): cv.boolean,
cv.Optional(CONF_BOARD_FLASH_MODE, default="dout"): cv.one_of(
*BUILD_FLASH_MODES, lower=True
),
@@ -197,6 +199,9 @@ async def to_code(config):
if config[CONF_RESTORE_FROM_FLASH]:
cg.add_define("USE_ESP8266_PREFERENCES_FLASH")
if config[CONF_EARLY_PIN_INIT]:
cg.add_define("USE_ESP8266_EARLY_PIN_INIT")
# Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when
# out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make
# new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of

View File

@@ -4,6 +4,7 @@ KEY_ESP8266 = "esp8266"
KEY_BOARD = "board"
KEY_PIN_INITIAL_STATES = "pin_initial_states"
CONF_RESTORE_FROM_FLASH = "restore_from_flash"
CONF_EARLY_PIN_INIT = "early_pin_init"
# esp8266 namespace is already defined by arduino, manually prefix esphome
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")

View File

@@ -1,6 +1,7 @@
#ifdef USE_ESP8266
#include "core.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
@@ -55,6 +56,7 @@ extern "C" void resetPins() { // NOLINT
// ourselves and this causes pins to toggle during reboot.
force_link_symbols();
#ifdef USE_ESP8266_EARLY_PIN_INIT
for (int i = 0; i < 16; i++) {
uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i];
uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i];
@@ -63,6 +65,7 @@ extern "C" void resetPins() { // NOLINT
if (level != 255)
digitalWrite(i, level); // NOLINT
}
#endif
}
} // namespace esphome

View File

@@ -1,5 +1,4 @@
#include "fan.h"
#include "fan_helpers.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -61,22 +60,6 @@ void FanCall::validate_() {
}
}
// This whole method is deprecated, don't warn about usage of deprecated methods inside of it.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
FanCall &FanCall::set_speed(const char *legacy_speed) {
const auto supported_speed_count = this->parent_.get_traits().supported_speed_count();
if (strcasecmp(legacy_speed, "low") == 0) {
this->set_speed(fan::speed_enum_to_level(FAN_SPEED_LOW, supported_speed_count));
} else if (strcasecmp(legacy_speed, "medium") == 0) {
this->set_speed(fan::speed_enum_to_level(FAN_SPEED_MEDIUM, supported_speed_count));
} else if (strcasecmp(legacy_speed, "high") == 0) {
this->set_speed(fan::speed_enum_to_level(FAN_SPEED_HIGH, supported_speed_count));
}
return *this;
}
#pragma GCC diagnostic pop
FanCall FanRestoreState::to_call(Fan &fan) {
auto call = fan.make_call();
call.set_state(this->state);
@@ -169,7 +152,6 @@ void Fan::dump_traits_(const char *tag, const char *prefix) {
if (this->get_traits().supports_direction())
ESP_LOGCONFIG(tag, "%s Direction: YES", prefix);
}
uint32_t Fan::hash_base() { return 418001110UL; }
} // namespace fan
} // namespace esphome

View File

@@ -16,13 +16,6 @@ namespace fan {
(obj)->dump_traits_(TAG, prefix); \
}
/// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon
enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed {
FAN_SPEED_LOW = 0, ///< The fan is running on low speed.
FAN_SPEED_MEDIUM = 1, ///< The fan is running on medium speed.
FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed.
};
/// Simple enum to represent the direction of a fan.
enum class FanDirection { FORWARD = 0, REVERSE = 1 };
@@ -143,7 +136,6 @@ class Fan : public EntityBase {
void save_state_();
void dump_traits_(const char *tag, const char *prefix);
uint32_t hash_base() override;
CallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;

View File

@@ -1,23 +0,0 @@
#include <cassert>
#include "fan_helpers.h"
namespace esphome {
namespace fan {
// This whole file is deprecated, don't warn about usage of deprecated types in here.
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) {
const auto speed_ratio = static_cast<float>(speed_level) / (supported_speed_levels + 1);
const auto legacy_level = clamp<int>(static_cast<int>(ceilf(speed_ratio * 3)), 1, 3);
return static_cast<FanSpeed>(legacy_level - 1);
}
int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) {
const auto enum_level = static_cast<int>(speed) + 1;
const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels);
return static_cast<int>(speed_level);
}
} // namespace fan
} // namespace esphome

View File

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

View File

@@ -1,5 +1,4 @@
#include "hbridge_fan.h"
#include "esphome/components/fan/fan_helpers.h"
#include "esphome/core/log.h"
namespace esphome {

View File

@@ -195,7 +195,7 @@ void HydreonRGxxComponent::process_line_() {
if (n == std::string::npos) {
continue;
}
int data = strtol(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr, 10);
float data = strtof(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr);
this->sensors_[i]->publish_state(data);
ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state());
this->sensors_received_ |= (1 << i);

View File

@@ -37,7 +37,7 @@ SUPPORTED_SENSORS = {
PROTOCOL_NAMES = {
CONF_MOISTURE: "R",
CONF_ACC: "Acc",
CONF_R_INT: "Rint",
CONF_R_INT: "RInt",
CONF_EVENT_ACC: "EventAcc",
CONF_TOTAL_ACC: "TotalAcc",
}

View File

View File

@@ -0,0 +1,156 @@
#include "i2s_audio_media_player.h"
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include "esphome/core/log.h"
namespace esphome {
namespace i2s_audio {
static const char *const TAG = "audio";
void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) {
if (call.get_media_url().has_value()) {
if (this->audio_->isRunning())
this->audio_->stopSong();
this->high_freq_.start();
this->audio_->connecttohost(call.get_media_url().value().c_str());
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
}
if (call.get_volume().has_value()) {
this->volume = call.get_volume().value();
this->set_volume_(volume);
this->unmute_();
}
if (call.get_command().has_value()) {
switch (call.get_command().value()) {
case media_player::MEDIA_PLAYER_COMMAND_PLAY:
if (!this->audio_->isRunning())
this->audio_->pauseResume();
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
break;
case media_player::MEDIA_PLAYER_COMMAND_PAUSE:
if (this->audio_->isRunning())
this->audio_->pauseResume();
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
break;
case media_player::MEDIA_PLAYER_COMMAND_STOP:
this->stop_();
break;
case media_player::MEDIA_PLAYER_COMMAND_MUTE:
this->mute_();
break;
case media_player::MEDIA_PLAYER_COMMAND_UNMUTE:
this->unmute_();
break;
case media_player::MEDIA_PLAYER_COMMAND_TOGGLE:
this->audio_->pauseResume();
if (this->audio_->isRunning()) {
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
} else {
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
}
break;
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: {
float new_volume = this->volume + 0.1f;
if (new_volume > 1.0f)
new_volume = 1.0f;
this->set_volume_(new_volume);
this->unmute_();
break;
}
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: {
float new_volume = this->volume - 0.1f;
if (new_volume < 0.0f)
new_volume = 0.0f;
this->set_volume_(new_volume);
this->unmute_();
break;
}
}
}
this->publish_state();
}
void I2SAudioMediaPlayer::mute_() {
if (this->mute_pin_ != nullptr) {
this->mute_pin_->digital_write(true);
} else {
this->set_volume_(0.0f, false);
}
this->muted_ = true;
}
void I2SAudioMediaPlayer::unmute_() {
if (this->mute_pin_ != nullptr) {
this->mute_pin_->digital_write(false);
} else {
this->set_volume_(this->volume, false);
}
this->muted_ = false;
}
void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) {
this->audio_->setVolume(remap<uint8_t, float>(volume, 0.0f, 1.0f, 0, 21));
if (publish)
this->volume = volume;
}
void I2SAudioMediaPlayer::stop_() {
if (this->audio_->isRunning())
this->audio_->stopSong();
this->high_freq_.stop();
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
}
void I2SAudioMediaPlayer::setup() {
ESP_LOGCONFIG(TAG, "Setting up Audio...");
if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
this->audio_ = make_unique<Audio>(true, this->internal_dac_mode_);
} else {
this->audio_ = make_unique<Audio>(false);
this->audio_->setPinout(this->bclk_pin_, this->lrclk_pin_, this->dout_pin_);
this->audio_->forceMono(this->external_dac_channels_ == 1);
}
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
}
void I2SAudioMediaPlayer::loop() {
this->audio_->loop();
if (this->state == media_player::MEDIA_PLAYER_STATE_PLAYING && !this->audio_->isRunning()) {
this->stop_();
this->publish_state();
}
}
media_player::MediaPlayerTraits I2SAudioMediaPlayer::get_traits() {
auto traits = media_player::MediaPlayerTraits();
traits.set_supports_pause(true);
return traits;
};
void I2SAudioMediaPlayer::dump_config() {
ESP_LOGCONFIG(TAG, "Audio:");
if (this->is_failed()) {
ESP_LOGCONFIG(TAG, "Audio failed to initialize!");
return;
}
if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
switch (this->internal_dac_mode_) {
case I2S_DAC_CHANNEL_LEFT_EN:
ESP_LOGCONFIG(TAG, " Internal DAC mode: Left");
break;
case I2S_DAC_CHANNEL_RIGHT_EN:
ESP_LOGCONFIG(TAG, " Internal DAC mode: Right");
break;
case I2S_DAC_CHANNEL_BOTH_EN:
ESP_LOGCONFIG(TAG, " Internal DAC mode: Left & Right");
break;
default:
break;
}
}
}
} // namespace i2s_audio
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO

View File

@@ -0,0 +1,63 @@
#pragma once
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include "esphome/components/media_player/media_player.h"
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
#include "esphome/core/helpers.h"
#include <Audio.h>
namespace esphome {
namespace i2s_audio {
class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer {
public:
void setup() override;
float get_setup_priority() const override { return esphome::setup_priority::LATE; }
void loop() override;
void dump_config() override;
void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
void set_bclk_pin(uint8_t pin) { this->bclk_pin_ = pin; }
void set_lrclk_pin(uint8_t pin) { this->lrclk_pin_ = pin; }
void set_mute_pin(GPIOPin *mute_pin) { this->mute_pin_ = mute_pin; }
void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; }
media_player::MediaPlayerTraits get_traits() override;
bool is_muted() const override { return this->muted_; }
protected:
void control(const media_player::MediaPlayerCall &call) override;
void mute_();
void unmute_();
void set_volume_(float volume, bool publish = true);
void stop_();
std::unique_ptr<Audio> audio_;
uint8_t dout_pin_{0};
uint8_t din_pin_{0};
uint8_t bclk_pin_;
uint8_t lrclk_pin_;
GPIOPin *mute_pin_{nullptr};
bool muted_{false};
float unmuted_volume_{0};
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
uint8_t external_dac_channels_;
HighFrequencyLoopRequester high_freq_;
};
} // namespace i2s_audio
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO

View File

@@ -0,0 +1,94 @@
import esphome.codegen as cg
from esphome.components import media_player
import esphome.config_validation as cv
from esphome import pins
from esphome.const import CONF_ID, CONF_MODE
from esphome.core import CORE
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["esp32"]
i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio")
I2SAudioMediaPlayer = i2s_audio_ns.class_(
"I2SAudioMediaPlayer", cg.Component, media_player.MediaPlayer
)
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
CONF_I2S_DOUT_PIN = "i2s_dout_pin"
CONF_I2S_BCLK_PIN = "i2s_bclk_pin"
CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin"
CONF_MUTE_PIN = "mute_pin"
CONF_AUDIO_ID = "audio_id"
CONF_DAC_TYPE = "dac_type"
INTERNAL_DAC_OPTIONS = {
"left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
"right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
"stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
}
EXTERNAL_DAC_OPTIONS = ["mono", "stereo"]
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
"internal": cv.Schema(
{
cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer),
cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True),
}
)
.extend(media_player.MEDIA_PLAYER_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
"external": cv.Schema(
{
cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer),
cv.Required(
CONF_I2S_DOUT_PIN
): pins.internal_gpio_output_pin_number,
cv.Required(
CONF_I2S_BCLK_PIN
): pins.internal_gpio_output_pin_number,
cv.Required(
CONF_I2S_LRCLK_PIN
): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_MUTE_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_MODE, default="mono"): cv.one_of(
*EXTERNAL_DAC_OPTIONS, lower=True
),
}
)
.extend(media_player.MEDIA_PLAYER_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
},
key=CONF_DAC_TYPE,
),
cv.only_with_arduino,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await media_player.register_media_player(var, config)
if config[CONF_DAC_TYPE] == "internal":
cg.add(var.set_internal_dac_mode(config[CONF_MODE]))
else:
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
if CONF_MUTE_PIN in config:
pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN])
cg.add(var.set_mute_pin(pin))
cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))
if CORE.is_esp32:
cg.add_library("WiFiClientSecure", None)
cg.add_library("HTTPClient", None)
cg.add_library("esphome/ESP32-audioI2S", "2.1.0")
cg.add_build_flag("-DAUDIO_NO_SD_FS")

View File

@@ -3,13 +3,16 @@ import esphome.config_validation as cv
from esphome import pins
from esphome.components import display, spi
from esphome.const import (
CONF_COLOR_PALETTE,
CONF_DC_PIN,
CONF_ID,
CONF_LAMBDA,
CONF_MODEL,
CONF_PAGES,
CONF_RAW_DATA_ID,
CONF_RESET_PIN,
)
from esphome.core import HexInt
DEPENDENCIES = ["spi"]
@@ -21,16 +24,21 @@ ili9341 = ili9341_ns.class_(
)
ILI9341M5Stack = ili9341_ns.class_("ILI9341M5Stack", ili9341)
ILI9341TFT24 = ili9341_ns.class_("ILI9341TFT24", ili9341)
ILI9341TFT24R = ili9341_ns.class_("ILI9341TFT24R", ili9341)
ILI9341Model = ili9341_ns.enum("ILI9341Model")
ILI9341ColorMode = ili9341_ns.enum("ILI9341ColorMode")
MODELS = {
"M5STACK": ILI9341Model.M5STACK,
"TFT_2.4": ILI9341Model.TFT_24,
"TFT_2.4R": ILI9341Model.TFT_24R,
}
ILI9341_MODEL = cv.enum(MODELS, upper=True, space="_")
COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE")
CONFIG_SCHEMA = cv.All(
display.FULL_DISPLAY_SCHEMA.extend(
{
@@ -39,6 +47,8 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_LED_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_COLOR_PALETTE, default="NONE"): COLOR_PALETTE,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
)
.extend(cv.polling_component_schema("1s"))
@@ -52,6 +62,8 @@ async def to_code(config):
lcd_type = ILI9341M5Stack
if config[CONF_MODEL] == "TFT_2.4":
lcd_type = ILI9341TFT24
if config[CONF_MODEL] == "TFT_2.4R":
lcd_type = ILI9341TFT24R
rhs = lcd_type.new()
var = cg.Pvariable(config[CONF_ID], rhs)
@@ -73,3 +85,13 @@ async def to_code(config):
if CONF_LED_PIN in config:
led_pin = await cg.gpio_pin_expression(config[CONF_LED_PIN])
cg.add(var.set_led_pin(led_pin))
if config[CONF_COLOR_PALETTE] == "GRAYSCALE":
cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8_INDEXED))
rhs = []
for x in range(256):
rhs.extend([HexInt(x), HexInt(x), HexInt(x)])
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
cg.add(var.set_palette(prog_arr))
else:
pass

View File

@@ -112,29 +112,9 @@ void ILI9341Display::display_() {
this->y_high_ = 0;
}
uint16_t ILI9341Display::convert_to_16bit_color_(uint8_t color_8bit) {
int r = color_8bit >> 5;
int g = (color_8bit >> 2) & 0x07;
int b = color_8bit & 0x03;
uint16_t color = (r * 0x04) << 11;
color |= (g * 0x09) << 5;
color |= (b * 0x0A);
return color;
}
uint8_t ILI9341Display::convert_to_8bit_color_(uint16_t color_16bit) {
// convert 16bit color to 8 bit buffer
uint8_t r = color_16bit >> 11;
uint8_t g = (color_16bit >> 5) & 0x3F;
uint8_t b = color_16bit & 0x1F;
return ((b / 0x0A) | ((g / 0x09) << 2) | ((r / 0x04) << 5));
}
void ILI9341Display::fill(Color color) {
auto color565 = display::ColorUtil::color_to_565(color);
memset(this->buffer_, convert_to_8bit_color_(color565), this->get_buffer_length_());
uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
memset(this->buffer_, color332, this->get_buffer_length_());
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->get_width_internal() - 1;
@@ -181,8 +161,13 @@ void HOT ILI9341Display::draw_absolute_pixel_internal(int x, int y, Color color)
this->y_high_ = (y > this->y_high_) ? y : this->y_high_;
uint32_t pos = (y * width_) + x;
auto color565 = display::ColorUtil::color_to_565(color);
buffer_[pos] = convert_to_8bit_color_(color565);
if (this->buffer_color_mode_ == BITS_8) {
uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
buffer_[pos] = color332;
} else { // if (this->buffer_color_mode_ == BITS_8_INDEXED) {
uint8_t index = display::ColorUtil::color_to_index8_palette888(color, this->palette_);
buffer_[pos] = index;
}
}
// should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color
@@ -247,7 +232,13 @@ uint32_t ILI9341Display::buffer_to_transfer_(uint32_t pos, uint32_t sz) {
}
for (uint32_t i = 0; i < sz; ++i) {
uint16_t color = convert_to_16bit_color_(*src++);
uint16_t color;
if (this->buffer_color_mode_ == BITS_8) {
color = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(*src++));
} else { // if (this->buffer_color_mode == BITS_8_INDEXED) {
Color col = display::ColorUtil::index8_to_color_palette888(*src++, this->palette_);
color = display::ColorUtil::color_to_565(col);
}
*dst++ = (uint8_t)(color >> 8);
*dst++ = (uint8_t) color;
}
@@ -272,5 +263,13 @@ void ILI9341TFT24::initialize() {
this->fill_internal_(Color::BLACK);
}
// 24_TFT rotated display
void ILI9341TFT24R::initialize() {
this->init_lcd_(INITCMD_TFT);
this->width_ = 320;
this->height_ = 240;
this->fill_internal_(Color::BLACK);
}
} // namespace ili9341
} // namespace esphome

View File

@@ -12,6 +12,12 @@ namespace ili9341 {
enum ILI9341Model {
M5STACK = 0,
TFT_24,
TFT_24R,
};
enum ILI9341ColorMode {
BITS_8,
BITS_8_INDEXED,
};
class ILI9341Display : public PollingComponent,
@@ -24,6 +30,8 @@ class ILI9341Display : public PollingComponent,
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
void set_led_pin(GPIOPin *led) { this->led_pin_ = led; }
void set_model(ILI9341Model model) { this->model_ = model; }
void set_palette(const uint8_t *palette) { this->palette_ = palette; }
void set_buffer_color_mode(ILI9341ColorMode color_mode) { this->buffer_color_mode_ = color_mode; }
void command(uint8_t value);
void data(uint8_t value);
@@ -41,6 +49,8 @@ class ILI9341Display : public PollingComponent,
this->initialize();
}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
void setup_pins_();
@@ -51,8 +61,6 @@ class ILI9341Display : public PollingComponent,
void reset_();
void fill_internal_(Color color);
void display_();
uint16_t convert_to_16bit_color_(uint8_t color_8bit);
uint8_t convert_to_8bit_color_(uint16_t color_16bit);
ILI9341Model model_;
int16_t width_{320}; ///< Display width as modified by current rotation
@@ -61,6 +69,9 @@ class ILI9341Display : public PollingComponent,
uint16_t y_low_{0};
uint16_t x_high_{0};
uint16_t y_high_{0};
const uint8_t *palette_;
ILI9341ColorMode buffer_color_mode_{BITS_8};
uint32_t get_buffer_length_();
int get_width_internal() override;
@@ -92,5 +103,12 @@ class ILI9341TFT24 : public ILI9341Display {
public:
void initialize() override;
};
//----------- ILI9341_24_TFT rotated display --------------
class ILI9341TFT24R : public ILI9341Display {
public:
void initialize() override;
};
} // namespace ili9341
} // namespace esphome

View File

@@ -25,6 +25,7 @@ IMAGE_TYPE = {
"GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
"RGB24": ImageType.IMAGE_TYPE_RGB24,
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY,
"RGB565": ImageType.IMAGE_TYPE_RGB565,
}
Image_ = display.display_ns.class_("Image")
@@ -89,6 +90,21 @@ async def to_code(config):
data[pos] = pix[2]
pos += 1
elif config[CONF_TYPE] == "RGB565":
image = image.convert("RGB")
pixels = list(image.getdata())
data = [0 for _ in range(height * width * 3)]
pos = 0
for pix in pixels:
R = pix[0] >> 3
G = pix[1] >> 2
B = pix[2] >> 3
rgb = (R << 11) | (G << 5) | B
data[pos] = rgb >> 8
pos += 1
data[pos] = rgb & 255
pos += 1
elif config[CONF_TYPE] == "BINARY":
image = image.convert("1", dither=dither)
width8 = ((width + 7) // 8) * 8

View File

@@ -1,6 +1,8 @@
from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_LOGGER
from esphome.components.logger import USB_CDC, USB_SERIAL_JTAG
from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.core import CORE
import esphome.final_validate as fv
CODEOWNERS = ["@esphome/core"]
@@ -17,14 +19,19 @@ CONFIG_SCHEMA = cv.Schema(
).extend(cv.COMPONENT_SCHEMA)
def validate_logger_baud_rate(config):
def validate_logger(config):
logger_conf = fv.full_config.get()[CONF_LOGGER]
if logger_conf[CONF_BAUD_RATE] == 0:
raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0")
if CORE.using_esp_idf:
if logger_conf[CONF_HARDWARE_UART] in [USB_SERIAL_JTAG, USB_CDC]:
raise cv.Invalid(
"improv_serial does not support the selected logger hardware_uart"
)
return config
FINAL_VALIDATE_SCHEMA = validate_logger_baud_rate
FINAL_VALIDATE_SCHEMA = validate_logger
async def to_code(config):

View File

@@ -51,7 +51,7 @@ class ImprovSerialComponent : public Component {
void write_data_(std::vector<uint8_t> &data);
#ifdef USE_ARDUINO
HardwareSerial *hw_serial_{nullptr};
Stream *hw_serial_{nullptr};
#endif
#ifdef USE_ESP_IDF
uart_port_t uart_num_;

View File

@@ -86,6 +86,10 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public
void block_partial() { this->block_partial_ = true; }
display::DisplayType get_display_type() override {
return get_greyscale() ? display::DisplayType::DISPLAY_TYPE_GRAYSCALE : display::DisplayType::DISPLAY_TYPE_BINARY;
}
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
void display1b_();

View File

@@ -26,21 +26,33 @@ std::string build_json(const json_build_t &f) {
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
#endif
const size_t request_size = std::min(free_heap, (size_t) 512);
DynamicJsonDocument json_document(request_size);
if (json_document.capacity() == 0) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
request_size, free_heap);
return "{}";
size_t request_size = std::min(free_heap, (size_t) 512);
while (true) {
ESP_LOGV(TAG, "Attempting to allocate %u bytes for JSON serialization", request_size);
DynamicJsonDocument json_document(request_size);
if (json_document.capacity() == 0) {
ESP_LOGE(TAG,
"Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
request_size, free_heap);
return "{}";
}
JsonObject root = json_document.to<JsonObject>();
f(root);
if (json_document.overflowed()) {
if (request_size == free_heap) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %u bytes",
free_heap);
return "{}";
}
request_size = std::min(request_size * 2, free_heap);
continue;
}
json_document.shrinkToFit();
ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity());
std::string output;
serializeJson(json_document, output);
return output;
}
JsonObject root = json_document.to<JsonObject>();
f(root);
json_document.shrinkToFit();
ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity());
std::string output;
serializeJson(json_document, output);
return output;
}
void parse_json(const std::string &data, const json_parse_t &f) {

View File

@@ -145,7 +145,6 @@ void LightState::loop() {
}
float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
uint32_t LightState::hash_base() { return 1114400283; }
void LightState::publish_state() { this->remote_values_callback_.call(); }

View File

@@ -150,8 +150,6 @@ class LightState : public EntityBase, public Component {
friend LightCall;
friend class AddressableLight;
uint32_t hash_base() override;
/// Internal method to start an effect with the given index
void start_effect_(uint32_t effect_index);
/// Internal method to get the currently active effect

View File

@@ -57,7 +57,6 @@ void Lock::publish_state(LockState state) {
}
void Lock::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
uint32_t Lock::hash_base() { return 856245656UL; }
void LockCall::perform() {
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());

View File

@@ -167,8 +167,6 @@ class Lock : public EntityBase {
*/
virtual void control(const LockCall &call) = 0;
uint32_t hash_base() override;
CallbackManager<void()> state_callback_{};
Deduplicator<LockState> publish_dedup_;
ESPPreferenceObject rtc_;

View File

@@ -19,8 +19,13 @@ from esphome.const import (
CONF_TX_BUFFER_SIZE,
)
from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority
from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32.const import VARIANT_ESP32S2, VARIANT_ESP32C3
from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32S2,
VARIANT_ESP32C3,
VARIANT_ESP32S3,
)
CODEOWNERS = ["@esphome/core"]
logger_ns = cg.esphome_ns.namespace("logger")
@@ -54,36 +59,51 @@ LOG_LEVEL_SEVERITY = [
"VERY_VERBOSE",
]
ESP32_REDUCED_VARIANTS = [VARIANT_ESP32C3, VARIANT_ESP32S2]
UART0 = "UART0"
UART1 = "UART1"
UART2 = "UART2"
UART0_SWAP = "UART0_SWAP"
USB_SERIAL_JTAG = "USB_SERIAL_JTAG"
USB_CDC = "USB_CDC"
UART_SELECTION_ESP32_REDUCED = ["UART0", "UART1"]
UART_SELECTION_ESP32 = {
VARIANT_ESP32: [UART0, UART1, UART2],
VARIANT_ESP32S2: [UART0, UART1, USB_CDC],
VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32C3: [UART0, UART1, USB_SERIAL_JTAG],
}
UART_SELECTION_ESP32 = ["UART0", "UART1", "UART2"]
UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1]
UART_SELECTION_ESP8266 = ["UART0", "UART0_SWAP", "UART1"]
ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG]
HARDWARE_UART_TO_UART_SELECTION = {
"UART0": logger_ns.UART_SELECTION_UART0,
"UART0_SWAP": logger_ns.UART_SELECTION_UART0_SWAP,
"UART1": logger_ns.UART_SELECTION_UART1,
"UART2": logger_ns.UART_SELECTION_UART2,
UART0: logger_ns.UART_SELECTION_UART0,
UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP,
UART1: logger_ns.UART_SELECTION_UART1,
UART2: logger_ns.UART_SELECTION_UART2,
USB_CDC: logger_ns.UART_SELECTION_USB_CDC,
USB_SERIAL_JTAG: logger_ns.UART_SELECTION_USB_SERIAL_JTAG,
}
HARDWARE_UART_TO_SERIAL = {
"UART0": cg.global_ns.Serial,
"UART0_SWAP": cg.global_ns.Serial,
"UART1": cg.global_ns.Serial1,
"UART2": cg.global_ns.Serial2,
UART0: cg.global_ns.Serial,
UART0_SWAP: cg.global_ns.Serial,
UART1: cg.global_ns.Serial1,
UART2: cg.global_ns.Serial2,
}
is_log_level = cv.one_of(*LOG_LEVELS, upper=True)
def uart_selection(value):
if value.upper() in ESP_IDF_UARTS:
if not CORE.using_esp_idf:
raise cv.Invalid(f"Only esp-idf framework supports {value}.")
if CORE.is_esp32:
if get_esp32_variant() in ESP32_REDUCED_VARIANTS:
return cv.one_of(*UART_SELECTION_ESP32_REDUCED, upper=True)(value)
return cv.one_of(*UART_SELECTION_ESP32, upper=True)(value)
variant = get_esp32_variant()
if variant in UART_SELECTION_ESP32:
return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value)
if CORE.is_esp8266:
return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value)
raise NotImplementedError
@@ -113,7 +133,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int,
cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes,
cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean,
cv.Optional(CONF_HARDWARE_UART, default="UART0"): uart_selection,
cv.Optional(CONF_HARDWARE_UART, default=UART0): uart_selection,
cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level,
cv.Optional(CONF_LOGS, default={}): cv.Schema(
{
@@ -185,6 +205,12 @@ async def to_code(config):
if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH):
cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH")
if CORE.using_esp_idf:
if config[CONF_HARDWARE_UART] == USB_CDC:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
# Register at end for safe mode
await cg.register_component(log, config)

View File

@@ -116,8 +116,22 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) {
this->hw_serial_->println(msg);
#endif // USE_ARDUINO
#ifdef USE_ESP_IDF
uart_write_bytes(uart_num_, msg, strlen(msg));
uart_write_bytes(uart_num_, "\n", 1);
if (
#if defined(USE_ESP32_VARIANT_ESP32S2)
uart_ == UART_SELECTION_USB_CDC
#elif defined(USE_ESP32_VARIANT_ESP32C3)
uart_ == UART_SELECTION_USB_SERIAL_JTAG
#elif defined(USE_ESP32_VARIANT_ESP32S3)
uart_ == UART_SELECTION_USB_CDC || uart_ == UART_SELECTION_USB_SERIAL_JTAG
#else
/* DISABLES CODE */ (false)
#endif
) {
puts(msg);
} else {
uart_write_bytes(uart_num_, msg, strlen(msg));
uart_write_bytes(uart_num_, "\n", 1);
}
#endif
}
@@ -149,13 +163,26 @@ void Logger::pre_setup() {
case UART_SELECTION_UART0_SWAP:
#endif
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
#ifdef USE_ESP8266
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
Serial.swap();
}
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
#endif
break;
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
#ifdef USE_ESP8266
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
#endif
break;
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2)
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && \
!defined(USE_ESP32_VARIANT_ESP32S3)
case UART_SELECTION_UART2:
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
break;
#endif
}
@@ -169,39 +196,41 @@ void Logger::pre_setup() {
case UART_SELECTION_UART1:
uart_num_ = UART_NUM_1;
break;
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2)
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
case UART_SELECTION_UART2:
uart_num_ = UART_NUM_2;
break;
#endif
#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
case UART_SELECTION_USB_CDC:
uart_num_ = -1;
break;
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3)
case UART_SELECTION_USB_SERIAL_JTAG:
uart_num_ = -1;
break;
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3
}
uart_config_t uart_config{};
uart_config.baud_rate = (int) baud_rate_;
uart_config.data_bits = UART_DATA_8_BITS;
uart_config.parity = UART_PARITY_DISABLE;
uart_config.stop_bits = UART_STOP_BITS_1;
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
uart_param_config(uart_num_, &uart_config);
const int uart_buffer_size = tx_buffer_size_;
// Install UART driver using an event queue here
uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0);
#endif
#ifdef USE_ARDUINO
this->hw_serial_->begin(this->baud_rate_);
#ifdef USE_ESP8266
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
this->hw_serial_->swap();
if (uart_num_ >= 0) {
uart_config_t uart_config{};
uart_config.baud_rate = (int) baud_rate_;
uart_config.data_bits = UART_DATA_8_BITS;
uart_config.parity = UART_PARITY_DISABLE;
uart_config.stop_bits = UART_STOP_BITS_1;
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
uart_param_config(uart_num_, &uart_config);
const int uart_buffer_size = tx_buffer_size_;
// Install UART driver using an event queue here
uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0);
}
this->hw_serial_->setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
#endif
#endif // USE_ARDUINO
#endif // USE_ESP_IDF
}
#ifdef USE_ESP8266
else {
uart_set_debug(UART_NO);
}
#endif
#endif // USE_ESP8266
global_logger = this;
#if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO)
@@ -209,7 +238,7 @@ void Logger::pre_setup() {
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
esp_log_level_set("*", ESP_LOG_VERBOSE);
}
#endif
#endif // USE_ESP_IDF || USE_ESP32_FRAMEWORK_ARDUINO
ESP_LOGI(TAG, "Log initialized");
}
@@ -224,11 +253,24 @@ void Logger::add_on_log_callback(std::function<void(int, const char *, const cha
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
#ifdef USE_ESP32
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART2"};
#endif
const char *const UART_SELECTIONS[] = {
"UART0", "UART1",
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
"UART2",
#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
#if defined(USE_ESP_IDF)
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
"USB_CDC",
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3)
"USB_SERIAL_JTAG",
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3
#endif // USE_ESP_IDF
};
#endif // USE_ESP32
#ifdef USE_ESP8266
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"};
#endif
#endif // USE_ESP8266
void Logger::dump_config() {
ESP_LOGCONFIG(TAG, "Logger:");
ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]);

View File

@@ -24,9 +24,19 @@ namespace logger {
enum UARTSelection {
UART_SELECTION_UART0 = 0,
UART_SELECTION_UART1,
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2)
#if defined(USE_ESP32)
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
UART_SELECTION_UART2,
#endif
#ifdef USE_ESP_IDF
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
UART_SELECTION_USB_CDC,
#endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3)
UART_SELECTION_USB_SERIAL_JTAG,
#endif
#endif
#endif
#ifdef USE_ESP8266
UART_SELECTION_UART0_SWAP,
#endif
@@ -40,7 +50,7 @@ class Logger : public Component {
void set_baud_rate(uint32_t baud_rate);
uint32_t get_baud_rate() const { return baud_rate_; }
#ifdef USE_ARDUINO
HardwareSerial *get_hw_serial() const { return hw_serial_; }
Stream *get_hw_serial() const { return hw_serial_; }
#endif
#ifdef USE_ESP_IDF
uart_port_t get_uart_num() const { return uart_num_; }
@@ -119,7 +129,7 @@ class Logger : public Component {
int tx_buffer_size_{0};
UARTSelection uart_{UART_SELECTION_UART0};
#ifdef USE_ARDUINO
HardwareSerial *hw_serial_{nullptr};
Stream *hw_serial_{nullptr};
#endif
#ifdef USE_ESP_IDF
uart_port_t uart_num_;

View File

@@ -94,6 +94,14 @@ void MAX31865Sensor::read_data_() {
const uint16_t rtd_resistance_register = this->read_register_16_(RTD_RESISTANCE_MSB_REG);
this->write_config_(0b11000000, 0b00000000);
// Check for bad connection
if (rtd_resistance_register == 0b0000000000000000 || rtd_resistance_register == 0b1111111111111111) {
ESP_LOGE(TAG, "SPI bus read all 0 or all 1 (0x%04X), check MAX31865 wiring & power.", rtd_resistance_register);
this->publish_state(NAN);
this->status_set_error();
return;
}
// Check faults
const uint8_t faults = this->read_register_(FAULT_STATUS_REG);
if ((has_fault_ = faults & 0b00111100)) {

View File

@@ -11,6 +11,9 @@ from esphome.const import (
UNIT_CELSIUS,
)
CODEOWNERS = ["@DAVe3283"]
DEPENDENCIES = ["spi"]
max31865_ns = cg.esphome_ns.namespace("max31865")
MAX31865Sensor = max31865_ns.class_(
"MAX31865Sensor", sensor.Sensor, cg.PollingComponent, spi.SPIDevice

View File

@@ -93,6 +93,8 @@ class MAX7219Component : public PollingComponent,
uint8_t strftimedigit(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0)));
#endif
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
protected:
void send_byte_(uint8_t a_register, uint8_t data);
void send_to_all_(uint8_t a_register, uint8_t data);

View File

@@ -0,0 +1,104 @@
from esphome import automation
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.automation import maybe_simple_id
from esphome.const import CONF_ID
from esphome.core import CORE
from esphome.coroutine import coroutine_with_priority
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@jesserockz"]
IS_PLATFORM_COMPONENT = True
media_player_ns = cg.esphome_ns.namespace("media_player")
MediaPlayer = media_player_ns.class_("MediaPlayer")
PlayAction = media_player_ns.class_(
"PlayAction", automation.Action, cg.Parented.template(MediaPlayer)
)
ToggleAction = media_player_ns.class_(
"ToggleAction", automation.Action, cg.Parented.template(MediaPlayer)
)
PauseAction = media_player_ns.class_(
"PauseAction", automation.Action, cg.Parented.template(MediaPlayer)
)
StopAction = media_player_ns.class_(
"StopAction", automation.Action, cg.Parented.template(MediaPlayer)
)
VolumeUpAction = media_player_ns.class_(
"VolumeUpAction", automation.Action, cg.Parented.template(MediaPlayer)
)
VolumeDownAction = media_player_ns.class_(
"VolumeDownAction", automation.Action, cg.Parented.template(MediaPlayer)
)
VolumeSetAction = media_player_ns.class_(
"VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer)
)
CONF_VOLUME = "volume"
async def setup_media_player_core_(var, config):
await setup_entity(var, config)
async def register_media_player(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_media_player(var))
await setup_media_player_core_(var, config)
MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.Schema({}))
MEDIA_PLAYER_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(MediaPlayer)})
@automation.register_action("media_player.play", PlayAction, MEDIA_PLAYER_ACTION_SCHEMA)
@automation.register_action(
"media_player.toggle", ToggleAction, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_action(
"media_player.pause", PauseAction, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_action("media_player.stop", StopAction, MEDIA_PLAYER_ACTION_SCHEMA)
@automation.register_action(
"media_player.volume_up", VolumeUpAction, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_action(
"media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA
)
async def media_player_action(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@automation.register_action(
"media_player.volume_set",
VolumeSetAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(MediaPlayer),
cv.Required(CONF_VOLUME): cv.templatable(cv.percentage),
},
key=CONF_VOLUME,
),
)
async def media_player_volume_set_action(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
volume = await cg.templatable(config[CONF_VOLUME], args, float)
cg.add(var.set_volume(volume))
return var
@coroutine_with_priority(100.0)
async def to_code(config):
cg.add_global(media_player_ns.using)
cg.add_define("USE_MEDIA_PLAYER")

View File

@@ -0,0 +1,30 @@
#pragma once
#include "esphome/core/automation.h"
#include "media_player.h"
namespace esphome {
namespace media_player {
#define MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ACTION_CLASS, ACTION_COMMAND) \
template<typename... Ts> class ACTION_CLASS : public Action<Ts...>, public Parented<MediaPlayer> { \
void play(Ts... x) override { \
this->parent_->make_call().set_command(MediaPlayerCommand::MEDIA_PLAYER_COMMAND_##ACTION_COMMAND).perform(); \
} \
};
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PlayAction, PLAY)
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PauseAction, PAUSE)
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(StopAction, STOP)
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ToggleAction, TOGGLE)
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeUpAction, VOLUME_UP)
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeDownAction, VOLUME_DOWN)
template<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Parented<MediaPlayer> {
TEMPLATABLE_VALUE(float, volume)
void play(Ts... x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); }
};
} // namespace media_player
} // namespace esphome

View File

@@ -0,0 +1,118 @@
#include "media_player.h"
#include "esphome/core/log.h"
namespace esphome {
namespace media_player {
static const char *const TAG = "media_player";
const char *media_player_state_to_string(MediaPlayerState state) {
switch (state) {
case MEDIA_PLAYER_STATE_IDLE:
return "IDLE";
case MEDIA_PLAYER_STATE_PLAYING:
return "PLAYING";
case MEDIA_PLAYER_STATE_PAUSED:
return "PAUSED";
case MEDIA_PLAYER_STATE_NONE:
default:
return "UNKNOWN";
}
}
const char *media_player_command_to_string(MediaPlayerCommand command) {
switch (command) {
case MEDIA_PLAYER_COMMAND_PLAY:
return "PLAY";
case MEDIA_PLAYER_COMMAND_PAUSE:
return "PAUSE";
case MEDIA_PLAYER_COMMAND_STOP:
return "STOP";
case MEDIA_PLAYER_COMMAND_MUTE:
return "MUTE";
case MEDIA_PLAYER_COMMAND_UNMUTE:
return "UNMUTE";
case MEDIA_PLAYER_COMMAND_TOGGLE:
return "TOGGLE";
default:
return "UNKNOWN";
}
}
void MediaPlayerCall::validate_() {
if (this->media_url_.has_value()) {
if (this->command_.has_value()) {
ESP_LOGW(TAG, "MediaPlayerCall: Setting both command and media_url is not needed.");
this->command_.reset();
}
}
if (this->volume_.has_value()) {
if (this->volume_.value() < 0.0f || this->volume_.value() > 1.0f) {
ESP_LOGW(TAG, "MediaPlayerCall: Volume must be between 0.0 and 1.0.");
this->volume_.reset();
}
}
}
void MediaPlayerCall::perform() {
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
this->validate_();
if (this->command_.has_value()) {
const char *command_s = media_player_command_to_string(this->command_.value());
ESP_LOGD(TAG, " Command: %s", command_s);
}
if (this->media_url_.has_value()) {
ESP_LOGD(TAG, " Media URL: %s", this->media_url_.value().c_str());
}
if (this->volume_.has_value()) {
ESP_LOGD(TAG, " Volume: %.2f", this->volume_.value());
}
this->parent_->control(*this);
}
MediaPlayerCall &MediaPlayerCall::set_command(MediaPlayerCommand command) {
this->command_ = command;
return *this;
}
MediaPlayerCall &MediaPlayerCall::set_command(optional<MediaPlayerCommand> command) {
this->command_ = command;
return *this;
}
MediaPlayerCall &MediaPlayerCall::set_command(const std::string &command) {
if (str_equals_case_insensitive(command, "PLAY")) {
this->set_command(MEDIA_PLAYER_COMMAND_PLAY);
} else if (str_equals_case_insensitive(command, "PAUSE")) {
this->set_command(MEDIA_PLAYER_COMMAND_PAUSE);
} else if (str_equals_case_insensitive(command, "STOP")) {
this->set_command(MEDIA_PLAYER_COMMAND_STOP);
} else if (str_equals_case_insensitive(command, "MUTE")) {
this->set_command(MEDIA_PLAYER_COMMAND_MUTE);
} else if (str_equals_case_insensitive(command, "UNMUTE")) {
this->set_command(MEDIA_PLAYER_COMMAND_UNMUTE);
} else if (str_equals_case_insensitive(command, "TOGGLE")) {
this->set_command(MEDIA_PLAYER_COMMAND_TOGGLE);
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command.c_str());
}
return *this;
}
MediaPlayerCall &MediaPlayerCall::set_media_url(const std::string &media_url) {
this->media_url_ = media_url;
return *this;
}
MediaPlayerCall &MediaPlayerCall::set_volume(float volume) {
this->volume_ = volume;
return *this;
}
void MediaPlayer::add_on_state_callback(std::function<void()> &&callback) {
this->state_callback_.add(std::move(callback));
}
void MediaPlayer::publish_state() { this->state_callback_.call(); }
} // namespace media_player
} // namespace esphome

View File

@@ -0,0 +1,93 @@
#pragma once
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace media_player {
enum MediaPlayerState : uint8_t {
MEDIA_PLAYER_STATE_NONE = 0,
MEDIA_PLAYER_STATE_IDLE = 1,
MEDIA_PLAYER_STATE_PLAYING = 2,
MEDIA_PLAYER_STATE_PAUSED = 3
};
const char *media_player_state_to_string(MediaPlayerState state);
enum MediaPlayerCommand : uint8_t {
MEDIA_PLAYER_COMMAND_PLAY = 0,
MEDIA_PLAYER_COMMAND_PAUSE = 1,
MEDIA_PLAYER_COMMAND_STOP = 2,
MEDIA_PLAYER_COMMAND_MUTE = 3,
MEDIA_PLAYER_COMMAND_UNMUTE = 4,
MEDIA_PLAYER_COMMAND_TOGGLE = 5,
MEDIA_PLAYER_COMMAND_VOLUME_UP = 6,
MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7,
};
const char *media_player_command_to_string(MediaPlayerCommand command);
class MediaPlayer;
class MediaPlayerTraits {
public:
MediaPlayerTraits() = default;
void set_supports_pause(bool supports_pause) { this->supports_pause_ = supports_pause; }
bool get_supports_pause() const { return this->supports_pause_; }
protected:
bool supports_pause_{false};
};
class MediaPlayerCall {
public:
MediaPlayerCall(MediaPlayer *parent) : parent_(parent) {}
MediaPlayerCall &set_command(MediaPlayerCommand command);
MediaPlayerCall &set_command(optional<MediaPlayerCommand> command);
MediaPlayerCall &set_command(const std::string &command);
MediaPlayerCall &set_media_url(const std::string &url);
MediaPlayerCall &set_volume(float volume);
void perform();
const optional<MediaPlayerCommand> &get_command() const { return command_; }
const optional<std::string> &get_media_url() const { return media_url_; }
const optional<float> &get_volume() const { return volume_; }
protected:
void validate_();
MediaPlayer *const parent_;
optional<MediaPlayerCommand> command_;
optional<std::string> media_url_;
optional<float> volume_;
};
class MediaPlayer : public EntityBase {
public:
MediaPlayerState state{MEDIA_PLAYER_STATE_NONE};
float volume{1.0f};
MediaPlayerCall make_call() { return MediaPlayerCall(this); }
void publish_state();
void add_on_state_callback(std::function<void()> &&callback);
virtual bool is_muted() const { return false; }
virtual MediaPlayerTraits get_traits() = 0;
protected:
friend MediaPlayerCall;
virtual void control(const MediaPlayerCall &call) = 0;
CallbackManager<void()> state_callback_{};
};
} // namespace media_player
} // namespace esphome

View File

@@ -56,6 +56,11 @@ template<typename... Ts> class PowerOffAction : public MideaActionBase<Ts...> {
void play(Ts... x) override { this->parent_->do_power_off(); }
};
template<typename... Ts> class PowerToggleAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_power_toggle(); }
};
} // namespace ac
} // namespace midea
} // namespace esphome

View File

@@ -39,6 +39,7 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
void do_beeper_off() { this->set_beeper_feedback(false); }
void do_power_on() { this->base_.setPowerState(true); }
void do_power_off() { this->base_.setPowerState(false); }
void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); }
void set_supported_modes(const std::set<ClimateMode> &modes) { this->supported_modes_ = modes; }
void set_supported_swing_modes(const std::set<ClimateSwingMode> &modes) { this->supported_swing_modes_ = modes; }
void set_supported_presets(const std::set<ClimatePreset> &presets) { this->supported_presets_ = presets; }

View File

@@ -113,7 +113,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_PERIOD, default="1s"): cv.time_period,
cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period,
cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5),
cv.Optional(CONF_TRANSMITTER_ID): cv.use_id(
cv.OnlyWith(CONF_TRANSMITTER_ID, "remote_transmitter"): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
),
cv.Optional(CONF_BEEPER, default=False): cv.boolean,
@@ -163,6 +163,7 @@ BeeperOnAction = midea_ac_ns.class_("BeeperOnAction", automation.Action)
BeeperOffAction = midea_ac_ns.class_("BeeperOffAction", automation.Action)
PowerOnAction = midea_ac_ns.class_("PowerOnAction", automation.Action)
PowerOffAction = midea_ac_ns.class_("PowerOffAction", automation.Action)
PowerToggleAction = midea_ac_ns.class_("PowerToggleAction", automation.Action)
MIDEA_ACTION_BASE_SCHEMA = cv.Schema(
{
@@ -249,6 +250,16 @@ async def power_off_to_code(var, config, args):
pass
# Power Toggle action
@register_action(
"power_toggle",
PowerToggleAction,
cv.Schema({}),
)
async def power_inv_to_code(var, config, args):
pass
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -68,33 +68,54 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
uint8_t data_len = raw[2];
uint8_t data_offset = 3;
// the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands
if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) {
data_offset = 2;
data_len = 4;
}
// Error ( msb indicates error )
// response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc
if ((function_code & 0x80) == 0x80) {
data_offset = 2;
data_len = 1;
}
// Per https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf Ch 5 User-Defined function codes
if (((function_code >= 65) && (function_code <= 72)) || ((function_code >= 100) && (function_code <= 110))) {
// Handle user-defined function, since we don't know how big this ought to be,
// ideally we should delegate the entire length detection to whatever handler is
// installed, but wait, there is the CRC, and if we get a hit there is a good
// chance that this is a complete message ... admittedly there is a small chance is
// isn't but that is quite small given the purpose of the CRC in the first place
data_len = at;
data_offset = 1;
// Byte data_offset..data_offset+data_len-1: Data
if (at < data_offset + data_len)
return true;
uint16_t computed_crc = crc16(raw, data_offset + data_len);
uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
// Byte 3+data_len: CRC_LO (over all bytes)
if (at == data_offset + data_len)
return true;
if (computed_crc != remote_crc)
return true;
// Byte data_offset+len+1: CRC_HI (over all bytes)
uint16_t computed_crc = crc16(raw, data_offset + data_len);
uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
if (computed_crc != remote_crc) {
ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
return false;
ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code);
} else {
// the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands
if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) {
data_offset = 2;
data_len = 4;
}
// Error ( msb indicates error )
// response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc
if ((function_code & 0x80) == 0x80) {
data_offset = 2;
data_len = 1;
}
// Byte data_offset..data_offset+data_len-1: Data
if (at < data_offset + data_len)
return true;
// Byte 3+data_len: CRC_LO (over all bytes)
if (at == data_offset + data_len)
return true;
// Byte data_offset+len+1: CRC_HI (over all bytes)
uint16_t computed_crc = crc16(raw, data_offset + data_len);
uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
if (computed_crc != remote_crc) {
ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
return false;
}
}
std::vector<uint8_t> data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len);
bool found = false;

View File

@@ -24,6 +24,8 @@ from esphome.const import (
CONF_LOG_TOPIC,
CONF_ON_JSON_MESSAGE,
CONF_ON_MESSAGE,
CONF_ON_CONNECT,
CONF_ON_DISCONNECT,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_PAYLOAD_AVAILABLE,
@@ -90,6 +92,10 @@ MQTTMessageTrigger = mqtt_ns.class_(
MQTTJsonMessageTrigger = mqtt_ns.class_(
"MQTTJsonMessageTrigger", automation.Trigger.template(cg.JsonObjectConst)
)
MQTTConnectTrigger = mqtt_ns.class_("MQTTConnectTrigger", automation.Trigger.template())
MQTTDisconnectTrigger = mqtt_ns.class_(
"MQTTDisconnectTrigger", automation.Trigger.template()
)
MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component)
MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition)
@@ -212,6 +218,18 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(
CONF_REBOOT_TIMEOUT, default="15min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTConnectTrigger),
}
),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
MQTTDisconnectTrigger
),
}
),
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTMessageTrigger),
@@ -362,6 +380,14 @@ async def to_code(config):
trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC], conf[CONF_QOS])
await automation.build_automation(trig, [(cg.JsonObjectConst, "x")], conf)
for conf in config.get(CONF_ON_CONNECT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_DISCONNECT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema(
{

View File

@@ -572,6 +572,14 @@ void MQTTClientComponent::on_shutdown() {
this->mqtt_backend_.disconnect();
}
void MQTTClientComponent::set_on_connect(mqtt_on_connect_callback_t &&callback) {
this->mqtt_backend_.set_on_connect(std::forward<mqtt_on_connect_callback_t>(callback));
}
void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&callback) {
this->mqtt_backend_.set_on_disconnect(std::forward<mqtt_on_disconnect_callback_t>(callback));
}
#if ASYNC_TCP_SSL_ENABLED
void MQTTClientComponent::add_ssl_fingerprint(const std::array<uint8_t, SHA1_SIZE> &fingerprint) {
this->mqtt_backend_.setSecure(true);

View File

@@ -19,6 +19,11 @@
namespace esphome {
namespace mqtt {
/** Callback for MQTT events.
*/
using mqtt_on_connect_callback_t = std::function<MQTTBackend::on_connect_callback_t>;
using mqtt_on_disconnect_callback_t = std::function<MQTTBackend::on_disconnect_callback_t>;
/** Callback for MQTT subscriptions.
*
* First parameter is the topic, the second one is the payload.
@@ -240,6 +245,8 @@ class MQTTClientComponent : public Component {
void set_username(const std::string &username) { this->credentials_.username = username; }
void set_password(const std::string &password) { this->credentials_.password = password; }
void set_client_id(const std::string &client_id) { this->credentials_.client_id = client_id; }
void set_on_connect(mqtt_on_connect_callback_t &&callback);
void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback);
protected:
/// Reconnect to the MQTT broker if not already connected.
@@ -328,6 +335,20 @@ class MQTTJsonMessageTrigger : public Trigger<JsonObjectConst> {
}
};
class MQTTConnectTrigger : public Trigger<> {
public:
explicit MQTTConnectTrigger(MQTTClientComponent *&client) {
client->set_on_connect([this](bool session_present) { this->trigger(); });
}
};
class MQTTDisconnectTrigger : public Trigger<> {
public:
explicit MQTTDisconnectTrigger(MQTTClientComponent *&client) {
client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(); });
}
};
template<typename... Ts> class MQTTPublishAction : public Action<Ts...> {
public:
MQTTPublishAction(MQTTClientComponent *parent) : parent_(parent) {}

View File

@@ -51,10 +51,9 @@ void MQTTCoverComponent::setup() {
void MQTTCoverComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str());
auto traits = this->cover_->get_traits();
// no state topic for position
bool state_topic = !traits.get_supports_position();
LOG_MQTT_COMPONENT(state_topic, true)
if (!state_topic) {
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
LOG_MQTT_COMPONENT(true, has_command_topic)
if (traits.get_supports_position()) {
ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic().c_str());
ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic().c_str());
}
@@ -72,7 +71,6 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
root[MQTT_OPTIMISTIC] = true;
}
if (traits.get_supports_position()) {
config.state_topic = false;
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic();
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic();
}
@@ -92,17 +90,7 @@ bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
bool MQTTCoverComponent::publish_state() {
auto traits = this->cover_->get_traits();
bool success = true;
if (!traits.get_supports_position()) {
const char *state_s = "unknown";
if (this->cover_->position == COVER_OPEN) {
state_s = "open";
} else if (this->cover_->position == COVER_CLOSED) {
state_s = "closed";
}
if (!this->publish(this->get_state_topic_(), state_s))
success = false;
} else {
if (traits.get_supports_position()) {
std::string pos = value_accuracy_to_string(roundf(this->cover_->position * 100), 0);
if (!this->publish(this->get_position_state_topic(), pos))
success = false;
@@ -112,6 +100,14 @@ bool MQTTCoverComponent::publish_state() {
if (!this->publish(this->get_tilt_state_topic(), pos))
success = false;
}
const char *state_s = this->cover_->current_operation == COVER_OPERATION_OPENING ? "opening"
: this->cover_->current_operation == COVER_OPERATION_CLOSING ? "closing"
: this->cover_->position == COVER_CLOSED ? "closed"
: this->cover_->position == COVER_OPEN ? "open"
: traits.get_supports_position() ? "open"
: "unknown";
if (!this->publish(this->get_state_topic_(), state_s))
success = false;
return success;
}

View File

@@ -5,7 +5,6 @@
#ifdef USE_MQTT
#ifdef USE_FAN
#include "esphome/components/fan/fan_helpers.h"
namespace esphome {
namespace mqtt {
@@ -88,17 +87,6 @@ void MQTTFanComponent::setup() {
});
}
if (this->state_->get_traits().supports_speed()) {
this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
this->state_->make_call()
.set_speed(payload.c_str()) // NOLINT(clang-diagnostic-deprecated-declarations)
.perform();
#pragma GCC diagnostic pop
});
}
auto f = std::bind(&MQTTFanComponent::publish_state, this);
this->state_->add_on_state_callback([this, f]() { this->defer("send", f); });
}
@@ -113,8 +101,6 @@ void MQTTFanComponent::dump_config() {
if (this->state_->get_traits().supports_speed()) {
ESP_LOGCONFIG(TAG, " Speed Level State Topic: '%s'", this->get_speed_level_state_topic().c_str());
ESP_LOGCONFIG(TAG, " Speed Level Command Topic: '%s'", this->get_speed_level_command_topic().c_str());
ESP_LOGCONFIG(TAG, " Speed State Topic: '%s'", this->get_speed_state_topic().c_str());
ESP_LOGCONFIG(TAG, " Speed Command Topic: '%s'", this->get_speed_command_topic().c_str());
}
}
@@ -126,10 +112,9 @@ void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig
root[MQTT_OSCILLATION_STATE_TOPIC] = this->get_oscillation_state_topic();
}
if (this->state_->get_traits().supports_speed()) {
root["speed_level_command_topic"] = this->get_speed_level_command_topic();
root["speed_level_state_topic"] = this->get_speed_level_state_topic();
root[MQTT_SPEED_COMMAND_TOPIC] = this->get_speed_command_topic();
root[MQTT_SPEED_STATE_TOPIC] = this->get_speed_state_topic();
root[MQTT_PERCENTAGE_COMMAND_TOPIC] = this->get_speed_level_command_topic();
root[MQTT_PERCENTAGE_STATE_TOPIC] = this->get_speed_level_state_topic();
root[MQTT_SPEED_RANGE_MAX] = this->state_->get_traits().supported_speed_count();
}
}
bool MQTTFanComponent::publish_state() {
@@ -148,31 +133,6 @@ bool MQTTFanComponent::publish_state() {
bool success = this->publish(this->get_speed_level_state_topic(), payload);
failed = failed || !success;
}
if (traits.supports_speed()) {
const char *payload;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
// NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations)
switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) {
case FAN_SPEED_LOW: { // NOLINT(clang-diagnostic-deprecated-declarations)
payload = "low";
break;
}
case FAN_SPEED_MEDIUM: { // NOLINT(clang-diagnostic-deprecated-declarations)
payload = "medium";
break;
}
default:
case FAN_SPEED_HIGH: { // NOLINT(clang-diagnostic-deprecated-declarations)
payload = "high";
break;
}
}
#pragma GCC diagnostic pop
bool success = this->publish(this->get_speed_state_topic(), payload);
failed = failed || !success;
}
return !failed;
}

View File

@@ -21,7 +21,7 @@ void MQTTSelectComponent::setup() {
call.set_option(state);
call.perform();
});
this->select_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); });
this->select_->add_on_state_callback([this](const std::string &state, size_t index) { this->publish_state(state); });
}
void MQTTSelectComponent::dump_config() {

View File

@@ -115,7 +115,7 @@ void Nextion::set_backlight_brightness(float brightness) {
ESP_LOGD(TAG, "Brightness out of bounds, percentage range 0-1.0");
return;
}
this->add_no_result_to_queue_with_set("backlight_brightness", "dim", static_cast<int>(brightness * 100));
this->add_no_result_to_queue_with_printf_("backlight_brightness", "dim=%d", static_cast<int>(brightness * 100));
}
void Nextion::set_auto_wake_on_touch(bool auto_wake) {

View File

@@ -14,6 +14,8 @@ from esphome.const import (
CONF_UNIT_OF_MEASUREMENT,
CONF_MQTT_ID,
CONF_VALUE,
CONF_OPERATION,
CONF_CYCLE,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_helpers import setup_entity
@@ -35,6 +37,7 @@ ValueRangeTrigger = number_ns.class_(
# Actions
NumberSetAction = number_ns.class_("NumberSetAction", automation.Action)
NumberOperationAction = number_ns.class_("NumberOperationAction", automation.Action)
# Conditions
NumberInRangeCondition = number_ns.class_(
@@ -49,6 +52,15 @@ NUMBER_MODES = {
"SLIDER": NumberMode.NUMBER_MODE_SLIDER,
}
NumberOperation = number_ns.enum("NumberOperation")
NUMBER_OPERATION_OPTIONS = {
"INCREMENT": NumberOperation.NUMBER_OP_INCREMENT,
"DECREMENT": NumberOperation.NUMBER_OP_DECREMENT,
"TO_MIN": NumberOperation.NUMBER_OP_TO_MIN,
"TO_MAX": NumberOperation.NUMBER_OP_TO_MAX,
}
icon = cv.icon
NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
@@ -159,12 +171,18 @@ async def to_code(config):
cg.add_global(number_ns.using)
OPERATION_BASE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(Number),
}
)
@automation.register_action(
"number.set",
NumberSetAction,
cv.Schema(
OPERATION_BASE_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.use_id(Number),
cv.Required(CONF_VALUE): cv.templatable(cv.float_),
}
),
@@ -175,3 +193,85 @@ async def number_set_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(config[CONF_VALUE], args, float)
cg.add(var.set_value(template_))
return var
@automation.register_action(
"number.increment",
NumberOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="INCREMENT"): cv.one_of(
"INCREMENT", upper=True
),
cv.Optional(CONF_CYCLE, default=True): cv.boolean,
}
)
),
)
@automation.register_action(
"number.decrement",
NumberOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="DECREMENT"): cv.one_of(
"DECREMENT", upper=True
),
cv.Optional(CONF_CYCLE, default=True): cv.boolean,
}
)
),
)
@automation.register_action(
"number.to_min",
NumberOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="TO_MIN"): cv.one_of(
"TO_MIN", upper=True
),
}
)
),
)
@automation.register_action(
"number.to_max",
NumberOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="TO_MAX"): cv.one_of(
"TO_MAX", upper=True
),
}
)
),
)
@automation.register_action(
"number.operation",
NumberOperationAction,
OPERATION_BASE_SCHEMA.extend(
{
cv.Required(CONF_OPERATION): cv.templatable(
cv.enum(NUMBER_OPERATION_OPTIONS, upper=True)
),
cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean),
}
),
)
async def number_to_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_OPERATION in config:
to_ = await cg.templatable(config[CONF_OPERATION], args, NumberOperation)
cg.add(var.set_operation(to_))
if CONF_CYCLE in config:
cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool)
cg.add(var.set_cycle(cycle_))
if CONF_MODE in config:
cg.add(var.set_operation(NUMBER_OPERATION_OPTIONS[config[CONF_MODE]]))
if CONF_CYCLE in config:
cg.add(var.set_cycle(config[CONF_CYCLE]))
return var

View File

@@ -29,6 +29,25 @@ template<typename... Ts> class NumberSetAction : public Action<Ts...> {
Number *number_;
};
template<typename... Ts> class NumberOperationAction : public Action<Ts...> {
public:
explicit NumberOperationAction(Number *number) : number_(number) {}
TEMPLATABLE_VALUE(NumberOperation, operation)
TEMPLATABLE_VALUE(bool, cycle)
void play(Ts... x) override {
auto call = this->number_->make_call();
call.with_operation(this->operation_.value(x...));
if (this->cycle_.has_value()) {
call.with_cycle(this->cycle_.value(x...));
}
call.perform();
}
protected:
Number *number_;
};
class ValueRangeTrigger : public Trigger<float>, public Component {
public:
explicit ValueRangeTrigger(Number *parent) : parent_(parent) {}

View File

@@ -6,30 +6,6 @@ namespace number {
static const char *const TAG = "number";
void NumberCall::perform() {
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
if (!this->value_.has_value() || std::isnan(*this->value_)) {
ESP_LOGW(TAG, "No value set for NumberCall");
return;
}
const auto &traits = this->parent_->traits;
auto value = *this->value_;
float min_value = traits.get_min_value();
if (value < min_value) {
ESP_LOGW(TAG, " Value %f must not be less than minimum %f", value, min_value);
return;
}
float max_value = traits.get_max_value();
if (value > max_value) {
ESP_LOGW(TAG, " Value %f must not be greater than maximum %f", value, max_value);
return;
}
ESP_LOGD(TAG, " Value: %f", *this->value_);
this->parent_->control(*this->value_);
}
void Number::publish_state(float state) {
this->has_state_ = true;
this->state = state;
@@ -41,16 +17,5 @@ void Number::add_on_state_callback(std::function<void(float)> &&callback) {
this->state_callback_.add(std::move(callback));
}
std::string NumberTraits::get_unit_of_measurement() {
if (this->unit_of_measurement_.has_value())
return *this->unit_of_measurement_;
return "";
}
void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) {
this->unit_of_measurement_ = unit_of_measurement;
}
uint32_t Number::hash_base() { return 2282307003UL; }
} // namespace number
} // namespace esphome

View File

@@ -3,6 +3,8 @@
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "number_call.h"
#include "number_traits.h"
namespace esphome {
namespace number {
@@ -20,54 +22,6 @@ namespace number {
class Number;
class NumberCall {
public:
explicit NumberCall(Number *parent) : parent_(parent) {}
void perform();
NumberCall &set_value(float value) {
value_ = value;
return *this;
}
const optional<float> &get_value() const { return value_; }
protected:
Number *const parent_;
optional<float> value_;
};
enum NumberMode : uint8_t {
NUMBER_MODE_AUTO = 0,
NUMBER_MODE_BOX = 1,
NUMBER_MODE_SLIDER = 2,
};
class NumberTraits {
public:
void set_min_value(float min_value) { min_value_ = min_value; }
float get_min_value() const { return min_value_; }
void set_max_value(float max_value) { max_value_ = max_value; }
float get_max_value() const { return max_value_; }
void set_step(float step) { step_ = step; }
float get_step() const { return step_; }
/// Get the unit of measurement, using the manual override if set.
std::string get_unit_of_measurement();
/// Manually set the unit of measurement.
void set_unit_of_measurement(const std::string &unit_of_measurement);
// Get/set the frontend mode.
NumberMode get_mode() const { return this->mode_; }
void set_mode(NumberMode mode) { this->mode_ = mode; }
protected:
float min_value_ = NAN;
float max_value_ = NAN;
float step_ = NAN;
optional<std::string> unit_of_measurement_; ///< Unit of measurement override
NumberMode mode_{NUMBER_MODE_AUTO};
};
/** Base-class for all numbers.
*
* A number can use publish_state to send out a new value.
@@ -79,7 +33,6 @@ class Number : public EntityBase {
void publish_state(float state);
NumberCall make_call() { return NumberCall(this); }
void set(float value) { make_call().set_value(value).perform(); }
void add_on_state_callback(std::function<void(float)> &&callback);
@@ -99,8 +52,6 @@ class Number : public EntityBase {
*/
virtual void control(float value) = 0;
uint32_t hash_base() override;
CallbackManager<void(float)> state_callback_;
bool has_state_{false};
};

View File

@@ -0,0 +1,118 @@
#include "number_call.h"
#include "number.h"
#include "esphome/core/log.h"
namespace esphome {
namespace number {
static const char *const TAG = "number";
NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); }
NumberCall &NumberCall::number_increment(bool cycle) {
return this->with_operation(NUMBER_OP_INCREMENT).with_cycle(cycle);
}
NumberCall &NumberCall::number_decrement(bool cycle) {
return this->with_operation(NUMBER_OP_DECREMENT).with_cycle(cycle);
}
NumberCall &NumberCall::number_to_min() { return this->with_operation(NUMBER_OP_TO_MIN); }
NumberCall &NumberCall::number_to_max() { return this->with_operation(NUMBER_OP_TO_MAX); }
NumberCall &NumberCall::with_operation(NumberOperation operation) {
this->operation_ = operation;
return *this;
}
NumberCall &NumberCall::with_value(float value) {
this->value_ = value;
return *this;
}
NumberCall &NumberCall::with_cycle(bool cycle) {
this->cycle_ = cycle;
return *this;
}
void NumberCall::perform() {
auto *parent = this->parent_;
const auto *name = parent->get_name().c_str();
const auto &traits = parent->traits;
if (this->operation_ == NUMBER_OP_NONE) {
ESP_LOGW(TAG, "'%s' - NumberCall performed without selecting an operation", name);
return;
}
float target_value = NAN;
float min_value = traits.get_min_value();
float max_value = traits.get_max_value();
if (this->operation_ == NUMBER_OP_SET) {
ESP_LOGD(TAG, "'%s' - Setting number value", name);
if (!this->value_.has_value() || std::isnan(*this->value_)) {
ESP_LOGW(TAG, "'%s' - No value set for NumberCall", name);
return;
}
target_value = this->value_.value();
} else if (this->operation_ == NUMBER_OP_TO_MIN) {
if (std::isnan(min_value)) {
ESP_LOGW(TAG, "'%s' - Can't set to min value through NumberCall: no min_value defined", name);
} else {
target_value = min_value;
}
} else if (this->operation_ == NUMBER_OP_TO_MAX) {
if (std::isnan(max_value)) {
ESP_LOGW(TAG, "'%s' - Can't set to max value through NumberCall: no max_value defined", name);
} else {
target_value = max_value;
}
} else if (this->operation_ == NUMBER_OP_INCREMENT) {
ESP_LOGD(TAG, "'%s' - Increment number, with%s cycling", name, this->cycle_ ? "" : "out");
if (!parent->has_state()) {
ESP_LOGW(TAG, "'%s' - Can't increment number through NumberCall: no active state to modify", name);
return;
}
auto step = traits.get_step();
target_value = parent->state + (std::isnan(step) ? 1 : step);
if (target_value > max_value) {
if (this->cycle_ && !std::isnan(min_value)) {
target_value = min_value;
} else {
target_value = max_value;
}
}
} else if (this->operation_ == NUMBER_OP_DECREMENT) {
ESP_LOGD(TAG, "'%s' - Decrement number, with%s cycling", name, this->cycle_ ? "" : "out");
if (!parent->has_state()) {
ESP_LOGW(TAG, "'%s' - Can't decrement number through NumberCall: no active state to modify", name);
return;
}
auto step = traits.get_step();
target_value = parent->state - (std::isnan(step) ? 1 : step);
if (target_value < min_value) {
if (this->cycle_ && !std::isnan(max_value)) {
target_value = max_value;
} else {
target_value = min_value;
}
}
}
if (target_value < min_value) {
ESP_LOGW(TAG, "'%s' - Value %f must not be less than minimum %f", name, target_value, min_value);
return;
}
if (target_value > max_value) {
ESP_LOGW(TAG, "'%s' - Value %f must not be greater than maximum %f", name, target_value, max_value);
return;
}
ESP_LOGD(TAG, " New number value: %f", target_value);
this->parent_->control(target_value);
}
} // namespace number
} // namespace esphome

View File

@@ -0,0 +1,43 @@
#pragma once
#include "esphome/core/helpers.h"
#include "number_traits.h"
namespace esphome {
namespace number {
class Number;
enum NumberOperation {
NUMBER_OP_NONE,
NUMBER_OP_SET,
NUMBER_OP_INCREMENT,
NUMBER_OP_DECREMENT,
NUMBER_OP_TO_MIN,
NUMBER_OP_TO_MAX,
};
class NumberCall {
public:
explicit NumberCall(Number *parent) : parent_(parent) {}
void perform();
NumberCall &set_value(float value);
NumberCall &number_increment(bool cycle);
NumberCall &number_decrement(bool cycle);
NumberCall &number_to_min();
NumberCall &number_to_max();
NumberCall &with_operation(NumberOperation operation);
NumberCall &with_value(float value);
NumberCall &with_cycle(bool cycle);
protected:
Number *const parent_;
NumberOperation operation_{NUMBER_OP_NONE};
optional<float> value_;
bool cycle_;
};
} // namespace number
} // namespace esphome

View File

@@ -0,0 +1,20 @@
#include "esphome/core/log.h"
#include "number_traits.h"
namespace esphome {
namespace number {
static const char *const TAG = "number";
void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) {
this->unit_of_measurement_ = unit_of_measurement;
}
std::string NumberTraits::get_unit_of_measurement() {
if (this->unit_of_measurement_.has_value())
return *this->unit_of_measurement_;
return "";
}
} // namespace number
} // namespace esphome

View File

@@ -0,0 +1,44 @@
#pragma once
#include "esphome/core/helpers.h"
namespace esphome {
namespace number {
enum NumberMode : uint8_t {
NUMBER_MODE_AUTO = 0,
NUMBER_MODE_BOX = 1,
NUMBER_MODE_SLIDER = 2,
};
class NumberTraits {
public:
// Set/get the number value boundaries.
void set_min_value(float min_value) { min_value_ = min_value; }
float get_min_value() const { return min_value_; }
void set_max_value(float max_value) { max_value_ = max_value; }
float get_max_value() const { return max_value_; }
// Set/get the step size for incrementing or decrementing the number value.
void set_step(float step) { step_ = step; }
float get_step() const { return step_; }
/// Manually set the unit of measurement.
void set_unit_of_measurement(const std::string &unit_of_measurement);
/// Get the unit of measurement, using the manual override if set.
std::string get_unit_of_measurement();
// Set/get the frontend mode.
void set_mode(NumberMode mode) { this->mode_ = mode; }
NumberMode get_mode() const { return this->mode_; }
protected:
float min_value_ = NAN;
float max_value_ = NAN;
float step_ = NAN;
optional<std::string> unit_of_measurement_; ///< Unit of measurement override
NumberMode mode_{NUMBER_MODE_AUTO};
};
} // namespace number
} // namespace esphome

View File

@@ -52,6 +52,8 @@ class PCD8544 : public PollingComponent,
this->initialize();
}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;

View File

@@ -49,6 +49,47 @@ void PMSX003Component::set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sens
void PMSX003Component::loop() {
const uint32_t now = millis();
// If we update less often than it takes the device to stabilise, spin the fan down
// rather than running it constantly. It does take some time to stabilise, so we
// need to keep track of what state we're in.
if (this->update_interval_ > PMS_STABILISING_MS) {
if (this->initialised_ == 0) {
this->send_command_(PMS_CMD_AUTO_MANUAL, 0);
this->send_command_(PMS_CMD_ON_STANDBY, 1);
this->initialised_ = 1;
}
switch (this->state_) {
case PMSX003_STATE_IDLE:
// Power on the sensor now so it'll be ready when we hit the update time
if (now - this->last_update_ < (this->update_interval_ - PMS_STABILISING_MS))
return;
this->state_ = PMSX003_STATE_STABILISING;
this->send_command_(PMS_CMD_ON_STANDBY, 1);
this->fan_on_time_ = now;
return;
case PMSX003_STATE_STABILISING:
// wait for the sensor to be stable
if (now - this->fan_on_time_ < PMS_STABILISING_MS)
return;
// consume any command responses that are in the serial buffer
while (this->available())
this->read_byte(&this->data_[0]);
// Trigger a new read
this->send_command_(PMS_CMD_TRIG_MANUAL, 0);
this->state_ = PMSX003_STATE_WAITING;
break;
case PMSX003_STATE_WAITING:
// Just go ahead and read stuff
break;
}
} else if (now - this->last_update_ < this->update_interval_) {
// Otherwise just leave the sensor powered up and come back when we hit the update
// time
return;
}
if (now - this->last_transmission_ >= 500) {
// last transmission too long ago. Reset RX index.
this->data_index_ = 0;
@@ -65,6 +106,7 @@ void PMSX003Component::loop() {
// finished
this->parse_data_();
this->data_index_ = 0;
this->last_update_ = now;
} else if (!*check) {
// wrong data
this->data_index_ = 0;
@@ -131,6 +173,25 @@ optional<bool> PMSX003Component::check_byte_() {
return {};
}
void PMSX003Component::send_command_(uint8_t cmd, uint16_t data) {
this->data_index_ = 0;
this->data_[data_index_++] = 0x42;
this->data_[data_index_++] = 0x4D;
this->data_[data_index_++] = cmd;
this->data_[data_index_++] = (data >> 8) & 0xFF;
this->data_[data_index_++] = (data >> 0) & 0xFF;
int sum = 0;
for (int i = 0; i < data_index_; i++) {
sum += this->data_[i];
}
this->data_[data_index_++] = (sum >> 8) & 0xFF;
this->data_[data_index_++] = (sum >> 0) & 0xFF;
for (int i = 0; i < data_index_; i++) {
this->write_byte(this->data_[i]);
}
this->data_index_ = 0;
}
void PMSX003Component::parse_data_() {
switch (this->type_) {
case PMSX003_TYPE_5003ST: {
@@ -218,6 +279,13 @@ void PMSX003Component::parse_data_() {
}
}
// Spin down the sensor again if we aren't going to need it until more time has
// passed than it takes to stabilise
if (this->update_interval_ > PMS_STABILISING_MS) {
this->send_command_(PMS_CMD_ON_STANDBY, 0);
this->state_ = PMSX003_STATE_IDLE;
}
this->status_clear_warning();
}
uint16_t PMSX003Component::get_16_bit_uint_(uint8_t start_index) {

View File

@@ -7,6 +7,13 @@
namespace esphome {
namespace pmsx003 {
// known command bytes
#define PMS_CMD_AUTO_MANUAL 0xE1 // data=0: perform measurement manually, data=1: perform measurement automatically
#define PMS_CMD_TRIG_MANUAL 0xE2 // trigger a manual measurement
#define PMS_CMD_ON_STANDBY 0xE4 // data=0: go to standby mode, data=1: go to normal mode
static const uint16_t PMS_STABILISING_MS = 30000; // time taken for the sensor to become stable after power on
enum PMSX003Type {
PMSX003_TYPE_X003 = 0,
PMSX003_TYPE_5003T,
@@ -14,6 +21,12 @@ enum PMSX003Type {
PMSX003_TYPE_5003S,
};
enum PMSX003State {
PMSX003_STATE_IDLE = 0,
PMSX003_STATE_STABILISING,
PMSX003_STATE_WAITING,
};
class PMSX003Component : public uart::UARTDevice, public Component {
public:
PMSX003Component() = default;
@@ -23,6 +36,8 @@ class PMSX003Component : public uart::UARTDevice, public Component {
void set_type(PMSX003Type type) { type_ = type; }
void set_update_interval(uint32_t val) { update_interval_ = val; };
void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor);
void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor);
void set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor);
@@ -45,11 +60,17 @@ class PMSX003Component : public uart::UARTDevice, public Component {
protected:
optional<bool> check_byte_();
void parse_data_();
void send_command_(uint8_t cmd, uint16_t data);
uint16_t get_16_bit_uint_(uint8_t start_index);
uint8_t data_[64];
uint8_t data_index_{0};
uint8_t initialised_{0};
uint32_t fan_on_time_{0};
uint32_t last_update_{0};
uint32_t last_transmission_{0};
uint32_t update_interval_{0};
PMSX003State state_{PMSX003_STATE_IDLE};
PMSX003Type type_;
// "Standard Particle"

View File

@@ -1,6 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, uart
from esphome.const import (
CONF_FORMALDEHYDE,
CONF_HUMIDITY,
@@ -17,6 +18,7 @@ from esphome.const import (
CONF_PM_2_5UM,
CONF_PM_5_0UM,
CONF_PM_10_0UM,
CONF_UPDATE_INTERVAL,
CONF_TEMPERATURE,
CONF_TYPE,
DEVICE_CLASS_PM1,
@@ -44,6 +46,7 @@ TYPE_PMS5003ST = "PMS5003ST"
TYPE_PMS5003S = "PMS5003S"
PMSX003Type = pmsx003_ns.enum("PMSX003Type")
PMSX003_TYPES = {
TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003,
TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T,
@@ -68,6 +71,17 @@ def validate_pmsx003_sensors(value):
return value
def validate_update_interval(value):
value = cv.positive_time_period_milliseconds(value)
if value == cv.time_period("0s"):
return value
if value < cv.time_period("30s"):
raise cv.Invalid(
"Update interval must be greater than or equal to 30 seconds if set."
)
return value
CONFIG_SCHEMA = (
cv.Schema(
{
@@ -157,6 +171,7 @@ CONFIG_SCHEMA = (
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_UPDATE_INTERVAL, default="0s"): validate_update_interval,
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -164,6 +179,17 @@ CONFIG_SCHEMA = (
)
def final_validate(config):
require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s")
schema = uart.final_validate_device_schema(
"pmsx003", baud_rate=9600, require_rx=True, require_tx=require_tx
)
schema(config)
FINAL_VALIDATE_SCHEMA = final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -230,3 +256,5 @@ async def to_code(config):
if CONF_FORMALDEHYDE in config:
sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE])
cg.add(var.set_formaldehyde_sensor(sens))
cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))

View File

@@ -1,6 +1,9 @@
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_ID
from esphome.const import (
CONF_ID,
CONF_INCLUDE_INTERNAL,
)
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
from esphome.components import web_server_base
@@ -15,6 +18,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(
web_server_base.WebServerBase
),
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
},
cv.only_with_arduino,
).extend(cv.COMPONENT_SCHEMA)
@@ -27,3 +31,5 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID], paren)
await cg.register_component(var, config)
cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL]))

View File

@@ -61,7 +61,7 @@ void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_sensor_failed GAUGE\n"));
}
void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
if (!std::isnan(obj->state)) {
// We have a valid value, output this value
@@ -98,7 +98,7 @@ void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_binary_sensor_failed GAUGE\n"));
}
void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
if (obj->has_state()) {
// We have a valid value, output this value
@@ -134,7 +134,7 @@ void PrometheusHandler::fan_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_fan_oscillation GAUGE\n"));
}
void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
stream->print(F("esphome_fan_failed{id=\""));
stream->print(obj->get_object_id().c_str());
@@ -179,7 +179,7 @@ void PrometheusHandler::light_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_light_effect_active GAUGE\n"));
}
void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
// State
stream->print(F("esphome_light_state{id=\""));
@@ -255,7 +255,7 @@ void PrometheusHandler::cover_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_cover_failed GAUGE\n"));
}
void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
if (!std::isnan(obj->position)) {
// We have a valid value, output this value
@@ -298,7 +298,7 @@ void PrometheusHandler::switch_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_switch_failed GAUGE\n"));
}
void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
stream->print(F("esphome_switch_failed{id=\""));
stream->print(obj->get_object_id().c_str());
@@ -322,7 +322,7 @@ void PrometheusHandler::lock_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_lock_failed GAUGE\n"));
}
void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
stream->print(F("esphome_lock_failed{id=\""));
stream->print(obj->get_object_id().c_str());

View File

@@ -13,6 +13,13 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
public:
PrometheusHandler(web_server_base::WebServerBase *base) : base_(base) {}
/** Determine whether internal components should be exported as metrics.
* Defaults to false.
*
* @param include_internal Whether internal components should be exported.
*/
void set_include_internal(bool include_internal) { include_internal_ = include_internal; }
bool canHandle(AsyncWebServerRequest *request) override {
if (request->method() == HTTP_GET) {
if (request->url() == "/metrics")
@@ -84,6 +91,7 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
#endif
web_server_base::WebServerBase *base_;
bool include_internal_{false};
};
} // namespace prometheus

View File

@@ -728,6 +728,48 @@ async def rc5_action(var, config, args):
cg.add(var.set_command(template_))
# RC6
RC6Data, RC6BinarySensor, RC6Trigger, RC6Action, RC6Dumper = declare_protocol("RC6")
RC6_SCHEMA = cv.Schema(
{
cv.Required(CONF_ADDRESS): cv.hex_uint8_t,
cv.Required(CONF_COMMAND): cv.hex_uint8_t,
}
)
@register_binary_sensor("rc6", RC6BinarySensor, RC6_SCHEMA)
def rc6_binary_sensor(var, config):
cg.add(
var.set_data(
cg.StructInitializer(
RC6Data,
("device", config[CONF_DEVICE]),
("address", config[CONF_ADDRESS]),
("command", config[CONF_COMMAND]),
)
)
)
@register_trigger("rc6", RC6Trigger, RC6Data)
def rc6_trigger(var, config):
pass
@register_dumper("rc6", RC6Dumper)
def rc6_dumper(var, config):
pass
@register_action("rc6", RC6Action, RC6_SCHEMA)
async def rc6_action(var, config, args):
template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
cg.add(var.set_address(template_))
template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8)
cg.add(var.set_command(template_))
# RC Switch Raw
RC_SWITCH_TIMING_SCHEMA = cv.All([cv.uint8_t], cv.Length(min=2, max=2))

View File

@@ -0,0 +1,181 @@
#include "rc6_protocol.h"
#include "esphome/core/log.h"
namespace esphome {
namespace remote_base {
static const char *const RC6_TAG = "remote.rc6";
static const uint16_t RC6_FREQ = 36000;
static const uint16_t RC6_UNIT = 444;
static const uint16_t RC6_HEADER_MARK = (6 * RC6_UNIT);
static const uint16_t RC6_HEADER_SPACE = (2 * RC6_UNIT);
static const uint16_t RC6_MODE_MASK = 0x07;
void RC6Protocol::encode(RemoteTransmitData *dst, const RC6Data &data) {
dst->reserve(44);
dst->set_carrier_frequency(RC6_FREQ);
// Encode header
dst->item(RC6_HEADER_MARK, RC6_HEADER_SPACE);
int32_t next{0};
// Encode startbit+mode
uint8_t header{static_cast<uint8_t>((1 << 3) | data.mode)};
for (uint8_t mask = 0x8; mask; mask >>= 1) {
if (header & mask) {
if (next < 0) {
dst->space(-next);
next = 0;
}
if (next >= 0) {
next = next + RC6_UNIT;
dst->mark(next);
next = -RC6_UNIT;
}
} else {
if (next > 0) {
dst->mark(next);
next = 0;
}
if (next <= 0) {
next = next - RC6_UNIT;
dst->space(-next);
next = RC6_UNIT;
}
}
}
// Toggle
if (data.toggle) {
if (next < 0) {
dst->space(-next);
next = 0;
}
if (next >= 0) {
next = next + RC6_UNIT * 2;
dst->mark(next);
next = -RC6_UNIT * 2;
}
} else {
if (next > 0) {
dst->mark(next);
next = 0;
}
if (next <= 0) {
next = next - RC6_UNIT * 2;
dst->space(-next);
next = RC6_UNIT * 2;
}
}
// Encode data
uint16_t raw{static_cast<uint16_t>((data.address << 8) | data.command)};
for (uint16_t mask = 0x8000; mask; mask >>= 1) {
if (raw & mask) {
if (next < 0) {
dst->space(-next);
next = 0;
}
if (next >= 0) {
next = next + RC6_UNIT;
dst->mark(next);
next = -RC6_UNIT;
}
} else {
if (next > 0) {
dst->mark(next);
next = 0;
}
if (next <= 0) {
next = next - RC6_UNIT;
dst->space(-next);
next = RC6_UNIT;
}
}
}
if (next > 0) {
dst->mark(next);
} else {
dst->space(-next);
}
}
optional<RC6Data> RC6Protocol::decode(RemoteReceiveData src) {
RC6Data data{
.mode = 0,
.toggle = 0,
.address = 0,
.command = 0,
};
// Check if header matches
if (!src.expect_item(RC6_HEADER_MARK, RC6_HEADER_SPACE)) {
return {};
}
uint8_t bit{1};
uint8_t offset{0};
uint8_t header{0};
uint32_t buffer{0};
// Startbit + mode
while (offset < 4) {
bit = src.peek() > 0;
header = header + (bit << (3 - offset++));
src.advance();
if (src.peek_mark(RC6_UNIT) || src.peek_space(RC6_UNIT)) {
src.advance();
} else if (offset == 4) {
break;
} else if (!src.peek_mark(RC6_UNIT * 2) && !src.peek_space(RC6_UNIT * 2)) {
return {};
}
}
data.mode = header & RC6_MODE_MASK;
if (data.mode != 0) {
return {}; // I dont have a device to test other modes
}
// Toggle
data.toggle = src.peek() > 0;
src.advance();
if (src.peek_mark(RC6_UNIT * 2) || src.peek_space(RC6_UNIT * 2)) {
src.advance();
}
// Data
offset = 0;
while (offset < 16) {
bit = src.peek() > 0;
buffer = buffer + (bit << (15 - offset++));
src.advance();
if (offset == 16) {
break;
} else if (src.peek_mark(RC6_UNIT) || src.peek_space(RC6_UNIT)) {
src.advance();
} else if (!src.peek_mark(RC6_UNIT * 2) && !src.peek_space(RC6_UNIT * 2)) {
return {};
}
}
data.address = (0xFF00 & buffer) >> 8;
data.command = (0x00FF & buffer);
return data;
}
void RC6Protocol::dump(const RC6Data &data) {
ESP_LOGD(RC6_TAG, "Received RC6: mode=0x%X, address=0x%02X, command=0x%02X, toggle=0x%X", data.mode, data.address,
data.command, data.toggle);
}
} // namespace remote_base
} // namespace esphome

View File

@@ -0,0 +1,46 @@
#pragma once
#include "remote_base.h"
namespace esphome {
namespace remote_base {
struct RC6Data {
uint8_t mode : 3;
uint8_t toggle : 1;
uint8_t address;
uint8_t command;
bool operator==(const RC6Data &rhs) const { return address == rhs.address && command == rhs.command; }
};
class RC6Protocol : public RemoteProtocol<RC6Data> {
public:
void encode(RemoteTransmitData *dst, const RC6Data &data) override;
optional<RC6Data> decode(RemoteReceiveData src) override;
void dump(const RC6Data &data) override;
};
DECLARE_REMOTE_PROTOCOL(RC6)
template<typename... Ts> class RC6Action : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(uint8_t, address)
TEMPLATABLE_VALUE(uint8_t, command)
void encode(RemoteTransmitData *dst, Ts... x) {
RC6Data data{};
data.mode = 0;
data.toggle = this->toggle_;
data.address = this->address_.value(x...);
data.command = this->command_.value(x...);
RC6Protocol().encode(dst, data);
this->toggle_ = !this->toggle_;
}
protected:
uint8_t toggle_{0};
};
} // namespace remote_base
} // namespace esphome

View File

@@ -103,7 +103,7 @@ void IRAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensorStore
rotation_dir = -1;
}
if (rotation_dir != 0) {
if (rotation_dir != 0 && !arg->first_read) {
auto *first_zero = std::find(arg->rotation_events.begin(), arg->rotation_events.end(), 0); // find first zero
if (first_zero == arg->rotation_events.begin() // are we at the start (first event this loop iteration)
|| std::signbit(*std::prev(first_zero)) !=
@@ -119,6 +119,7 @@ void IRAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensorStore
*std::prev(first_zero) += rotation_dir; // store the rotation into the previous slot
}
}
arg->first_read = false;
arg->state = new_state;
}

View File

@@ -34,6 +34,7 @@ struct RotaryEncoderSensorStore {
int32_t max_value{INT32_MAX};
int32_t last_read{0};
uint8_t state{0};
bool first_read{true};
std::array<int8_t, 8> rotation_events{};
bool rotation_events_overflow{false};

View File

@@ -0,0 +1,28 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "scd4x.h"
namespace esphome {
namespace scd4x {
template<typename... Ts> class PerformForcedCalibrationAction : public Action<Ts...>, public Parented<SCD4XComponent> {
public:
void play(Ts... x) override {
if (this->value_.has_value()) {
this->parent_->perform_forced_calibration(this->value_.value(x...));
}
}
protected:
TEMPLATABLE_VALUE(uint16_t, value)
};
template<typename... Ts> class FactoryResetAction : public Action<Ts...>, public Parented<SCD4XComponent> {
public:
void play(Ts... x) override { this->parent_->factory_reset(); }
};
} // namespace scd4x
} // namespace esphome

View File

@@ -13,39 +13,32 @@ static const uint16_t SCD4X_CMD_ALTITUDE_COMPENSATION = 0x2427;
static const uint16_t SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION = 0xe000;
static const uint16_t SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION = 0x2416;
static const uint16_t SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS = 0x21b1;
static const uint16_t SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS = 0x21ac;
static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT = 0x219d; // SCD41 only
static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY = 0x2196;
static const uint16_t SCD4X_CMD_GET_DATA_READY_STATUS = 0xe4b8;
static const uint16_t SCD4X_CMD_READ_MEASUREMENT = 0xec05;
static const uint16_t SCD4X_CMD_PERFORM_FORCED_CALIBRATION = 0x362f;
static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86;
static const uint16_t SCD4X_CMD_FACTORY_RESET = 0x3632;
static const uint16_t SCD4X_CMD_GET_FEATURESET = 0x202f;
static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f;
static const uint16_t SCD41_ID = 0x1408;
static const uint16_t SCD40_ID = 0x440;
void SCD4XComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up scd4x...");
// the sensor needs 1000 ms to enter the idle state
this->set_timeout(1000, [this]() {
uint16_t raw_read_status;
if (!this->get_register(SCD4X_CMD_GET_DATA_READY_STATUS, raw_read_status)) {
ESP_LOGE(TAG, "Failed to read data ready status");
this->status_clear_error();
if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
ESP_LOGE(TAG, "Failed to stop measurements");
this->mark_failed();
return;
}
uint32_t stop_measurement_delay = 0;
// In order to query the device periodic measurement must be ceased
if (raw_read_status) {
ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement");
if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
ESP_LOGE(TAG, "Failed to stop measurements");
this->mark_failed();
return;
}
// According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after
// issuing the stop_periodic_measurement command
stop_measurement_delay = 500;
}
this->set_timeout(stop_measurement_delay, [this]() {
// According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after
// issuing the stop_periodic_measurement command
this->set_timeout(500, [this]() {
uint16_t raw_serial_number[3];
if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) {
ESP_LOGE(TAG, "Failed to read serial number");
@@ -89,15 +82,9 @@ void SCD4XComponent::setup() {
return;
}
// Finally start sensor measurements
if (!this->write_command(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) {
ESP_LOGE(TAG, "Error starting continuous measurements.");
this->error_code_ = MEASUREMENT_INIT_FAILED;
this->mark_failed();
return;
}
initialized_ = true;
// Finally start sensor measurements
this->start_measurement_();
ESP_LOGD(TAG, "Sensor initialized");
});
});
@@ -123,12 +110,31 @@ void SCD4XComponent::dump_config() {
}
}
ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_));
if (this->ambient_pressure_compensation_) {
ESP_LOGCONFIG(TAG, " Altitude compensation disabled");
ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_);
if (this->ambient_pressure_source_ != nullptr) {
ESP_LOGCONFIG(TAG, " Dynamic ambient pressure compensation using sensor '%s'",
this->ambient_pressure_source_->get_name().c_str());
} else {
ESP_LOGCONFIG(TAG, " Ambient pressure compensation disabled");
ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_);
if (this->ambient_pressure_compensation_) {
ESP_LOGCONFIG(TAG, " Altitude compensation disabled");
ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_);
} else {
ESP_LOGCONFIG(TAG, " Ambient pressure compensation disabled");
ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_);
}
}
switch (this->measurement_mode_) {
case PERIODIC:
ESP_LOGCONFIG(TAG, " Measurement mode: periodic (5s)");
break;
case LOW_POWER_PERIODIC:
ESP_LOGCONFIG(TAG, " Measurement mode: low power periodic (30s)");
break;
case SINGLE_SHOT:
ESP_LOGCONFIG(TAG, " Measurement mode: single shot");
break;
case SINGLE_SHOT_RHT_ONLY:
ESP_LOGCONFIG(TAG, " Measurement mode: single shot rht only");
break;
}
ESP_LOGCONFIG(TAG, " Temperature offset: %.2f °C", this->temperature_offset_);
LOG_UPDATE_INTERVAL(this);
@@ -149,47 +155,105 @@ void SCD4XComponent::update() {
}
}
// Check if data is ready
if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) {
this->status_set_warning();
return;
uint32_t wait_time = 0;
if (this->measurement_mode_ == SINGLE_SHOT || this->measurement_mode_ == SINGLE_SHOT_RHT_ONLY) {
start_measurement_();
wait_time =
this->measurement_mode_ == SINGLE_SHOT ? 5000 : 50; // Single shot measurement takes 5 secs rht mode 50 ms
}
this->set_timeout(wait_time, [this]() {
// Check if data is ready
if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) {
this->status_set_warning();
return;
}
uint16_t raw_read_status;
if (!this->read_data(raw_read_status) || raw_read_status == 0x00) {
this->status_set_warning();
ESP_LOGW(TAG, "Data not ready yet!");
return;
}
uint16_t raw_read_status;
if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) {
ESP_LOGW(TAG, "Error reading measurement!");
this->status_set_warning();
return;
}
if (!this->read_data(raw_read_status) || raw_read_status == 0x00) {
this->status_set_warning();
ESP_LOGW(TAG, "Data not ready yet!");
return;
}
// Read off sensor data
uint16_t raw_data[3];
if (!this->read_data(raw_data, 3)) {
this->status_set_warning();
return;
}
if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) {
ESP_LOGW(TAG, "Error reading measurement!");
this->status_set_warning();
return; // NO RETRY
}
// Read off sensor data
uint16_t raw_data[3];
if (!this->read_data(raw_data, 3)) {
this->status_set_warning();
return;
}
if (this->co2_sensor_ != nullptr)
this->co2_sensor_->publish_state(raw_data[0]);
if (this->co2_sensor_ != nullptr)
this->co2_sensor_->publish_state(raw_data[0]);
if (this->temperature_sensor_ != nullptr) {
const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16);
this->temperature_sensor_->publish_state(temperature);
}
if (this->humidity_sensor_ != nullptr) {
const float humidity = (100.0f * raw_data[2]) / (1 << 16);
this->humidity_sensor_->publish_state(humidity);
}
this->status_clear_warning();
if (this->temperature_sensor_ != nullptr) {
const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16);
this->temperature_sensor_->publish_state(temperature);
}
if (this->humidity_sensor_ != nullptr) {
const float humidity = (100.0f * raw_data[2]) / (1 << 16);
this->humidity_sensor_->publish_state(humidity);
}
this->status_clear_warning();
}); // set_timeout
}
bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentration) {
/*
Operate the SCD4x in the operation mode later used in normal sensor operation (periodic measurement, low power
periodic measurement or single shot) for > 3 minutes in an environment with homogenous and constant CO2
concentration before performing a forced recalibration.
*/
if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
ESP_LOGE(TAG, "Failed to stop measurements");
this->status_set_warning();
}
this->set_timeout(500, [this, current_co2_concentration]() {
if (this->write_command(SCD4X_CMD_PERFORM_FORCED_CALIBRATION, current_co2_concentration)) {
ESP_LOGD(TAG, "setting forced calibration Co2 level %d ppm", current_co2_concentration);
// frc takes 400 ms
// because this method will be used very rarly
// the simple aproach with delay is ok
delay(400); // NOLINT'
if (!this->start_measurement_()) {
return false;
} else {
ESP_LOGD(TAG, "forced calibration complete");
}
return true;
} else {
ESP_LOGE(TAG, "force calibration failed");
this->error_code_ = FRC_FAILED;
this->status_set_warning();
return false;
}
});
return true;
}
bool SCD4XComponent::factory_reset() {
if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
ESP_LOGE(TAG, "Failed to stop measurements");
this->status_set_warning();
return false;
}
this->set_timeout(500, [this]() {
if (!this->write_command(SCD4X_CMD_FACTORY_RESET)) {
ESP_LOGE(TAG, "Failed to send factory reset command");
this->status_set_warning();
return false;
}
ESP_LOGD(TAG, "Factory reset complete");
return true;
});
return true;
}
// Note pressure in bar here. Convert to hPa
void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) {
ambient_pressure_compensation_ = true;
@@ -213,5 +277,38 @@ bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_
}
}
bool SCD4XComponent::start_measurement_() {
uint16_t measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS;
switch (this->measurement_mode_) {
case PERIODIC:
measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS;
break;
case LOW_POWER_PERIODIC:
measurement_command = SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS;
break;
case SINGLE_SHOT:
measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT;
break;
case SINGLE_SHOT_RHT_ONLY:
measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY;
break;
}
static uint8_t remaining_retries = 3;
while (remaining_retries) {
if (!this->write_command(measurement_command)) {
ESP_LOGE(TAG, "Error starting measurements.");
this->error_code_ = MEASUREMENT_INIT_FAILED;
this->status_set_warning();
if (--remaining_retries == 0)
return false;
delay(50); // NOLINT wait 50 ms and try again
}
this->status_clear_warning();
return true;
}
return false;
}
} // namespace scd4x
} // namespace esphome

View File

@@ -1,5 +1,6 @@
#pragma once
#include <vector>
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensirion_common/i2c_sensirion.h"
@@ -7,7 +8,14 @@
namespace esphome {
namespace scd4x {
enum ERRORCODE { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN };
enum ERRORCODE {
COMMUNICATION_FAILED,
SERIAL_NUMBER_IDENTIFICATION_FAILED,
MEASUREMENT_INIT_FAILED,
FRC_FAILED,
UNKNOWN
};
enum MeasurementMode { PERIODIC, LOW_POWER_PERIODIC, SINGLE_SHOT, SINGLE_SHOT_RHT_ONLY };
class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
public:
@@ -25,10 +33,13 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri
void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; }
void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; };
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_measurement_mode(MeasurementMode mode) { measurement_mode_ = mode; }
bool perform_forced_calibration(uint16_t current_co2_concentration);
bool factory_reset();
protected:
bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa);
bool start_measurement_();
ERRORCODE error_code_;
bool initialized_{false};
@@ -38,7 +49,7 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri
bool ambient_pressure_compensation_;
uint16_t ambient_pressure_;
bool enable_asc_;
MeasurementMode measurement_mode_{PERIODIC};
sensor::Sensor *co2_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};

View File

@@ -2,11 +2,15 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.components import sensirion_common
from esphome import automation
from esphome.automation import maybe_simple_id
from esphome.const import (
CONF_ID,
CONF_CO2,
CONF_HUMIDITY,
CONF_TEMPERATURE,
CONF_VALUE,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
@@ -19,7 +23,7 @@ from esphome.const import (
UNIT_PERCENT,
)
CODEOWNERS = ["@sjtrny"]
CODEOWNERS = ["@sjtrny", "@martgras"]
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
@@ -27,12 +31,29 @@ scd4x_ns = cg.esphome_ns.namespace("scd4x")
SCD4XComponent = scd4x_ns.class_(
"SCD4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
)
MeasurementMode = scd4x_ns.enum("MEASUREMENT_MODE")
MEASUREMENT_MODE_OPTIONS = {
"periodic": MeasurementMode.PERIODIC,
"low_power_periodic": MeasurementMode.LOW_POWER_PERIODIC,
"single_shot": MeasurementMode.SINGLE_SHOT,
"single_shot_rht_only": MeasurementMode.SINGLE_SHOT_RHT_ONLY,
}
# Actions
PerformForcedCalibrationAction = scd4x_ns.class_(
"PerformForcedCalibrationAction", automation.Action
)
FactoryResetAction = scd4x_ns.class_("FactoryResetAction", automation.Action)
CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration"
CONF_ALTITUDE_COMPENSATION = "altitude_compensation"
CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation"
CONF_TEMPERATURE_OFFSET = "temperature_offset"
CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source"
CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration"
CONF_MEASUREMENT_MODE = "measurement_mode"
CONF_TEMPERATURE_OFFSET = "temperature_offset"
CONFIG_SCHEMA = (
cv.Schema(
@@ -69,6 +90,9 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id(
sensor.Sensor
),
cv.Optional(CONF_MEASUREMENT_MODE, default="periodic"): cv.enum(
MEASUREMENT_MODE_OPTIONS, lower=True
),
}
)
.extend(cv.polling_component_schema("60s"))
@@ -106,3 +130,42 @@ async def to_code(config):
if CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE in config:
sens = await cg.get_variable(config[CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE])
cg.add(var.set_ambient_pressure_source(sens))
cg.add(var.set_measurement_mode(config[CONF_MEASUREMENT_MODE]))
SCD4X_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(SCD4XComponent),
cv.Required(CONF_VALUE): cv.templatable(cv.positive_int),
}
)
@automation.register_action(
"scd4x.perform_forced_calibration",
PerformForcedCalibrationAction,
SCD4X_ACTION_SCHEMA,
)
async def scd4x_frc_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint16)
cg.add(var.set_value(template_))
return var
SCD4X_RESET_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(SCD4XComponent),
}
)
@automation.register_action(
"scd4x.factory_reset", FactoryResetAction, SCD4X_RESET_ACTION_SCHEMA
)
async def scd4x_reset_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var

View File

@@ -40,7 +40,7 @@ void SDP3XComponent::setup() {
}
uint16_t data[6];
if (this->read_data(data, 6) != i2c::ERROR_OK) {
if (!this->read_data(data, 6)) {
ESP_LOGE(TAG, "Read ID SDP3X failed!");
this->mark_failed();
return;
@@ -78,8 +78,7 @@ void SDP3XComponent::setup() {
}
}
if (this->write_command(measurement_mode_ == DP_AVG ? SDP3X_START_DP_AVG : SDP3X_START_MASS_FLOW_AVG) !=
i2c::ERROR_OK) {
if (!this->write_command(measurement_mode_ == DP_AVG ? SDP3X_START_DP_AVG : SDP3X_START_MASS_FLOW_AVG)) {
ESP_LOGE(TAG, "Start Measurements SDP3X failed!");
this->mark_failed();
return;
@@ -98,7 +97,7 @@ void SDP3XComponent::dump_config() {
void SDP3XComponent::read_pressure_() {
uint16_t data[3];
if (this->read_data(data, 3) != i2c::ERROR_OK) {
if (!this->read_data(data, 3)) {
ESP_LOGW(TAG, "Couldn't read SDP3X data!");
this->status_set_warning();
return;

View File

@@ -9,6 +9,10 @@ from esphome.const import (
CONF_OPTION,
CONF_TRIGGER_ID,
CONF_MQTT_ID,
CONF_CYCLE,
CONF_MODE,
CONF_OPERATION,
CONF_INDEX,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_helpers import setup_entity
@@ -22,14 +26,27 @@ SelectPtr = Select.operator("ptr")
# Triggers
SelectStateTrigger = select_ns.class_(
"SelectStateTrigger", automation.Trigger.template(cg.float_)
"SelectStateTrigger",
automation.Trigger.template(cg.std_string, cg.size_t),
)
# Actions
SelectSetAction = select_ns.class_("SelectSetAction", automation.Action)
SelectSetIndexAction = select_ns.class_("SelectSetIndexAction", automation.Action)
SelectOperationAction = select_ns.class_("SelectOperationAction", automation.Action)
# Enums
SelectOperation = select_ns.enum("SelectOperation")
SELECT_OPERATION_OPTIONS = {
"NEXT": SelectOperation.SELECT_OP_NEXT,
"PREVIOUS": SelectOperation.SELECT_OP_PREVIOUS,
"FIRST": SelectOperation.SELECT_OP_FIRST,
"LAST": SelectOperation.SELECT_OP_LAST,
}
icon = cv.icon
SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent),
@@ -50,7 +67,9 @@ async def setup_select_core_(var, config, *, options: List[str]):
for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
await automation.build_automation(
trigger, [(cg.std_string, "x"), (cg.size_t, "i")], conf
)
if CONF_MQTT_ID in config:
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
@@ -76,12 +95,18 @@ async def to_code(config):
cg.add_global(select_ns.using)
OPERATION_BASE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(Select),
}
)
@automation.register_action(
"select.set",
SelectSetAction,
cv.Schema(
OPERATION_BASE_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.use_id(Select),
cv.Required(CONF_OPTION): cv.templatable(cv.string_strict),
}
),
@@ -92,3 +117,96 @@ async def select_set_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(config[CONF_OPTION], args, cg.std_string)
cg.add(var.set_option(template_))
return var
@automation.register_action(
"select.set_index",
SelectSetIndexAction,
OPERATION_BASE_SCHEMA.extend(
{
cv.Required(CONF_INDEX): cv.templatable(cv.positive_int),
}
),
)
async def select_set_index_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_INDEX], args, cg.size_t)
cg.add(var.set_index(template_))
return var
@automation.register_action(
"select.operation",
SelectOperationAction,
OPERATION_BASE_SCHEMA.extend(
{
cv.Required(CONF_OPERATION): cv.templatable(
cv.enum(SELECT_OPERATION_OPTIONS, upper=True)
),
cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean),
}
),
)
@automation.register_action(
"select.next",
SelectOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="NEXT"): cv.one_of("NEXT", upper=True),
cv.Optional(CONF_CYCLE, default=True): cv.boolean,
}
)
),
)
@automation.register_action(
"select.previous",
SelectOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="PREVIOUS"): cv.one_of(
"PREVIOUS", upper=True
),
cv.Optional(CONF_CYCLE, default=True): cv.boolean,
}
)
),
)
@automation.register_action(
"select.first",
SelectOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="FIRST"): cv.one_of("FIRST", upper=True),
}
)
),
)
@automation.register_action(
"select.last",
SelectOperationAction,
automation.maybe_simple_id(
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_MODE, default="LAST"): cv.one_of("LAST", upper=True),
}
)
),
)
async def select_operation_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_OPERATION in config:
op_ = await cg.templatable(config[CONF_OPERATION], args, SelectOperation)
cg.add(var.set_operation(op_))
if CONF_CYCLE in config:
cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool)
cg.add(var.set_cycle(cycle_))
if CONF_MODE in config:
cg.add(var.set_operation(SELECT_OPERATION_OPTIONS[config[CONF_MODE]]))
if CONF_CYCLE in config:
cg.add(var.set_cycle(config[CONF_CYCLE]))
return var

View File

@@ -7,16 +7,16 @@
namespace esphome {
namespace select {
class SelectStateTrigger : public Trigger<std::string> {
class SelectStateTrigger : public Trigger<std::string, size_t> {
public:
explicit SelectStateTrigger(Select *parent) {
parent->add_on_state_callback([this](const std::string &value) { this->trigger(value); });
parent->add_on_state_callback([this](const std::string &value, size_t index) { this->trigger(value, index); });
}
};
template<typename... Ts> class SelectSetAction : public Action<Ts...> {
public:
SelectSetAction(Select *select) : select_(select) {}
explicit SelectSetAction(Select *select) : select_(select) {}
TEMPLATABLE_VALUE(std::string, option)
void play(Ts... x) override {
@@ -29,5 +29,39 @@ template<typename... Ts> class SelectSetAction : public Action<Ts...> {
Select *select_;
};
template<typename... Ts> class SelectSetIndexAction : public Action<Ts...> {
public:
explicit SelectSetIndexAction(Select *select) : select_(select) {}
TEMPLATABLE_VALUE(size_t, index)
void play(Ts... x) override {
auto call = this->select_->make_call();
call.set_index(this->index_.value(x...));
call.perform();
}
protected:
Select *select_;
};
template<typename... Ts> class SelectOperationAction : public Action<Ts...> {
public:
explicit SelectOperationAction(Select *select) : select_(select) {}
TEMPLATABLE_VALUE(bool, cycle)
TEMPLATABLE_VALUE(SelectOperation, operation)
void play(Ts... x) override {
auto call = this->select_->make_call();
call.with_operation(this->operation_.value(x...));
if (this->cycle_.has_value()) {
call.with_cycle(this->cycle_.value(x...));
}
call.perform();
}
protected:
Select *select_;
};
} // namespace select
} // namespace esphome

View File

@@ -6,38 +6,57 @@ namespace select {
static const char *const TAG = "select";
void SelectCall::perform() {
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
if (!this->option_.has_value()) {
ESP_LOGW(TAG, "No value set for SelectCall");
return;
}
const auto &traits = this->parent_->traits;
auto value = *this->option_;
auto options = traits.get_options();
if (std::find(options.begin(), options.end(), value) == options.end()) {
ESP_LOGW(TAG, " Option %s is not a valid option.", value.c_str());
return;
}
ESP_LOGD(TAG, " Option: %s", (*this->option_).c_str());
this->parent_->control(*this->option_);
}
void Select::publish_state(const std::string &state) {
this->has_state_ = true;
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str());
this->state_callback_.call(state);
auto index = this->index_of(state);
const auto *name = this->get_name().c_str();
if (index.has_value()) {
this->has_state_ = true;
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %s (index %d)", name, state.c_str(), index.value());
this->state_callback_.call(state, index.value());
} else {
ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str());
}
}
void Select::add_on_state_callback(std::function<void(std::string)> &&callback) {
void Select::add_on_state_callback(std::function<void(std::string, size_t)> &&callback) {
this->state_callback_.add(std::move(callback));
}
uint32_t Select::hash_base() { return 2812997003UL; }
bool Select::has_option(const std::string &option) const { return this->index_of(option).has_value(); }
bool Select::has_index(size_t index) const { return index < this->size(); }
size_t Select::size() const {
auto options = traits.get_options();
return options.size();
}
optional<size_t> Select::index_of(const std::string &option) const {
auto options = traits.get_options();
auto it = std::find(options.begin(), options.end(), option);
if (it == options.end()) {
return {};
}
return std::distance(options.begin(), it);
}
optional<size_t> Select::active_index() const {
if (this->has_state()) {
return this->index_of(this->state);
} else {
return {};
}
}
optional<std::string> Select::at(size_t index) const {
if (this->has_index(index)) {
auto options = traits.get_options();
return options.at(index);
} else {
return {};
}
}
} // namespace select
} // namespace esphome

View File

@@ -1,10 +1,10 @@
#pragma once
#include <set>
#include <utility>
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "select_call.h"
#include "select_traits.h"
namespace esphome {
namespace select {
@@ -17,33 +17,6 @@ namespace select {
} \
}
class Select;
class SelectCall {
public:
explicit SelectCall(Select *parent) : parent_(parent) {}
void perform();
SelectCall &set_option(const std::string &option) {
option_ = option;
return *this;
}
const optional<std::string> &get_option() const { return option_; }
protected:
Select *const parent_;
optional<std::string> option_;
};
class SelectTraits {
public:
void set_options(std::vector<std::string> options) { this->options_ = std::move(options); }
std::vector<std::string> get_options() const { return this->options_; }
protected:
std::vector<std::string> options_;
};
/** Base-class for all selects.
*
* A select can use publish_state to send out a new value.
@@ -51,19 +24,36 @@ class SelectTraits {
class Select : public EntityBase {
public:
std::string state;
SelectTraits traits;
void publish_state(const std::string &state);
SelectCall make_call() { return SelectCall(this); }
void set(const std::string &value) { make_call().set_option(value).perform(); }
void add_on_state_callback(std::function<void(std::string)> &&callback);
SelectTraits traits;
/// Return whether this select has gotten a full state yet.
/// Return whether this select component has gotten a full state yet.
bool has_state() const { return has_state_; }
/// Instantiate a SelectCall object to modify this select component's state.
SelectCall make_call() { return SelectCall(this); }
/// Return whether this select component contains the provided option.
bool has_option(const std::string &option) const;
/// Return whether this select component contains the provided index offset.
bool has_index(size_t index) const;
/// Return the number of options in this select component.
size_t size() const;
/// Find the (optional) index offset of the provided option value.
optional<size_t> index_of(const std::string &option) const;
/// Return the (optional) index offset of the currently active option.
optional<size_t> active_index() const;
/// Return the (optional) option value at the provided index offset.
optional<std::string> at(size_t index) const;
void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
protected:
friend class SelectCall;
@@ -75,9 +65,7 @@ class Select : public EntityBase {
*/
virtual void control(const std::string &value) = 0;
uint32_t hash_base() override;
CallbackManager<void(std::string)> state_callback_;
CallbackManager<void(std::string, size_t)> state_callback_;
bool has_state_{false};
};

View File

@@ -0,0 +1,120 @@
#include "select_call.h"
#include "select.h"
#include "esphome/core/log.h"
namespace esphome {
namespace select {
static const char *const TAG = "select";
SelectCall &SelectCall::set_option(const std::string &option) {
return with_operation(SELECT_OP_SET).with_option(option);
}
SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); }
SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); }
SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); }
SelectCall &SelectCall::select_first() { return with_operation(SELECT_OP_FIRST); }
SelectCall &SelectCall::select_last() { return with_operation(SELECT_OP_LAST); }
SelectCall &SelectCall::with_operation(SelectOperation operation) {
this->operation_ = operation;
return *this;
}
SelectCall &SelectCall::with_cycle(bool cycle) {
this->cycle_ = cycle;
return *this;
}
SelectCall &SelectCall::with_option(const std::string &option) {
this->option_ = option;
return *this;
}
SelectCall &SelectCall::with_index(size_t index) {
this->index_ = index;
return *this;
}
void SelectCall::perform() {
auto *parent = this->parent_;
const auto *name = parent->get_name().c_str();
const auto &traits = parent->traits;
auto options = traits.get_options();
if (this->operation_ == SELECT_OP_NONE) {
ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name);
return;
}
if (options.empty()) {
ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name);
return;
}
std::string target_value;
if (this->operation_ == SELECT_OP_SET) {
ESP_LOGD(TAG, "'%s' - Setting", name);
if (!this->option_.has_value()) {
ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name);
return;
}
target_value = this->option_.value();
} else if (this->operation_ == SELECT_OP_SET_INDEX) {
if (!this->index_.has_value()) {
ESP_LOGW(TAG, "'%s' - No index value set for SelectCall", name);
return;
}
if (this->index_.value() >= options.size()) {
ESP_LOGW(TAG, "'%s' - Index value %d out of bounds", name, this->index_.value());
return;
}
target_value = options[this->index_.value()];
} else if (this->operation_ == SELECT_OP_FIRST) {
target_value = options.front();
} else if (this->operation_ == SELECT_OP_LAST) {
target_value = options.back();
} else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) {
auto cycle = this->cycle_;
ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous",
cycle ? "" : "out");
if (!parent->has_state()) {
target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back();
} else {
auto index = parent->index_of(parent->state);
if (index.has_value()) {
auto size = options.size();
if (cycle) {
auto use_index = (size + index.value() + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size;
target_value = options[use_index];
} else {
if (this->operation_ == SELECT_OP_PREVIOUS && index.value() > 0) {
target_value = options[index.value() - 1];
} else if (this->operation_ == SELECT_OP_NEXT && index.value() < options.size() - 1) {
target_value = options[index.value() + 1];
} else {
return;
}
}
} else {
target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back();
}
}
}
if (std::find(options.begin(), options.end(), target_value) == options.end()) {
ESP_LOGW(TAG, "'%s' - Option %s is not a valid option", name, target_value.c_str());
return;
}
ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, target_value.c_str());
parent->control(target_value);
}
} // namespace select
} // namespace esphome

View File

@@ -0,0 +1,47 @@
#pragma once
#include "esphome/core/helpers.h"
namespace esphome {
namespace select {
class Select;
enum SelectOperation {
SELECT_OP_NONE,
SELECT_OP_SET,
SELECT_OP_SET_INDEX,
SELECT_OP_NEXT,
SELECT_OP_PREVIOUS,
SELECT_OP_FIRST,
SELECT_OP_LAST
};
class SelectCall {
public:
explicit SelectCall(Select *parent) : parent_(parent) {}
void perform();
SelectCall &set_option(const std::string &option);
SelectCall &set_index(size_t index);
SelectCall &select_next(bool cycle);
SelectCall &select_previous(bool cycle);
SelectCall &select_first();
SelectCall &select_last();
SelectCall &with_operation(SelectOperation operation);
SelectCall &with_cycle(bool cycle);
SelectCall &with_option(const std::string &option);
SelectCall &with_index(size_t index);
protected:
Select *const parent_;
optional<std::string> option_;
optional<size_t> index_;
SelectOperation operation_{SELECT_OP_NONE};
bool cycle_;
};
} // namespace select
} // namespace esphome

View File

@@ -0,0 +1,11 @@
#include "select_traits.h"
namespace esphome {
namespace select {
void SelectTraits::set_options(std::vector<std::string> options) { this->options_ = std::move(options); }
std::vector<std::string> SelectTraits::get_options() const { return this->options_; }
} // namespace select
} // namespace esphome

View File

@@ -0,0 +1,19 @@
#pragma once
#include <vector>
#include <string>
namespace esphome {
namespace select {
class SelectTraits {
public:
void set_options(std::vector<std::string> options);
std::vector<std::string> get_options() const;
protected:
std::vector<std::string> options_;
};
} // namespace select
} // namespace esphome

View File

View File

@@ -0,0 +1,21 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "sen5x.h"
namespace esphome {
namespace sen5x {
template<typename... Ts> class StartFanAction : public Action<Ts...> {
public:
explicit StartFanAction(SEN5XComponent *sen5x) : sen5x_(sen5x) {}
void play(Ts... x) override { this->sen5x_->start_fan_cleaning(); }
protected:
SEN5XComponent *sen5x_;
};
} // namespace sen5x
} // namespace esphome

View File

@@ -0,0 +1,413 @@
#include "sen5x.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace sen5x {
static const char *const TAG = "sen5x";
static const uint16_t SEN5X_CMD_AUTO_CLEANING_INTERVAL = 0x8004;
static const uint16_t SEN5X_CMD_GET_DATA_READY_STATUS = 0x0202;
static const uint16_t SEN5X_CMD_GET_FIRMWARE_VERSION = 0xD100;
static const uint16_t SEN5X_CMD_GET_PRODUCT_NAME = 0xD014;
static const uint16_t SEN5X_CMD_GET_SERIAL_NUMBER = 0xD033;
static const uint16_t SEN5X_CMD_NOX_ALGORITHM_TUNING = 0x60E1;
static const uint16_t SEN5X_CMD_READ_MEASUREMENT = 0x03C4;
static const uint16_t SEN5X_CMD_RHT_ACCELERATION_MODE = 0x60F7;
static const uint16_t SEN5X_CMD_START_CLEANING_FAN = 0x5607;
static const uint16_t SEN5X_CMD_START_MEASUREMENTS = 0x0021;
static const uint16_t SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY = 0x0037;
static const uint16_t SEN5X_CMD_STOP_MEASUREMENTS = 0x3f86;
static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2;
static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181;
static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
void SEN5XComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up sen5x...");
// the sensor needs 1000 ms to enter the idle state
this->set_timeout(1000, [this]() {
// Check if measurement is ready before reading the value
if (!this->write_command(SEN5X_CMD_GET_DATA_READY_STATUS)) {
ESP_LOGE(TAG, "Failed to write data ready status command");
this->mark_failed();
return;
}
uint16_t raw_read_status;
if (!this->read_data(raw_read_status)) {
ESP_LOGE(TAG, "Failed to read data ready status");
this->mark_failed();
return;
}
uint32_t stop_measurement_delay = 0;
// In order to query the device periodic measurement must be ceased
if (raw_read_status) {
ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement");
if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) {
ESP_LOGE(TAG, "Failed to stop measurements");
this->mark_failed();
return;
}
// According to the SEN5x datasheet the sensor will only respond to other commands after waiting 200 ms after
// issuing the stop_periodic_measurement command
stop_measurement_delay = 200;
}
this->set_timeout(stop_measurement_delay, [this]() {
uint16_t raw_serial_number[3];
if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) {
ESP_LOGE(TAG, "Failed to read serial number");
this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
this->mark_failed();
return;
}
this->serial_number_[0] = static_cast<bool>(uint16_t(raw_serial_number[0]) & 0xFF);
this->serial_number_[1] = static_cast<uint16_t>(raw_serial_number[0] & 0xFF);
this->serial_number_[2] = static_cast<uint16_t>(raw_serial_number[1] >> 8);
ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]);
uint16_t raw_product_name[16];
if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
ESP_LOGE(TAG, "Failed to read product name");
this->error_code_ = PRODUCT_NAME_FAILED;
this->mark_failed();
return;
}
// 2 ASCII bytes are encoded in an int
const uint16_t *current_int = raw_product_name;
char current_char;
uint8_t max = 16;
do {
// first char
current_char = *current_int >> 8;
if (current_char) {
product_name_.push_back(current_char);
// second char
current_char = *current_int & 0xFF;
if (current_char)
product_name_.push_back(current_char);
}
current_int++;
} while (current_char && --max);
Sen5xType sen5x_type = UNKNOWN;
if (product_name_ == "SEN50") {
sen5x_type = SEN50;
} else {
if (product_name_ == "SEN54") {
sen5x_type = SEN54;
} else {
if (product_name_ == "SEN55") {
sen5x_type = SEN55;
}
}
ESP_LOGD(TAG, "Productname %s", product_name_.c_str());
}
if (this->humidity_sensor_ && sen5x_type == SEN50) {
ESP_LOGE(TAG, "For Relative humidity a SEN54 OR SEN55 is required. You are using a <%s> sensor",
this->product_name_.c_str());
this->humidity_sensor_ = nullptr; // mark as not used
}
if (this->temperature_sensor_ && sen5x_type == SEN50) {
ESP_LOGE(TAG, "For Temperature a SEN54 OR SEN55 is required. You are using a <%s> sensor",
this->product_name_.c_str());
this->temperature_sensor_ = nullptr; // mark as not used
}
if (this->voc_sensor_ && sen5x_type == SEN50) {
ESP_LOGE(TAG, "For VOC a SEN54 OR SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str());
this->voc_sensor_ = nullptr; // mark as not used
}
if (this->nox_sensor_ && sen5x_type != SEN55) {
ESP_LOGE(TAG, "For NOx a SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str());
this->nox_sensor_ = nullptr; // mark as not used
}
if (!this->get_register(SEN5X_CMD_GET_FIRMWARE_VERSION, this->firmware_version_, 20)) {
ESP_LOGE(TAG, "Failed to read firmware version");
this->error_code_ = FIRMWARE_FAILED;
this->mark_failed();
return;
}
this->firmware_version_ >>= 8;
ESP_LOGD(TAG, "Firmware version %d", this->firmware_version_);
if (this->voc_sensor_ && this->store_baseline_) {
// Hash with compilation time
// This ensures the baseline storage is cleared after OTA
uint32_t hash = fnv1_hash(App.get_compilation_time());
this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true);
if (this->pref_.load(&this->voc_baselines_storage_)) {
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0,
voc_baselines_storage_.state1);
}
// Initialize storage timestamp
this->seconds_since_last_store_ = 0;
if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X",
this->voc_baselines_storage_.state0, voc_baselines_storage_.state1);
uint16_t states[4];
states[0] = voc_baselines_storage_.state0 >> 16;
states[1] = voc_baselines_storage_.state0 & 0xFFFF;
states[2] = voc_baselines_storage_.state1 >> 16;
states[3] = voc_baselines_storage_.state1 & 0xFFFF;
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) {
ESP_LOGE(TAG, "Failed to set VOC baseline from saved state");
}
}
}
bool result;
if (this->auto_cleaning_interval_.has_value()) {
// override default value
result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value());
} else {
result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
}
if (result) {
delay(20);
uint16_t secs[2];
if (this->read_data(secs, 2)) {
auto_cleaning_interval_ = secs[0] << 16 | secs[1];
}
}
if (acceleration_mode_.has_value()) {
result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, acceleration_mode_.value());
} else {
result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE);
}
if (!result) {
ESP_LOGE(TAG, "Failed to set rh/t acceleration mode");
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
delay(20);
if (!acceleration_mode_.has_value()) {
uint16_t mode;
if (this->read_data(mode)) {
this->acceleration_mode_ = RhtAccelerationMode(mode);
} else {
ESP_LOGE(TAG, "Failed to read RHT Acceleration mode");
}
}
if (this->voc_tuning_params_.has_value())
this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value());
if (this->nox_tuning_params_.has_value())
this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
if (this->temperature_compensation_.has_value())
this->write_temperature_compensation_(this->temperature_compensation_.value());
// Finally start sensor measurements
auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY;
if (this->pm_1_0_sensor_ || this->pm_2_5_sensor_ || this->pm_4_0_sensor_ || this->pm_10_0_sensor_) {
// if any of the gas sensors are active we need a full measurement
cmd = SEN5X_CMD_START_MEASUREMENTS;
}
if (!this->write_command(cmd)) {
ESP_LOGE(TAG, "Error starting continuous measurements.");
this->error_code_ = MEASUREMENT_INIT_FAILED;
this->mark_failed();
return;
}
initialized_ = true;
ESP_LOGD(TAG, "Sensor initialized");
});
});
}
void SEN5XComponent::dump_config() {
ESP_LOGCONFIG(TAG, "sen5x:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
break;
case MEASUREMENT_INIT_FAILED:
ESP_LOGW(TAG, "Measurement Initialization failed!");
break;
case SERIAL_NUMBER_IDENTIFICATION_FAILED:
ESP_LOGW(TAG, "Unable to read sensor serial id");
break;
case PRODUCT_NAME_FAILED:
ESP_LOGW(TAG, "Unable to read product name");
break;
case FIRMWARE_FAILED:
ESP_LOGW(TAG, "Unable to read sensor firmware version");
break;
default:
ESP_LOGW(TAG, "Unknown setup error!");
break;
}
}
ESP_LOGCONFIG(TAG, " Productname: %s", this->product_name_.c_str());
ESP_LOGCONFIG(TAG, " Firmware version: %d", this->firmware_version_);
ESP_LOGCONFIG(TAG, " Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]);
if (this->auto_cleaning_interval_.has_value()) {
ESP_LOGCONFIG(TAG, " Auto auto cleaning interval %d seconds", auto_cleaning_interval_.value());
}
if (this->acceleration_mode_.has_value()) {
switch (this->acceleration_mode_.value()) {
case LOW_ACCELERATION:
ESP_LOGCONFIG(TAG, " Low RH/T acceleration mode");
break;
case MEDIUM_ACCELERATION:
ESP_LOGCONFIG(TAG, " Medium RH/T accelertion mode");
break;
case HIGH_ACCELERATION:
ESP_LOGCONFIG(TAG, " High RH/T accelertion mode");
break;
}
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_);
LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
LOG_SENSOR(" ", "VOC", this->voc_sensor_); // SEN54 and SEN55 only
LOG_SENSOR(" ", "NOx", this->nox_sensor_); // SEN55 only
}
void SEN5XComponent::update() {
if (!initialized_) {
return;
}
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
// much
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
// run it a bit later to avoid adding a delay here
this->set_timeout(550, [this]() {
uint16_t states[4];
if (this->read_data(states, 4)) {
uint32_t state0 = states[0] << 16 | states[1];
uint32_t state1 = states[2] << 16 | states[3];
if ((uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state0 - state0)) >
MAXIMUM_STORAGE_DIFF ||
(uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state1 - state1)) >
MAXIMUM_STORAGE_DIFF) {
this->seconds_since_last_store_ = 0;
this->voc_baselines_storage_.state0 = state0;
this->voc_baselines_storage_.state1 = state1;
if (this->pref_.save(&this->voc_baselines_storage_)) {
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0,
voc_baselines_storage_.state1);
} else {
ESP_LOGW(TAG, "Could not store VOC baselines");
}
}
}
});
}
}
if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) {
this->status_set_warning();
ESP_LOGD(TAG, "write error read measurement (%d)", this->last_error_);
return;
}
this->set_timeout(20, [this]() {
uint16_t measurements[8];
if (!this->read_data(measurements, 8)) {
this->status_set_warning();
ESP_LOGD(TAG, "read data error (%d)", this->last_error_);
return;
}
float pm_1_0 = measurements[0] / 10.0;
if (measurements[0] == 0xFFFF)
pm_1_0 = NAN;
float pm_2_5 = measurements[1] / 10.0;
if (measurements[1] == 0xFFFF)
pm_2_5 = NAN;
float pm_4_0 = measurements[2] / 10.0;
if (measurements[2] == 0xFFFF)
pm_4_0 = NAN;
float pm_10_0 = measurements[3] / 10.0;
if (measurements[3] == 0xFFFF)
pm_10_0 = NAN;
float humidity = measurements[4] / 100.0;
if (measurements[4] == 0xFFFF)
humidity = NAN;
float temperature = measurements[5] / 200.0;
if (measurements[5] == 0xFFFF)
temperature = NAN;
float voc = measurements[6] / 10.0;
if (measurements[6] == 0xFFFF)
voc = NAN;
float nox = measurements[7] / 10.0;
if (measurements[7] == 0xFFFF)
nox = NAN;
if (this->pm_1_0_sensor_ != nullptr)
this->pm_1_0_sensor_->publish_state(pm_1_0);
if (this->pm_2_5_sensor_ != nullptr)
this->pm_2_5_sensor_->publish_state(pm_2_5);
if (this->pm_4_0_sensor_ != nullptr)
this->pm_4_0_sensor_->publish_state(pm_4_0);
if (this->pm_10_0_sensor_ != nullptr)
this->pm_10_0_sensor_->publish_state(pm_10_0);
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature);
if (this->humidity_sensor_ != nullptr)
this->humidity_sensor_->publish_state(humidity);
if (this->voc_sensor_ != nullptr)
this->voc_sensor_->publish_state(voc);
if (this->nox_sensor_ != nullptr)
this->nox_sensor_->publish_state(nox);
this->status_clear_warning();
});
}
bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) {
uint16_t params[6];
params[0] = tuning.index_offset;
params[1] = tuning.learning_time_offset_hours;
params[2] = tuning.learning_time_gain_hours;
params[3] = tuning.gating_max_duration_minutes;
params[4] = tuning.std_initial;
params[5] = tuning.gain_factor;
auto result = write_command(i2c_command, params, 6);
if (!result) {
ESP_LOGE(TAG, "set tuning parameters failed. i2c command=%0xX, err=%d", i2c_command, this->last_error_);
}
return result;
}
bool SEN5XComponent::write_temperature_compensation_(const TemperatureCompensation &compensation) {
uint16_t params[3];
params[0] = compensation.offset;
params[1] = compensation.normalized_offset_slope;
params[2] = compensation.time_constant;
if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) {
ESP_LOGE(TAG, "set temperature_compensation failed. Err=%d", this->last_error_);
return false;
}
return true;
}
bool SEN5XComponent::start_fan_cleaning() {
if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) {
this->status_set_warning();
ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_);
return false;
} else {
ESP_LOGD(TAG, "Fan auto clean started");
}
return true;
}
} // namespace sen5x
} // namespace esphome

View File

@@ -0,0 +1,128 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensirion_common/i2c_sensirion.h"
#include "esphome/core/application.h"
#include "esphome/core/preferences.h"
namespace esphome {
namespace sen5x {
enum ERRORCODE {
COMMUNICATION_FAILED,
SERIAL_NUMBER_IDENTIFICATION_FAILED,
MEASUREMENT_INIT_FAILED,
PRODUCT_NAME_FAILED,
FIRMWARE_FAILED,
UNKNOWN
};
// Shortest time interval of 3H for storing baseline values.
// Prevents wear of the flash because of too many write operations
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
// Store anyway if the baseline difference exceeds the max storage diff value
const uint32_t MAXIMUM_STORAGE_DIFF = 50;
struct Sen5xBaselines {
int32_t state0;
int32_t state1;
} PACKED; // NOLINT
enum RhtAccelerationMode : uint16_t { LOW_ACCELERATION = 0, MEDIUM_ACCELERATION = 1, HIGH_ACCELERATION = 2 };
struct GasTuning {
uint16_t index_offset;
uint16_t learning_time_offset_hours;
uint16_t learning_time_gain_hours;
uint16_t gating_max_duration_minutes;
uint16_t std_initial;
uint16_t gain_factor;
};
struct TemperatureCompensation {
uint16_t offset;
uint16_t normalized_offset_slope;
uint16_t time_constant;
};
class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
public:
float get_setup_priority() const override { return setup_priority::DATA; }
void setup() override;
void dump_config() override;
void update() override;
enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN };
void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; }
void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; }
void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; }
void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; }
void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; }
void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
void set_acceleration_mode(RhtAccelerationMode mode) { acceleration_mode_ = mode; }
void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { auto_cleaning_interval_ = auto_cleaning_interval; }
void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t std_initial, uint16_t gain_factor) {
voc_tuning_params_.value().index_offset = index_offset;
voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
voc_tuning_params_.value().std_initial = std_initial;
voc_tuning_params_.value().gain_factor = gain_factor;
}
void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t gain_factor) {
nox_tuning_params_.value().index_offset = index_offset;
nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
nox_tuning_params_.value().std_initial = 50;
nox_tuning_params_.value().gain_factor = gain_factor;
}
void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) {
temperature_compensation_.value().offset = offset * 200;
temperature_compensation_.value().normalized_offset_slope = normalized_offset_slope * 100;
temperature_compensation_.value().time_constant = time_constant;
}
bool start_fan_cleaning();
protected:
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
bool write_temperature_compensation_(const TemperatureCompensation &compensation);
ERRORCODE error_code_;
bool initialized_{false};
sensor::Sensor *pm_1_0_sensor_{nullptr};
sensor::Sensor *pm_2_5_sensor_{nullptr};
sensor::Sensor *pm_4_0_sensor_{nullptr};
sensor::Sensor *pm_10_0_sensor_{nullptr};
// SEN54 and SEN55 only
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *voc_sensor_{nullptr};
// SEN55 only
sensor::Sensor *nox_sensor_{nullptr};
std::string product_name_;
uint8_t serial_number_[4];
uint16_t firmware_version_;
Sen5xBaselines voc_baselines_storage_;
bool store_baseline_;
uint32_t seconds_since_last_store_;
ESPPreferenceObject pref_;
optional<RhtAccelerationMode> acceleration_mode_;
optional<uint32_t> auto_cleaning_interval_;
optional<GasTuning> voc_tuning_params_;
optional<GasTuning> nox_tuning_params_;
optional<TemperatureCompensation> temperature_compensation_;
};
} // namespace sen5x
} // namespace esphome

View File

@@ -0,0 +1,241 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor, sensirion_common
from esphome import automation
from esphome.automation import maybe_simple_id
from esphome.const import (
CONF_HUMIDITY,
CONF_ID,
CONF_OFFSET,
CONF_PM_1_0,
CONF_PM_10_0,
CONF_PM_2_5,
CONF_PM_4_0,
CONF_STORE_BASELINE,
CONF_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_NITROUS_OXIDE,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
DEVICE_CLASS_PM25,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
ICON_CHEMICAL_WEAPON,
ICON_RADIATOR,
ICON_THERMOMETER,
ICON_WATER_PERCENT,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_MICROGRAMS_PER_CUBIC_METER,
UNIT_PERCENT,
)
CODEOWNERS = ["@martgras"]
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
sen5x_ns = cg.esphome_ns.namespace("sen5x")
SEN5XComponent = sen5x_ns.class_(
"SEN5XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
)
RhtAccelerationMode = sen5x_ns.enum("RhtAccelerationMode")
CONF_ACCELERATION_MODE = "acceleration_mode"
CONF_ALGORITHM_TUNING = "algorithm_tuning"
CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval"
CONF_GAIN_FACTOR = "gain_factor"
CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes"
CONF_INDEX_OFFSET = "index_offset"
CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours"
CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope"
CONF_NOX = "nox"
CONF_STD_INITIAL = "std_initial"
CONF_TEMPERATURE_COMPENSATION = "temperature_compensation"
CONF_TIME_CONSTANT = "time_constant"
CONF_VOC = "voc"
CONF_VOC_BASELINE = "voc_baseline"
# Actions
StartFanAction = sen5x_ns.class_("StartFanAction", automation.Action)
ACCELERATION_MODES = {
"low": RhtAccelerationMode.LOW_ACCELERATION,
"medium": RhtAccelerationMode.MEDIUM_ACCELERATION,
"high": RhtAccelerationMode.HIGH_ACCELERATION,
}
GAS_SENSOR = cv.Schema(
{
cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema(
{
cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_range(1, 250),
cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_range(
1, 1000
),
cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_range(
1, 1000
),
cv.Optional(
CONF_GATING_MAX_DURATION_MINUTES, default=720
): cv.int_range(0, 3000),
cv.Optional(CONF_STD_INITIAL, default=50): cv.int_,
cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_range(1, 1000),
}
)
}
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SEN5XComponent),
cv.Optional(CONF_PM_1_0): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PM1,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_4_0): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PM10,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.time_period_in_seconds_,
cv.Optional(CONF_VOC): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
).extend(GAS_SENSOR),
cv.Optional(CONF_NOX): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_NITROUS_OXIDE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(GAS_SENSOR),
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_WATER_PERCENT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE_COMPENSATION): cv.Schema(
{
cv.Optional(CONF_OFFSET, default=0): cv.float_,
cv.Optional(CONF_NORMALIZED_OFFSET_SLOPE, default=0): cv.percentage,
cv.Optional(CONF_TIME_CONSTANT, default=0): cv.int_,
}
),
cv.Optional(CONF_ACCELERATION_MODE): cv.enum(ACCELERATION_MODES),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x69))
)
SENSOR_MAP = {
CONF_PM_1_0: "set_pm_1_0_sensor",
CONF_PM_2_5: "set_pm_2_5_sensor",
CONF_PM_4_0: "set_pm_4_0_sensor",
CONF_PM_10_0: "set_pm_10_0_sensor",
CONF_VOC: "set_voc_sensor",
CONF_NOX: "set_nox_sensor",
CONF_TEMPERATURE: "set_temperature_sensor",
CONF_HUMIDITY: "set_humidity_sensor",
}
SETTING_MAP = {
CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval",
CONF_ACCELERATION_MODE: "set_acceleration_mode",
}
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
for key, funcName in SETTING_MAP.items():
if key in config:
cg.add(getattr(var, funcName)(config[key]))
for key, funcName in SENSOR_MAP.items():
if key in config:
sens = await sensor.new_sensor(config[key])
cg.add(getattr(var, funcName)(sens))
if CONF_VOC in config and CONF_ALGORITHM_TUNING in config[CONF_VOC]:
cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING]
cg.add(
var.set_voc_algorithm_tuning(
cfg[CONF_INDEX_OFFSET],
cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
cfg[CONF_LEARNING_TIME_GAIN_HOURS],
cfg[CONF_GATING_MAX_DURATION_MINUTES],
cfg[CONF_STD_INITIAL],
cfg[CONF_GAIN_FACTOR],
)
)
if CONF_NOX in config and CONF_ALGORITHM_TUNING in config[CONF_NOX]:
cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING]
cg.add(
var.set_nox_algorithm_tuning(
cfg[CONF_INDEX_OFFSET],
cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
cfg[CONF_LEARNING_TIME_GAIN_HOURS],
cfg[CONF_GATING_MAX_DURATION_MINUTES],
cfg[CONF_GAIN_FACTOR],
)
)
if CONF_TEMPERATURE_COMPENSATION in config:
cg.add(
var.set_temperature_compensation(
config[CONF_TEMPERATURE_COMPENSATION][CONF_OFFSET],
config[CONF_TEMPERATURE_COMPENSATION][CONF_NORMALIZED_OFFSET_SLOPE],
config[CONF_TEMPERATURE_COMPENSATION][CONF_TIME_CONSTANT],
)
)
SEN5X_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(SEN5XComponent),
}
)
@automation.register_action(
"sen5x.start_fan_autoclean", StartFanAction, SEN5X_ACTION_SCHEMA
)
async def sen54_fan_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)

View File

@@ -29,6 +29,7 @@ from esphome.const import (
CONF_WINDOW_SIZE,
CONF_MQTT_ID,
CONF_FORCE_UPDATE,
DEVICE_CLASS_DURATION,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_AQI,
DEVICE_CLASS_BATTERY,
@@ -70,6 +71,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_DURATION,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_HUMIDITY,

View File

@@ -126,7 +126,6 @@ void Sensor::internal_send_state_to_frontend(float state) {
this->callback_.call(state);
}
bool Sensor::has_state() const { return this->has_state_; }
uint32_t Sensor::hash_base() { return 2455723294UL; }
} // namespace sensor
} // namespace esphome

View File

@@ -174,8 +174,6 @@ class Sensor : public EntityBase {
*/
virtual StateClass state_class(); // NOLINT
uint32_t hash_base() override;
CallbackManager<void(float)> raw_callback_; ///< Storage for raw state callbacks.
CallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.

View File

@@ -1,628 +0,0 @@
#include "sensirion_voc_algorithm.h"
namespace esphome {
namespace sgp40 {
/* The VOC code were originally created by
* https://github.com/Sensirion/embedded-sgp
* The fixed point arithmetic parts of this code were originally created by
* https://github.com/PetteriAimonen/libfixmath
*/
/*!< the maximum value of fix16_t */
#define FIX16_MAXIMUM 0x7FFFFFFF
/*!< the minimum value of fix16_t */
static const uint32_t FIX16_MINIMUM = 0x80000000;
/*!< the value used to indicate overflows when FIXMATH_NO_OVERFLOW is not
* specified */
static const uint32_t FIX16_OVERFLOW = 0x80000000;
/*!< fix16_t value of 1 */
const uint32_t FIX16_ONE = 0x00010000;
inline fix16_t fix16_from_int(int32_t a) { return a * FIX16_ONE; }
inline int32_t fix16_cast_to_int(fix16_t a) { return (a >> 16); }
/*! Multiplies the two given fix16_t's and returns the result. */
static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1);
/*! Divides the first given fix16_t by the second and returns the result. */
static fix16_t fix16_div(fix16_t a, fix16_t b);
/*! Returns the square root of the given fix16_t. */
static fix16_t fix16_sqrt(fix16_t in_value);
/*! Returns the exponent (e^) of the given fix16_t. */
static fix16_t fix16_exp(fix16_t in_value);
static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1) {
// Each argument is divided to 16-bit parts.
// AB
// * CD
// -----------
// BD 16 * 16 -> 32 bit products
// CB
// AD
// AC
// |----| 64 bit product
int32_t a = (in_arg0 >> 16), c = (in_arg1 >> 16);
uint32_t b = (in_arg0 & 0xFFFF), d = (in_arg1 & 0xFFFF);
int32_t ac = a * c;
int32_t ad_cb = a * d + c * b;
uint32_t bd = b * d;
int32_t product_hi = ac + (ad_cb >> 16); // NOLINT
// Handle carry from lower 32 bits to upper part of result.
uint32_t ad_cb_temp = ad_cb << 16; // NOLINT
uint32_t product_lo = bd + ad_cb_temp;
if (product_lo < bd)
product_hi++;
#ifndef FIXMATH_NO_OVERFLOW
// The upper 17 bits should all be the same (the sign).
if (product_hi >> 31 != product_hi >> 15)
return FIX16_OVERFLOW;
#endif
#ifdef FIXMATH_NO_ROUNDING
return (product_hi << 16) | (product_lo >> 16);
#else
// Subtracting 0x8000 (= 0.5) and then using signed right shift
// achieves proper rounding to result-1, except in the corner
// case of negative numbers and lowest word = 0x8000.
// To handle that, we also have to subtract 1 for negative numbers.
uint32_t product_lo_tmp = product_lo;
product_lo -= 0x8000;
product_lo -= (uint32_t) product_hi >> 31;
if (product_lo > product_lo_tmp)
product_hi--;
// Discard the lowest 16 bits. Note that this is not exactly the same
// as dividing by 0x10000. For example if product = -1, result will
// also be -1 and not 0. This is compensated by adding +1 to the result
// and compensating this in turn in the rounding above.
fix16_t result = (product_hi << 16) | (product_lo >> 16); // NOLINT
result += 1;
return result;
#endif
}
static fix16_t fix16_div(fix16_t a, fix16_t b) {
// This uses the basic binary restoring division algorithm.
// It appears to be faster to do the whole division manually than
// trying to compose a 64-bit divide out of 32-bit divisions on
// platforms without hardware divide.
if (b == 0)
return FIX16_MINIMUM;
uint32_t remainder = (a >= 0) ? a : (-a);
uint32_t divider = (b >= 0) ? b : (-b);
uint32_t quotient = 0;
uint32_t bit = 0x10000;
/* The algorithm requires D >= R */
while (divider < remainder) {
divider <<= 1;
bit <<= 1;
}
#ifndef FIXMATH_NO_OVERFLOW
if (!bit)
return FIX16_OVERFLOW;
#endif
if (divider & 0x80000000) {
// Perform one step manually to avoid overflows later.
// We know that divider's bottom bit is 0 here.
if (remainder >= divider) {
quotient |= bit;
remainder -= divider;
}
divider >>= 1;
bit >>= 1;
}
/* Main division loop */
while (bit && remainder) {
if (remainder >= divider) {
quotient |= bit;
remainder -= divider;
}
remainder <<= 1;
bit >>= 1;
}
#ifndef FIXMATH_NO_ROUNDING
if (remainder >= divider) {
quotient++;
}
#endif
fix16_t result = quotient;
/* Figure out the sign of result */
if ((a ^ b) & 0x80000000) {
#ifndef FIXMATH_NO_OVERFLOW
if (result == FIX16_MINIMUM) // NOLINT(clang-diagnostic-sign-compare)
return FIX16_OVERFLOW;
#endif
result = -result;
}
return result;
}
static fix16_t fix16_sqrt(fix16_t in_value) {
// It is assumed that x is not negative
uint32_t num = in_value;
uint32_t result = 0;
uint32_t bit;
uint8_t n;
bit = (uint32_t) 1 << 30;
while (bit > num)
bit >>= 2;
// The main part is executed twice, in order to avoid
// using 64 bit values in computations.
for (n = 0; n < 2; n++) {
// First we get the top 24 bits of the answer.
while (bit) {
if (num >= result + bit) {
num -= result + bit;
result = (result >> 1) + bit;
} else {
result = (result >> 1);
}
bit >>= 2;
}
if (n == 0) {
// Then process it again to get the lowest 8 bits.
if (num > 65535) {
// The remainder 'num' is too large to be shifted left
// by 16, so we have to add 1 to result manually and
// adjust 'num' accordingly.
// num = a - (result + 0.5)^2
// = num + result^2 - (result + 0.5)^2
// = num - result - 0.5
num -= result;
num = (num << 16) - 0x8000;
result = (result << 16) + 0x8000;
} else {
num <<= 16;
result <<= 16;
}
bit = 1 << 14;
}
}
#ifndef FIXMATH_NO_ROUNDING
// Finally, if next bit would have been 1, round the result upwards.
if (num > result) {
result++;
}
#endif
return (fix16_t) result;
}
static fix16_t fix16_exp(fix16_t in_value) {
// Function to approximate exp(); optimized more for code size than speed
// exp(x) for x = +/- {1, 1/8, 1/64, 1/512}
fix16_t x = in_value;
static const uint8_t NUM_EXP_VALUES = 4;
static const fix16_t EXP_POS_VALUES[4] = {F16(2.7182818), F16(1.1331485), F16(1.0157477), F16(1.0019550)};
static const fix16_t EXP_NEG_VALUES[4] = {F16(0.3678794), F16(0.8824969), F16(0.9844964), F16(0.9980488)};
const fix16_t *exp_values;
fix16_t res, arg;
uint16_t i;
if (x >= F16(10.3972))
return FIX16_MAXIMUM;
if (x <= F16(-11.7835))
return 0;
if (x < 0) {
x = -x;
exp_values = EXP_NEG_VALUES;
} else {
exp_values = EXP_POS_VALUES;
}
res = FIX16_ONE;
arg = FIX16_ONE;
for (i = 0; i < NUM_EXP_VALUES; i++) {
while (x >= arg) {
res = fix16_mul(res, exp_values[i]);
x -= arg;
}
arg >>= 3;
}
return res;
}
static void voc_algorithm_init_instances(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial,
fix16_t tau_mean_variance_hours,
fix16_t gating_max_duration_minutes);
static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std,
fix16_t uptime_gamma);
static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params);
static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params,
fix16_t voc_index_from_prior);
static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw,
fix16_t voc_index_from_prior);
static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l,
fix16_t x0, fix16_t k);
static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample);
static void voc_algorithm_mox_model_init(VocAlgorithmParams *params);
static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean);
static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw);
static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params);
static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset);
static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample);
static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params);
static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params);
static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample);
void voc_algorithm_init(VocAlgorithmParams *params) {
params->mVoc_Index_Offset = F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT);
params->mTau_Mean_Variance_Hours = F16(VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS);
params->mGating_Max_Duration_Minutes = F16(VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES);
params->mSraw_Std_Initial = F16(VOC_ALGORITHM_SRAW_STD_INITIAL);
params->mUptime = F16(0.);
params->mSraw = F16(0.);
params->mVoc_Index = 0;
voc_algorithm_init_instances(params);
}
static void voc_algorithm_init_instances(VocAlgorithmParams *params) {
voc_algorithm_mean_variance_estimator_init(params);
voc_algorithm_mean_variance_estimator_set_parameters(
params, params->mSraw_Std_Initial, params->mTau_Mean_Variance_Hours, params->mGating_Max_Duration_Minutes);
voc_algorithm_mox_model_init(params);
voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params),
voc_algorithm_mean_variance_estimator_get_mean(params));
voc_algorithm_sigmoid_scaled_init(params);
voc_algorithm_sigmoid_scaled_set_parameters(params, params->mVoc_Index_Offset);
voc_algorithm_adaptive_lowpass_init(params);
voc_algorithm_adaptive_lowpass_set_parameters(params);
}
void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1) {
*state0 = voc_algorithm_mean_variance_estimator_get_mean(params);
*state1 = voc_algorithm_mean_variance_estimator_get_std(params);
}
void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1) {
voc_algorithm_mean_variance_estimator_set_states(params, state0, state1, F16(VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA));
params->mSraw = state0;
}
void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset,
int32_t learning_time_hours, int32_t gating_max_duration_minutes,
int32_t std_initial) {
params->mVoc_Index_Offset = (fix16_from_int(voc_index_offset));
params->mTau_Mean_Variance_Hours = (fix16_from_int(learning_time_hours));
params->mGating_Max_Duration_Minutes = (fix16_from_int(gating_max_duration_minutes));
params->mSraw_Std_Initial = (fix16_from_int(std_initial));
voc_algorithm_init_instances(params);
}
void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index) {
if ((params->mUptime <= F16(VOC_ALGORITHM_INITIAL_BLACKOUT))) {
params->mUptime = (params->mUptime + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
} else {
if (((sraw > 0) && (sraw < 65000))) {
if ((sraw < 20001)) {
sraw = 20001;
} else if ((sraw > 52767)) {
sraw = 52767;
}
params->mSraw = (fix16_from_int((sraw - 20000)));
}
params->mVoc_Index = voc_algorithm_mox_model_process(params, params->mSraw);
params->mVoc_Index = voc_algorithm_sigmoid_scaled_process(params, params->mVoc_Index);
params->mVoc_Index = voc_algorithm_adaptive_lowpass_process(params, params->mVoc_Index);
if ((params->mVoc_Index < F16(0.5))) {
params->mVoc_Index = F16(0.5);
}
if ((params->mSraw > F16(0.))) {
voc_algorithm_mean_variance_estimator_process(params, params->mSraw, params->mVoc_Index);
voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params),
voc_algorithm_mean_variance_estimator_get_mean(params));
}
}
*voc_index = (fix16_cast_to_int((params->mVoc_Index + F16(0.5))));
}
static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params) {
voc_algorithm_mean_variance_estimator_set_parameters(params, F16(0.), F16(0.), F16(0.));
voc_algorithm_mean_variance_estimator_init_instances(params);
}
static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params) {
voc_algorithm_mean_variance_estimator_sigmoid_init(params);
}
static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial,
fix16_t tau_mean_variance_hours,
fix16_t gating_max_duration_minutes) {
params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes = gating_max_duration_minutes;
params->m_Mean_Variance_Estimator_Initialized = false;
params->m_Mean_Variance_Estimator_Mean = F16(0.);
params->m_Mean_Variance_Estimator_Sraw_Offset = F16(0.);
params->m_Mean_Variance_Estimator_Std = std_initial;
params->m_Mean_Variance_Estimator_Gamma =
(fix16_div(F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * (VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.))),
(tau_mean_variance_hours + F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.)))));
params->m_Mean_Variance_Estimator_Gamma_Initial_Mean =
F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) /
(VOC_ALGORITHM_TAU_INITIAL_MEAN + VOC_ALGORITHM_SAMPLING_INTERVAL)));
params->m_Mean_Variance_Estimator_Gamma_Initial_Variance =
F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) /
(VOC_ALGORITHM_TAU_INITIAL_VARIANCE + VOC_ALGORITHM_SAMPLING_INTERVAL)));
params->m_Mean_Variance_Estimator_Gamma_Mean = F16(0.);
params->m_Mean_Variance_Estimator_Gamma_Variance = F16(0.);
params->m_Mean_Variance_Estimator_Uptime_Gamma = F16(0.);
params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.);
params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.);
}
static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std,
fix16_t uptime_gamma) {
params->m_Mean_Variance_Estimator_Mean = mean;
params->m_Mean_Variance_Estimator_Std = std;
params->m_Mean_Variance_Estimator_Uptime_Gamma = uptime_gamma;
params->m_Mean_Variance_Estimator_Initialized = true;
}
static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params) {
return params->m_Mean_Variance_Estimator_Std;
}
static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params) {
return (params->m_Mean_Variance_Estimator_Mean + params->m_Mean_Variance_Estimator_Sraw_Offset);
}
static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params,
fix16_t voc_index_from_prior) {
fix16_t uptime_limit;
fix16_t sigmoid_gamma_mean;
fix16_t gamma_mean;
fix16_t gating_threshold_mean;
fix16_t sigmoid_gating_mean;
fix16_t sigmoid_gamma_variance;
fix16_t gamma_variance;
fix16_t gating_threshold_variance;
fix16_t sigmoid_gating_variance;
uptime_limit = F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX - VOC_ALGORITHM_SAMPLING_INTERVAL));
if ((params->m_Mean_Variance_Estimator_Uptime_Gamma < uptime_limit)) {
params->m_Mean_Variance_Estimator_Uptime_Gamma =
(params->m_Mean_Variance_Estimator_Uptime_Gamma + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
}
if ((params->m_Mean_Variance_Estimator_Uptime_Gating < uptime_limit)) {
params->m_Mean_Variance_Estimator_Uptime_Gating =
(params->m_Mean_Variance_Estimator_Uptime_Gating + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
}
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_MEAN),
F16(VOC_ALGORITHM_INIT_TRANSITION_MEAN));
sigmoid_gamma_mean =
voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma);
gamma_mean =
(params->m_Mean_Variance_Estimator_Gamma +
(fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Mean - params->m_Mean_Variance_Estimator_Gamma),
sigmoid_gamma_mean)));
gating_threshold_mean = (F16(VOC_ALGORITHM_GATING_THRESHOLD) +
(fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)),
voc_algorithm_mean_variance_estimator_sigmoid_process(
params, params->m_Mean_Variance_Estimator_Uptime_Gating))));
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_mean,
F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION));
sigmoid_gating_mean = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior);
params->m_Mean_Variance_Estimator_Gamma_Mean = (fix16_mul(sigmoid_gating_mean, gamma_mean));
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(
params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_VARIANCE), F16(VOC_ALGORITHM_INIT_TRANSITION_VARIANCE));
sigmoid_gamma_variance =
voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma);
gamma_variance =
(params->m_Mean_Variance_Estimator_Gamma +
(fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Variance - params->m_Mean_Variance_Estimator_Gamma),
(sigmoid_gamma_variance - sigmoid_gamma_mean))));
gating_threshold_variance =
(F16(VOC_ALGORITHM_GATING_THRESHOLD) +
(fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)),
voc_algorithm_mean_variance_estimator_sigmoid_process(
params, params->m_Mean_Variance_Estimator_Uptime_Gating))));
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_variance,
F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION));
sigmoid_gating_variance = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior);
params->m_Mean_Variance_Estimator_Gamma_Variance = (fix16_mul(sigmoid_gating_variance, gamma_variance));
params->m_Mean_Variance_Estimator_Gating_Duration_Minutes =
(params->m_Mean_Variance_Estimator_Gating_Duration_Minutes +
(fix16_mul(F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 60.)),
((fix16_mul((F16(1.) - sigmoid_gating_mean), F16((1. + VOC_ALGORITHM_GATING_MAX_RATIO)))) -
F16(VOC_ALGORITHM_GATING_MAX_RATIO)))));
if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes < F16(0.))) {
params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.);
}
if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes >
params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes)) {
params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.);
}
}
static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw,
fix16_t voc_index_from_prior) {
fix16_t delta_sgp;
fix16_t c;
fix16_t additional_scaling;
if ((!params->m_Mean_Variance_Estimator_Initialized)) {
params->m_Mean_Variance_Estimator_Initialized = true;
params->m_Mean_Variance_Estimator_Sraw_Offset = sraw;
params->m_Mean_Variance_Estimator_Mean = F16(0.);
} else {
if (((params->m_Mean_Variance_Estimator_Mean >= F16(100.)) ||
(params->m_Mean_Variance_Estimator_Mean <= F16(-100.)))) {
params->m_Mean_Variance_Estimator_Sraw_Offset =
(params->m_Mean_Variance_Estimator_Sraw_Offset + params->m_Mean_Variance_Estimator_Mean);
params->m_Mean_Variance_Estimator_Mean = F16(0.);
}
sraw = (sraw - params->m_Mean_Variance_Estimator_Sraw_Offset);
voc_algorithm_mean_variance_estimator_calculate_gamma(params, voc_index_from_prior);
delta_sgp = (fix16_div((sraw - params->m_Mean_Variance_Estimator_Mean),
F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING)));
if ((delta_sgp < F16(0.))) {
c = (params->m_Mean_Variance_Estimator_Std - delta_sgp);
} else {
c = (params->m_Mean_Variance_Estimator_Std + delta_sgp);
}
additional_scaling = F16(1.);
if ((c > F16(1440.))) {
additional_scaling = F16(4.);
}
params->m_Mean_Variance_Estimator_Std = (fix16_mul(
fix16_sqrt((fix16_mul(additional_scaling, (F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING) -
params->m_Mean_Variance_Estimator_Gamma_Variance)))),
fix16_sqrt(((fix16_mul(params->m_Mean_Variance_Estimator_Std,
(fix16_div(params->m_Mean_Variance_Estimator_Std,
(fix16_mul(F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING),
additional_scaling)))))) +
(fix16_mul((fix16_div((fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Variance, delta_sgp)),
additional_scaling)),
delta_sgp))))));
params->m_Mean_Variance_Estimator_Mean =
(params->m_Mean_Variance_Estimator_Mean + (fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Mean, delta_sgp)));
}
}
static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params) {
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(0.), F16(0.), F16(0.));
}
static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l,
fix16_t x0, fix16_t k) {
params->m_Mean_Variance_Estimator_Sigmoid_L = l;
params->m_Mean_Variance_Estimator_Sigmoid_K = k;
params->m_Mean_Variance_Estimator_Sigmoid_X0 = x0;
}
static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample) {
fix16_t x;
x = (fix16_mul(params->m_Mean_Variance_Estimator_Sigmoid_K, (sample - params->m_Mean_Variance_Estimator_Sigmoid_X0)));
if ((x < F16(-50.))) {
return params->m_Mean_Variance_Estimator_Sigmoid_L;
} else if ((x > F16(50.))) {
return F16(0.);
} else {
return (fix16_div(params->m_Mean_Variance_Estimator_Sigmoid_L, (F16(1.) + fix16_exp(x))));
}
}
static void voc_algorithm_mox_model_init(VocAlgorithmParams *params) {
voc_algorithm_mox_model_set_parameters(params, F16(1.), F16(0.));
}
static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean) {
params->m_Mox_Model_Sraw_Std = sraw_std;
params->m_Mox_Model_Sraw_Mean = sraw_mean;
}
static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw) {
return (fix16_mul((fix16_div((sraw - params->m_Mox_Model_Sraw_Mean),
(-(params->m_Mox_Model_Sraw_Std + F16(VOC_ALGORITHM_SRAW_STD_BONUS))))),
F16(VOC_ALGORITHM_VOC_INDEX_GAIN)));
}
static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params) {
voc_algorithm_sigmoid_scaled_set_parameters(params, F16(0.));
}
static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset) {
params->m_Sigmoid_Scaled_Offset = offset;
}
static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample) {
fix16_t x;
fix16_t shift;
x = (fix16_mul(F16(VOC_ALGORITHM_SIGMOID_K), (sample - F16(VOC_ALGORITHM_SIGMOID_X0))));
if ((x < F16(-50.))) {
return F16(VOC_ALGORITHM_SIGMOID_L);
} else if ((x > F16(50.))) {
return F16(0.);
} else {
if ((sample >= F16(0.))) {
shift =
(fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) - (fix16_mul(F16(5.), params->m_Sigmoid_Scaled_Offset))), F16(4.)));
return ((fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) + shift), (F16(1.) + fix16_exp(x)))) - shift);
} else {
return (fix16_mul((fix16_div(params->m_Sigmoid_Scaled_Offset, F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT))),
(fix16_div(F16(VOC_ALGORITHM_SIGMOID_L), (F16(1.) + fix16_exp(x))))));
}
}
}
static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params) {
voc_algorithm_adaptive_lowpass_set_parameters(params);
}
static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params) {
params->m_Adaptive_Lowpass_A1 =
F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_FAST + VOC_ALGORITHM_SAMPLING_INTERVAL)));
params->m_Adaptive_Lowpass_A2 =
F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_SLOW + VOC_ALGORITHM_SAMPLING_INTERVAL)));
params->m_Adaptive_Lowpass_Initialized = false;
}
static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample) {
fix16_t abs_delta;
fix16_t f1;
fix16_t tau_a;
fix16_t a3;
if ((!params->m_Adaptive_Lowpass_Initialized)) {
params->m_Adaptive_Lowpass_X1 = sample;
params->m_Adaptive_Lowpass_X2 = sample;
params->m_Adaptive_Lowpass_X3 = sample;
params->m_Adaptive_Lowpass_Initialized = true;
}
params->m_Adaptive_Lowpass_X1 =
((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A1), params->m_Adaptive_Lowpass_X1)) +
(fix16_mul(params->m_Adaptive_Lowpass_A1, sample)));
params->m_Adaptive_Lowpass_X2 =
((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A2), params->m_Adaptive_Lowpass_X2)) +
(fix16_mul(params->m_Adaptive_Lowpass_A2, sample)));
abs_delta = (params->m_Adaptive_Lowpass_X1 - params->m_Adaptive_Lowpass_X2);
if ((abs_delta < F16(0.))) {
abs_delta = (-abs_delta);
}
f1 = fix16_exp((fix16_mul(F16(VOC_ALGORITHM_LP_ALPHA), abs_delta)));
tau_a =
((fix16_mul(F16((VOC_ALGORITHM_LP_TAU_SLOW - VOC_ALGORITHM_LP_TAU_FAST)), f1)) + F16(VOC_ALGORITHM_LP_TAU_FAST));
a3 = (fix16_div(F16(VOC_ALGORITHM_SAMPLING_INTERVAL), (F16(VOC_ALGORITHM_SAMPLING_INTERVAL) + tau_a)));
params->m_Adaptive_Lowpass_X3 =
((fix16_mul((F16(1.) - a3), params->m_Adaptive_Lowpass_X3)) + (fix16_mul(a3, sample)));
return params->m_Adaptive_Lowpass_X3;
}
} // namespace sgp40
} // namespace esphome

View File

@@ -1,147 +0,0 @@
#pragma once
#include <cstdint>
namespace esphome {
namespace sgp40 {
/* The VOC code were originally created by
* https://github.com/Sensirion/embedded-sgp
* The fixed point arithmetic parts of this code were originally created by
* https://github.com/PetteriAimonen/libfixmath
*/
using fix16_t = int32_t;
#define F16(x) ((fix16_t)(((x) >= 0) ? ((x) *65536.0 + 0.5) : ((x) *65536.0 - 0.5)))
static const float VOC_ALGORITHM_SAMPLING_INTERVAL(1.);
static const float VOC_ALGORITHM_INITIAL_BLACKOUT(45.);
static const float VOC_ALGORITHM_VOC_INDEX_GAIN(230.);
static const float VOC_ALGORITHM_SRAW_STD_INITIAL(50.);
static const float VOC_ALGORITHM_SRAW_STD_BONUS(220.);
static const float VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS(12.);
static const float VOC_ALGORITHM_TAU_INITIAL_MEAN(20.);
static const float VOC_ALGORITHM_INIT_DURATION_MEAN((3600. * 0.75));
static const float VOC_ALGORITHM_INIT_TRANSITION_MEAN(0.01);
static const float VOC_ALGORITHM_TAU_INITIAL_VARIANCE(2500.);
static const float VOC_ALGORITHM_INIT_DURATION_VARIANCE((3600. * 1.45));
static const float VOC_ALGORITHM_INIT_TRANSITION_VARIANCE(0.01);
static const float VOC_ALGORITHM_GATING_THRESHOLD(340.);
static const float VOC_ALGORITHM_GATING_THRESHOLD_INITIAL(510.);
static const float VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION(0.09);
static const float VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES((60. * 3.));
static const float VOC_ALGORITHM_GATING_MAX_RATIO(0.3);
static const float VOC_ALGORITHM_SIGMOID_L(500.);
static const float VOC_ALGORITHM_SIGMOID_K(-0.0065);
static const float VOC_ALGORITHM_SIGMOID_X0(213.);
static const float VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT(100.);
static const float VOC_ALGORITHM_LP_TAU_FAST(20.0);
static const float VOC_ALGORITHM_LP_TAU_SLOW(500.0);
static const float VOC_ALGORITHM_LP_ALPHA(-0.2);
static const float VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA((3. * 3600.));
static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING(64.);
static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX(32767.);
/**
* Struct to hold all the states of the VOC algorithm.
*/
struct VocAlgorithmParams {
fix16_t mVoc_Index_Offset;
fix16_t mTau_Mean_Variance_Hours;
fix16_t mGating_Max_Duration_Minutes;
fix16_t mSraw_Std_Initial;
fix16_t mUptime;
fix16_t mSraw;
fix16_t mVoc_Index;
fix16_t m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes;
bool m_Mean_Variance_Estimator_Initialized;
fix16_t m_Mean_Variance_Estimator_Mean;
fix16_t m_Mean_Variance_Estimator_Sraw_Offset;
fix16_t m_Mean_Variance_Estimator_Std;
fix16_t m_Mean_Variance_Estimator_Gamma;
fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Mean;
fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Variance;
fix16_t m_Mean_Variance_Estimator_Gamma_Mean;
fix16_t m_Mean_Variance_Estimator_Gamma_Variance;
fix16_t m_Mean_Variance_Estimator_Uptime_Gamma;
fix16_t m_Mean_Variance_Estimator_Uptime_Gating;
fix16_t m_Mean_Variance_Estimator_Gating_Duration_Minutes;
fix16_t m_Mean_Variance_Estimator_Sigmoid_L;
fix16_t m_Mean_Variance_Estimator_Sigmoid_K;
fix16_t m_Mean_Variance_Estimator_Sigmoid_X0;
fix16_t m_Mox_Model_Sraw_Std;
fix16_t m_Mox_Model_Sraw_Mean;
fix16_t m_Sigmoid_Scaled_Offset;
fix16_t m_Adaptive_Lowpass_A1;
fix16_t m_Adaptive_Lowpass_A2;
bool m_Adaptive_Lowpass_Initialized;
fix16_t m_Adaptive_Lowpass_X1;
fix16_t m_Adaptive_Lowpass_X2;
fix16_t m_Adaptive_Lowpass_X3;
};
/**
* Initialize the VOC algorithm parameters. Call this once at the beginning or
* whenever the sensor stopped measurements.
* @param params Pointer to the VocAlgorithmParams struct
*/
void voc_algorithm_init(VocAlgorithmParams *params);
/**
* Get current algorithm states. Retrieved values can be used in
* voc_algorithm_set_states() to resume operation after a short interruption,
* skipping initial learning phase. This feature can only be used after at least
* 3 hours of continuous operation.
* @param params Pointer to the VocAlgorithmParams struct
* @param state0 State0 to be stored
* @param state1 State1 to be stored
*/
void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1);
/**
* Set previously retrieved algorithm states to resume operation after a short
* interruption, skipping initial learning phase. This feature should not be
* used after inerruptions of more than 10 minutes. Call this once after
* voc_algorithm_init() and the optional voc_algorithm_set_tuning_parameters(), if
* desired. Otherwise, the algorithm will start with initial learning phase.
* @param params Pointer to the VocAlgorithmParams struct
* @param state0 State0 to be restored
* @param state1 State1 to be restored
*/
void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1);
/**
* Set parameters to customize the VOC algorithm. Call this once after
* voc_algorithm_init(), if desired. Otherwise, the default values will be used.
*
* @param params Pointer to the VocAlgorithmParams struct
* @param voc_index_offset VOC index representing typical (average)
* conditions. Range 1..250, default 100
* @param learning_time_hours Time constant of long-term estimator.
* Past events will be forgotten after about
* twice the learning time.
* Range 1..72 [hours], default 12 [hours]
* @param gating_max_duration_minutes Maximum duration of gating (freeze of
* estimator during high VOC index signal).
* 0 (no gating) or range 1..720 [minutes],
* default 180 [minutes]
* @param std_initial Initial estimate for standard deviation.
* Lower value boosts events during initial
* learning period, but may result in larger
* device-to-device variations.
* Range 10..500, default 50
*/
void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset,
int32_t learning_time_hours, int32_t gating_max_duration_minutes,
int32_t std_initial);
/**
* Calculate the VOC index value from the raw sensor value.
*
* @param params Pointer to the VocAlgorithmParams struct
* @param sraw Raw value from the SGP40 sensor
* @param voc_index Calculated VOC index value from the raw sensor value. Zero
* during initial blackout period and 1..500 afterwards
*/
void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index);
} // namespace sgp40
} // namespace esphome

View File

@@ -1,70 +1,8 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor, sensirion_common
from esphome.const import (
CONF_STORE_BASELINE,
CONF_TEMPERATURE_SOURCE,
ICON_RADIATOR,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
STATE_CLASS_MEASUREMENT,
)
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
CODEOWNERS = ["@SenexCrenshaw"]
sgp40_ns = cg.esphome_ns.namespace("sgp40")
SGP40Component = sgp40_ns.class_(
"SGP40Component",
sensor.Sensor,
cg.PollingComponent,
sensirion_common.SensirionI2CDevice,
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid(
"SGP40 is deprecated.\nPlease use the SGP4x platform instead.\nSGP4x supports both SPG40 and SGP41.\n"
" See https://esphome.io/components/sensor/sgp4x.html"
)
CONF_COMPENSATION = "compensation"
CONF_HUMIDITY_SOURCE = "humidity_source"
CONF_VOC_BASELINE = "voc_baseline"
CONFIG_SCHEMA = (
sensor.sensor_schema(
SGP40Component,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
cv.Optional(CONF_COMPENSATION): cv.Schema(
{
cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor),
cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor),
},
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x59))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_COMPENSATION in config:
compensation_config = config[CONF_COMPENSATION]
sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
cg.add(var.set_humidity_sensor(sens))
sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE]))
if CONF_VOC_BASELINE in config:
cg.add(var.set_voc_baseline(CONF_VOC_BASELINE))

View File

@@ -1,274 +0,0 @@
#include "sgp40.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cinttypes>
namespace esphome {
namespace sgp40 {
static const char *const TAG = "sgp40";
void SGP40Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up SGP40...");
// Serial Number identification
if (!this->write_command(SGP40_CMD_GET_SERIAL_ID)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
uint16_t raw_serial_number[3];
if (!this->read_data(raw_serial_number, 3)) {
this->mark_failed();
return;
}
this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) |
(uint64_t(raw_serial_number[2]));
ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_);
// Featureset identification for future use
if (!this->write_command(SGP40_CMD_GET_FEATURESET)) {
ESP_LOGD(TAG, "raw_featureset write_command_ failed");
this->mark_failed();
return;
}
uint16_t raw_featureset;
if (!this->read_data(raw_featureset)) {
ESP_LOGD(TAG, "raw_featureset read_data_ failed");
this->mark_failed();
return;
}
this->featureset_ = raw_featureset;
if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) {
ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF),
SGP40_FEATURESET);
this->mark_failed();
return;
}
ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF));
voc_algorithm_init(&this->voc_algorithm_params_);
if (this->store_baseline_) {
// Hash with compilation time
// This ensures the baseline storage is cleared after OTA
uint32_t hash = fnv1_hash(App.get_compilation_time());
this->pref_ = global_preferences->make_preference<SGP40Baselines>(hash, true);
if (this->pref_.load(&this->baselines_storage_)) {
this->state0_ = this->baselines_storage_.state0;
this->state1_ = this->baselines_storage_.state1;
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0,
baselines_storage_.state1);
}
// Initialize storage timestamp
this->seconds_since_last_store_ = 0;
if (this->baselines_storage_.state0 > 0 && this->baselines_storage_.state1 > 0) {
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0,
baselines_storage_.state1);
voc_algorithm_set_states(&this->voc_algorithm_params_, this->baselines_storage_.state0,
this->baselines_storage_.state1);
}
}
this->self_test_();
/* The official spec for this sensor at https://docs.rs-online.com/1956/A700000007055193.pdf
indicates this sensor should be driven at 1Hz. Comments from the developers at:
https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit
resilient to slight timing variations so the software timer should be accurate enough for
this.
This block starts sampling from the sensor at 1Hz, and is done seperately from the call
to the update method. This seperation is to support getting accurate measurements but
limit the amount of communication done over wifi for power consumption or to keep the
number of records reported from being overwhelming.
*/
ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler");
this->set_interval(1000, [this]() { this->update_voc_index(); });
}
void SGP40Component::self_test_() {
ESP_LOGD(TAG, "Self-test started");
if (!this->write_command(SGP40_CMD_SELF_TEST)) {
this->error_code_ = COMMUNICATION_FAILED;
ESP_LOGD(TAG, "Self-test communication failed");
this->mark_failed();
}
this->set_timeout(250, [this]() {
uint16_t reply;
if (!this->read_data(reply)) {
ESP_LOGD(TAG, "Self-test read_data_ failed");
this->mark_failed();
return;
}
if (reply == 0xD400) {
this->self_test_complete_ = true;
ESP_LOGD(TAG, "Self-test completed");
return;
}
ESP_LOGD(TAG, "Self-test failed");
this->mark_failed();
});
}
/**
* @brief Combined the measured gasses, temperature, and humidity
* to calculate the VOC Index
*
* @param temperature The measured temperature in degrees C
* @param humidity The measured relative humidity in % rH
* @return int32_t The VOC Index
*/
int32_t SGP40Component::measure_voc_index_() {
int32_t voc_index;
uint16_t sraw = measure_raw_();
if (sraw == UINT16_MAX)
return UINT16_MAX;
this->status_clear_warning();
voc_algorithm_process(&voc_algorithm_params_, sraw, &voc_index);
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
// much
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
voc_algorithm_get_states(&voc_algorithm_params_, &this->state0_, &this->state1_);
if ((uint32_t) abs(this->baselines_storage_.state0 - this->state0_) > MAXIMUM_STORAGE_DIFF ||
(uint32_t) abs(this->baselines_storage_.state1 - this->state1_) > MAXIMUM_STORAGE_DIFF) {
this->seconds_since_last_store_ = 0;
this->baselines_storage_.state0 = this->state0_;
this->baselines_storage_.state1 = this->state1_;
if (this->pref_.save(&this->baselines_storage_)) {
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->baselines_storage_.state0,
baselines_storage_.state1);
} else {
ESP_LOGW(TAG, "Could not store VOC baselines");
}
}
}
return voc_index;
}
/**
* @brief Return the raw gas measurement
*
* @param temperature The measured temperature in degrees C
* @param humidity The measured relative humidity in % rH
* @return uint16_t The current raw gas measurement
*/
uint16_t SGP40Component::measure_raw_() {
float humidity = NAN;
if (!this->self_test_complete_) {
ESP_LOGD(TAG, "Self-test not yet complete");
return UINT16_MAX;
}
if (this->humidity_sensor_ != nullptr) {
humidity = this->humidity_sensor_->state;
}
if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
humidity = 50;
}
float temperature = NAN;
if (this->temperature_sensor_ != nullptr) {
temperature = float(this->temperature_sensor_->state);
}
if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
temperature = 25;
}
uint16_t data[2];
uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100));
uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175);
// first paramater is the relative humidity ticks
data[0] = rhticks;
// second paramater is the temperature ticks
data[1] = tempticks;
if (!this->write_command(SGP40_CMD_MEASURE_RAW, data, 2)) {
this->status_set_warning();
ESP_LOGD(TAG, "write error (%d)", this->last_error_);
return false;
}
delay(30);
uint16_t raw_data;
if (!this->read_data(raw_data)) {
this->status_set_warning();
ESP_LOGD(TAG, "read_data_ error");
return UINT16_MAX;
}
return raw_data;
}
void SGP40Component::update_voc_index() {
this->seconds_since_last_store_ += 1;
this->voc_index_ = this->measure_voc_index_();
if (this->samples_read_ < this->samples_to_stabalize_) {
this->samples_read_++;
ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_,
this->samples_to_stabalize_, this->voc_index_);
return;
}
}
void SGP40Component::update() {
if (this->samples_read_ < this->samples_to_stabalize_) {
return;
}
if (this->voc_index_ != UINT16_MAX) {
this->status_clear_warning();
this->publish_state(this->voc_index_);
} else {
this->status_set_warning();
}
}
void SGP40Component::dump_config() {
ESP_LOGCONFIG(TAG, "SGP40:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_);
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
break;
default:
ESP_LOGW(TAG, "Unknown setup error!");
break;
}
} else {
ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_);
ESP_LOGCONFIG(TAG, " Minimum Samples: %f", VOC_ALGORITHM_INITIAL_BLACKOUT);
}
LOG_UPDATE_INTERVAL(this);
if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) {
ESP_LOGCONFIG(TAG, " Compensation:");
LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
} else {
ESP_LOGCONFIG(TAG, " Compensation: No source configured");
}
}
} // namespace sgp40
} // namespace esphome

View File

@@ -1,93 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensirion_common/i2c_sensirion.h"
#include "esphome/core/application.h"
#include "esphome/core/preferences.h"
#include "sensirion_voc_algorithm.h"
#include <cmath>
namespace esphome {
namespace sgp40 {
struct SGP40Baselines {
int32_t state0;
int32_t state1;
} PACKED; // NOLINT
// commands and constants
static const uint8_t SGP40_FEATURESET = 0x0020; ///< The required set for this library
static const uint8_t SGP40_CRC8_POLYNOMIAL = 0x31; ///< Seed for SGP40's CRC polynomial
static const uint8_t SGP40_CRC8_INIT = 0xFF; ///< Init value for CRC
static const uint8_t SGP40_WORD_LEN = 2; ///< 2 bytes per word
// Commands
static const uint16_t SGP40_CMD_GET_SERIAL_ID = 0x3682;
static const uint16_t SGP40_CMD_GET_FEATURESET = 0x202f;
static const uint16_t SGP40_CMD_SELF_TEST = 0x280e;
static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F;
// Shortest time interval of 3H for storing baseline values.
// Prevents wear of the flash because of too many write operations
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
// Store anyway if the baseline difference exceeds the max storage diff value
const uint32_t MAXIMUM_STORAGE_DIFF = 50;
class SGP40Component;
/// This class implements support for the Sensirion sgp40 i2c GAS (VOC) sensors.
class SGP40Component : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice {
public:
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void setup() override;
void update() override;
void update_voc_index();
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
protected:
/// Input sensor for humidity and temperature compensation.
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
int16_t sensirion_init_sensors_();
int16_t sgp40_probe_();
uint64_t serial_number_;
uint16_t featureset_;
int32_t measure_voc_index_();
uint8_t generate_crc_(const uint8_t *data, uint8_t datalen);
uint16_t measure_raw_();
ESPPreferenceObject pref_;
uint32_t seconds_since_last_store_;
SGP40Baselines baselines_storage_;
VocAlgorithmParams voc_algorithm_params_;
bool self_test_complete_;
bool store_baseline_;
int32_t state0_;
int32_t state1_;
int32_t voc_index_ = 0;
uint8_t samples_read_ = 0;
uint8_t samples_to_stabalize_ = static_cast<int8_t>(VOC_ALGORITHM_INITIAL_BLACKOUT) * 2;
/**
* @brief Request the sensor to perform a self-test, returning the result
*
* @return true: success false:failure
*/
void self_test_();
enum ErrorCode {
COMMUNICATION_FAILED,
MEASUREMENT_INIT_FAILED,
INVALID_ID,
UNSUPPORTED_ID,
UNKNOWN
} error_code_{UNKNOWN};
};
} // namespace sgp40
} // namespace esphome

View File

View File

@@ -0,0 +1,144 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor, sensirion_common
from esphome.const import (
CONF_ID,
CONF_STORE_BASELINE,
CONF_TEMPERATURE_SOURCE,
ICON_RADIATOR,
DEVICE_CLASS_NITROUS_OXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
STATE_CLASS_MEASUREMENT,
)
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
CODEOWNERS = ["@SenexCrenshaw", "@martgras"]
sgp4x_ns = cg.esphome_ns.namespace("sgp4x")
SGP4xComponent = sgp4x_ns.class_(
"SGP4xComponent",
sensor.Sensor,
cg.PollingComponent,
sensirion_common.SensirionI2CDevice,
)
CONF_ALGORITHM_TUNING = "algorithm_tuning"
CONF_COMPENSATION = "compensation"
CONF_GAIN_FACTOR = "gain_factor"
CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes"
CONF_HUMIDITY_SOURCE = "humidity_source"
CONF_INDEX_OFFSET = "index_offset"
CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours"
CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
CONF_NOX = "nox"
CONF_STD_INITIAL = "std_initial"
CONF_VOC = "voc"
CONF_VOC_BASELINE = "voc_baseline"
def validate_sensors(config):
if CONF_VOC not in config and CONF_NOX not in config:
raise cv.Invalid(
f"At least one sensor is required. Define {CONF_VOC} and/or {CONF_NOX}"
)
return config
GAS_SENSOR = cv.Schema(
{
cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema(
{
cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_,
cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_,
cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_,
cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_,
cv.Optional(CONF_STD_INITIAL, default=50): cv.int_,
cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_,
}
)
}
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SGP4xComponent),
cv.Optional(CONF_VOC): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
).extend(GAS_SENSOR),
cv.Optional(CONF_NOX): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_NITROUS_OXIDE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(GAS_SENSOR),
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
cv.Optional(CONF_COMPENSATION): cv.Schema(
{
cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor),
cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor),
},
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x59)),
validate_sensors,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_COMPENSATION in config:
compensation_config = config[CONF_COMPENSATION]
sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
cg.add(var.set_humidity_sensor(sens))
sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE]))
if CONF_VOC_BASELINE in config:
cg.add(var.set_voc_baseline(CONF_VOC_BASELINE))
if CONF_VOC in config:
sens = await sensor.new_sensor(config[CONF_VOC])
cg.add(var.set_voc_sensor(sens))
if CONF_ALGORITHM_TUNING in config[CONF_VOC]:
cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING]
cg.add(
var.set_voc_algorithm_tuning(
cfg[CONF_INDEX_OFFSET],
cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
cfg[CONF_LEARNING_TIME_GAIN_HOURS],
cfg[CONF_GATING_MAX_DURATION_MINUTES],
cfg[CONF_STD_INITIAL],
cfg[CONF_GAIN_FACTOR],
)
)
if CONF_NOX in config:
sens = await sensor.new_sensor(config[CONF_NOX])
cg.add(var.set_nox_sensor(sens))
if CONF_ALGORITHM_TUNING in config[CONF_NOX]:
cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING]
cg.add(
var.set_nox_algorithm_tuning(
cfg[CONF_INDEX_OFFSET],
cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
cfg[CONF_LEARNING_TIME_GAIN_HOURS],
cfg[CONF_GATING_MAX_DURATION_MINUTES],
cfg[CONF_GAIN_FACTOR],
)
)
cg.add_library(
None, None, "https://github.com/Sensirion/arduino-gas-index-algorithm.git"
)

View File

@@ -0,0 +1,343 @@
#include "sgp4x.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cinttypes>
namespace esphome {
namespace sgp4x {
static const char *const TAG = "sgp4x";
void SGP4xComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up SGP4x...");
// Serial Number identification
uint16_t raw_serial_number[3];
if (!this->get_register(SGP4X_CMD_GET_SERIAL_ID, raw_serial_number, 3, 1)) {
ESP_LOGE(TAG, "Failed to read serial number");
this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
this->mark_failed();
return;
}
this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) |
(uint64_t(raw_serial_number[2]));
ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_);
// Featureset identification for future use
uint16_t raw_featureset;
if (!this->get_register(SGP4X_CMD_GET_FEATURESET, raw_featureset, 1)) {
ESP_LOGD(TAG, "raw_featureset write_command_ failed");
this->mark_failed();
return;
}
this->featureset_ = raw_featureset;
if ((this->featureset_ & 0x1FF) == SGP40_FEATURESET) {
sgp_type_ = SGP40;
self_test_time_ = SPG40_SELFTEST_TIME;
measure_time_ = SGP40_MEASURE_TIME;
if (this->nox_sensor_) {
ESP_LOGE(TAG, "Measuring NOx requires a SGP41 sensor but a SGP40 sensor is detected");
// disable the sensor
this->nox_sensor_->set_disabled_by_default(true);
// make sure it's not visiable in HA
this->nox_sensor_->set_internal(true);
this->nox_sensor_->state = NAN;
// remove pointer to sensor
this->nox_sensor_ = nullptr;
}
} else {
if ((this->featureset_ & 0x1FF) == SGP41_FEATURESET) {
sgp_type_ = SGP41;
self_test_time_ = SPG41_SELFTEST_TIME;
measure_time_ = SGP41_MEASURE_TIME;
} else {
ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF),
SGP40_FEATURESET);
this->mark_failed();
return;
}
}
ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF));
if (this->store_baseline_) {
// Hash with compilation time
// This ensures the baseline storage is cleared after OTA
uint32_t hash = fnv1_hash(App.get_compilation_time());
this->pref_ = global_preferences->make_preference<SGP4xBaselines>(hash, true);
if (this->pref_.load(&this->voc_baselines_storage_)) {
this->voc_state0_ = this->voc_baselines_storage_.state0;
this->voc_state1_ = this->voc_baselines_storage_.state1;
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0,
voc_baselines_storage_.state1);
}
// Initialize storage timestamp
this->seconds_since_last_store_ = 0;
if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X",
this->voc_baselines_storage_.state0, voc_baselines_storage_.state1);
voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
}
}
if (this->voc_sensor_ && this->voc_tuning_params_.has_value()) {
voc_algorithm_.set_tuning_parameters(
voc_tuning_params_.value().index_offset, voc_tuning_params_.value().learning_time_offset_hours,
voc_tuning_params_.value().learning_time_gain_hours, voc_tuning_params_.value().gating_max_duration_minutes,
voc_tuning_params_.value().std_initial, voc_tuning_params_.value().gain_factor);
}
if (this->nox_sensor_ && this->nox_tuning_params_.has_value()) {
nox_algorithm_.set_tuning_parameters(
nox_tuning_params_.value().index_offset, nox_tuning_params_.value().learning_time_offset_hours,
nox_tuning_params_.value().learning_time_gain_hours, nox_tuning_params_.value().gating_max_duration_minutes,
nox_tuning_params_.value().std_initial, nox_tuning_params_.value().gain_factor);
}
this->self_test_();
/* The official spec for this sensor at
https://sensirion.com/media/documents/296373BB/6203C5DF/Sensirion_Gas_Sensors_Datasheet_SGP40.pdf indicates this
sensor should be driven at 1Hz. Comments from the developers at:
https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit resilient to slight
timing variations so the software timer should be accurate enough for this.
This block starts sampling from the sensor at 1Hz, and is done seperately from the call
to the update method. This seperation is to support getting accurate measurements but
limit the amount of communication done over wifi for power consumption or to keep the
number of records reported from being overwhelming.
*/
ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler");
this->set_interval(1000, [this]() { this->update_gas_indices(); });
}
void SGP4xComponent::self_test_() {
ESP_LOGD(TAG, "Self-test started");
if (!this->write_command(SGP4X_CMD_SELF_TEST)) {
this->error_code_ = COMMUNICATION_FAILED;
ESP_LOGD(TAG, "Self-test communication failed");
this->mark_failed();
}
this->set_timeout(self_test_time_, [this]() {
uint16_t reply;
if (!this->read_data(reply)) {
this->error_code_ = SELF_TEST_FAILED;
ESP_LOGD(TAG, "Self-test read_data_ failed");
this->mark_failed();
return;
}
if (reply == 0xD400) {
this->self_test_complete_ = true;
ESP_LOGD(TAG, "Self-test completed");
return;
} else {
this->error_code_ = SELF_TEST_FAILED;
ESP_LOGD(TAG, "Self-test failed 0x%X", reply);
return;
}
ESP_LOGD(TAG, "Self-test failed 0x%X", reply);
this->mark_failed();
});
}
/**
* @brief Combined the measured gasses, temperature, and humidity
* to calculate the VOC Index
*
* @param temperature The measured temperature in degrees C
* @param humidity The measured relative humidity in % rH
* @return int32_t The VOC Index
*/
bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) {
uint16_t voc_sraw;
uint16_t nox_sraw;
if (!measure_raw_(voc_sraw, nox_sraw))
return false;
this->status_clear_warning();
voc = voc_algorithm_.process(voc_sraw);
if (nox_sensor_) {
nox = nox_algorithm_.process(nox_sraw);
}
ESP_LOGV(TAG, "VOC = %d, NOx = %d", voc, nox);
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
// much
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
if ((uint32_t) abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF ||
(uint32_t) abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) {
this->seconds_since_last_store_ = 0;
this->voc_baselines_storage_.state0 = this->voc_state0_;
this->voc_baselines_storage_.state1 = this->voc_state1_;
if (this->pref_.save(&this->voc_baselines_storage_)) {
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0,
voc_baselines_storage_.state1);
} else {
ESP_LOGW(TAG, "Could not store VOC baselines");
}
}
}
return true;
}
/**
* @brief Return the raw gas measurement
*
* @param temperature The measured temperature in degrees C
* @param humidity The measured relative humidity in % rH
* @return uint16_t The current raw gas measurement
*/
bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) {
float humidity = NAN;
static uint32_t nox_conditioning_start = millis();
if (!this->self_test_complete_) {
ESP_LOGD(TAG, "Self-test not yet complete");
return false;
}
if (this->humidity_sensor_ != nullptr) {
humidity = this->humidity_sensor_->state;
}
if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
humidity = 50;
}
float temperature = NAN;
if (this->temperature_sensor_ != nullptr) {
temperature = float(this->temperature_sensor_->state);
}
if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
temperature = 25;
}
uint16_t command;
uint16_t data[2];
size_t response_words;
// Use SGP40 measure command if we don't care about NOx
if (nox_sensor_ == nullptr) {
command = SGP40_CMD_MEASURE_RAW;
response_words = 1;
} else {
// SGP41 sensor must use NOx conditioning command for the first 10 seconds
if (millis() - nox_conditioning_start < 10000) {
command = SGP41_CMD_NOX_CONDITIONING;
response_words = 1;
} else {
command = SGP41_CMD_MEASURE_RAW;
response_words = 2;
}
}
uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100));
uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175);
// first paramater are the relative humidity ticks
data[0] = rhticks;
// secomd paramater are the temperature ticks
data[1] = tempticks;
if (!this->write_command(command, data, 2)) {
this->status_set_warning();
ESP_LOGD(TAG, "write error (%d)", this->last_error_);
return false;
}
delay(measure_time_);
uint16_t raw_data[2];
raw_data[1] = 0;
if (!this->read_data(raw_data, response_words)) {
this->status_set_warning();
ESP_LOGD(TAG, "read error (%d)", this->last_error_);
return false;
}
voc_raw = raw_data[0];
nox_raw = raw_data[1]; // either 0 or the measured NOx ticks
return true;
}
void SGP4xComponent::update_gas_indices() {
if (!this->self_test_complete_)
return;
this->seconds_since_last_store_ += 1;
if (!this->measure_gas_indices_(this->voc_index_, this->nox_index_)) {
// Set values to UINT16_MAX to indicate failure
this->voc_index_ = this->nox_index_ = UINT16_MAX;
ESP_LOGE(TAG, "measure gas indices failed");
return;
}
if (this->samples_read_ < this->samples_to_stabilize_) {
this->samples_read_++;
ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_,
this->samples_to_stabilize_, this->voc_index_);
return;
}
}
void SGP4xComponent::update() {
if (this->samples_read_ < this->samples_to_stabilize_) {
return;
}
if (this->voc_sensor_) {
if (this->voc_index_ != UINT16_MAX) {
this->status_clear_warning();
this->voc_sensor_->publish_state(this->voc_index_);
} else {
this->status_set_warning();
}
}
if (this->nox_sensor_) {
if (this->nox_index_ != UINT16_MAX) {
this->status_clear_warning();
this->nox_sensor_->publish_state(this->nox_index_);
} else {
this->status_set_warning();
}
}
}
void SGP4xComponent::dump_config() {
ESP_LOGCONFIG(TAG, "SGP4x:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_);
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
break;
case SERIAL_NUMBER_IDENTIFICATION_FAILED:
ESP_LOGW(TAG, "Get Serial number failed.");
break;
case SELF_TEST_FAILED:
ESP_LOGW(TAG, "Self test failed.");
break;
default:
ESP_LOGW(TAG, "Unknown setup error!");
break;
}
} else {
ESP_LOGCONFIG(TAG, " Type: %s", sgp_type_ == SGP41 ? "SGP41" : "SPG40");
ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_);
ESP_LOGCONFIG(TAG, " Minimum Samples: %f", GasIndexAlgorithm_INITIAL_BLACKOUT);
}
LOG_UPDATE_INTERVAL(this);
if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) {
ESP_LOGCONFIG(TAG, " Compensation:");
LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
} else {
ESP_LOGCONFIG(TAG, " Compensation: No source configured");
}
LOG_SENSOR(" ", "VOC", this->voc_sensor_);
LOG_SENSOR(" ", "NOx", this->nox_sensor_);
}
} // namespace sgp4x
} // namespace esphome

View File

@@ -0,0 +1,142 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensirion_common/i2c_sensirion.h"
#include "esphome/core/application.h"
#include "esphome/core/preferences.h"
#include <VOCGasIndexAlgorithm.h>
#include <NOxGasIndexAlgorithm.h>
#include <cmath>
namespace esphome {
namespace sgp4x {
struct SGP4xBaselines {
int32_t state0;
int32_t state1;
} PACKED; // NOLINT
enum SgpType { SGP40, SGP41 };
struct GasTuning {
uint16_t index_offset;
uint16_t learning_time_offset_hours;
uint16_t learning_time_gain_hours;
uint16_t gating_max_duration_minutes;
uint16_t std_initial;
uint16_t gain_factor;
};
// commands and constants
static const uint8_t SGP40_FEATURESET = 0x0020; // can measure VOC
static const uint8_t SGP41_FEATURESET = 0x0040; // can measure VOC and NOX
// Commands
static const uint16_t SGP4X_CMD_GET_SERIAL_ID = 0x3682;
static const uint16_t SGP4X_CMD_GET_FEATURESET = 0x202f;
static const uint16_t SGP4X_CMD_SELF_TEST = 0x280e;
static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F;
static const uint16_t SGP41_CMD_MEASURE_RAW = 0x2619;
static const uint16_t SGP41_CMD_NOX_CONDITIONING = 0x2612;
static const uint8_t SGP41_SUBCMD_NOX_CONDITIONING = 0x12;
// Shortest time interval of 3H for storing baseline values.
// Prevents wear of the flash because of too many write operations
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
static const uint16_t SPG40_SELFTEST_TIME = 250; // 250 ms for self test
static const uint16_t SPG41_SELFTEST_TIME = 320; // 320 ms for self test
static const uint16_t SGP40_MEASURE_TIME = 30;
static const uint16_t SGP41_MEASURE_TIME = 55;
// Store anyway if the baseline difference exceeds the max storage diff value
const uint32_t MAXIMUM_STORAGE_DIFF = 50;
class SGP4xComponent;
/// This class implements support for the Sensirion sgp4x i2c GAS (VOC) sensors.
class SGP4xComponent : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice {
enum ErrorCode {
COMMUNICATION_FAILED,
MEASUREMENT_INIT_FAILED,
INVALID_ID,
UNSUPPORTED_ID,
SERIAL_NUMBER_IDENTIFICATION_FAILED,
SELF_TEST_FAILED,
UNKNOWN
} error_code_{UNKNOWN};
public:
// SGP4xComponent() {};
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void setup() override;
void update() override;
void update_gas_indices();
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; }
void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; }
void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t std_initial, uint16_t gain_factor) {
voc_tuning_params_.value().index_offset = index_offset;
voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
voc_tuning_params_.value().std_initial = std_initial;
voc_tuning_params_.value().gain_factor = gain_factor;
}
void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t gain_factor) {
nox_tuning_params_.value().index_offset = index_offset;
nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
nox_tuning_params_.value().std_initial = 50;
nox_tuning_params_.value().gain_factor = gain_factor;
}
protected:
void self_test_();
/// Input sensor for humidity and temperature compensation.
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
int16_t sensirion_init_sensors_();
bool measure_gas_indices_(int32_t &voc, int32_t &nox);
bool measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw);
SgpType sgp_type_{SGP40};
uint64_t serial_number_;
uint16_t featureset_;
bool self_test_complete_;
uint16_t self_test_time_;
sensor::Sensor *voc_sensor_{nullptr};
VOCGasIndexAlgorithm voc_algorithm_;
optional<GasTuning> voc_tuning_params_;
int32_t voc_state0_;
int32_t voc_state1_;
int32_t voc_index_ = 0;
sensor::Sensor *nox_sensor_{nullptr};
int32_t nox_index_ = 0;
NOxGasIndexAlgorithm nox_algorithm_;
optional<GasTuning> nox_tuning_params_;
uint16_t measure_time_;
uint8_t samples_read_ = 0;
uint8_t samples_to_stabilize_ = static_cast<int8_t>(GasIndexAlgorithm_INITIAL_BLACKOUT) * 2;
bool store_baseline_;
ESPPreferenceObject pref_;
uint32_t seconds_since_last_store_;
SGP4xBaselines voc_baselines_storage_;
};
} // namespace sgp4x
} // namespace esphome

View File

@@ -158,11 +158,8 @@ bool ShellyDimmer::upgrade_firmware_() {
ESP_LOGW(TAG, "Starting STM32 firmware upgrade");
this->reset_dfu_boot_();
// Could be constexpr in c++17
static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); };
// Cleanup with RAII
std::unique_ptr<stm32_t, decltype(CLOSE)> stm32{stm32_init(this, STREAM_SERIAL, 1), CLOSE};
auto stm32 = stm32_init(this, STREAM_SERIAL, 1);
if (!stm32) {
ESP_LOGW(TAG, "Failed to initialize STM32");
@@ -170,7 +167,7 @@ bool ShellyDimmer::upgrade_firmware_() {
}
// Erase STM32 flash.
if (stm32_erase_memory(stm32.get(), 0, STM32_MASS_ERASE) != STM32_ERR_OK) {
if (stm32_erase_memory(stm32, 0, STM32_MASS_ERASE) != STM32_ERR_OK) {
ESP_LOGW(TAG, "Failed to erase STM32 flash memory");
return false;
}
@@ -196,7 +193,7 @@ bool ShellyDimmer::upgrade_firmware_() {
std::memcpy(buffer, p, BUFFER_SIZE);
p += BUFFER_SIZE;
if (stm32_write_memory(stm32.get(), addr, buffer, len) != STM32_ERR_OK) {
if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) {
ESP_LOGW(TAG, "Failed to write to STM32 flash memory");
return false;
}

View File

@@ -117,7 +117,7 @@ namespace shelly_dimmer {
namespace {
int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) {
int flash_addr_to_page_ceil(const stm32_unique_ptr &stm, uint32_t addr) {
if (!(addr >= stm->dev->fl_start && addr <= stm->dev->fl_end))
return 0;
@@ -135,7 +135,7 @@ int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) {
return addr ? page + 1 : page;
}
stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) {
stm32_err_t stm32_get_ack_timeout(const stm32_unique_ptr &stm, uint32_t timeout) {
auto *stream = stm->stream;
uint8_t rxbyte;
@@ -168,9 +168,9 @@ stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) {
} while (true);
}
stm32_err_t stm32_get_ack(const stm32_t *stm) { return stm32_get_ack_timeout(stm, 0); }
stm32_err_t stm32_get_ack(const stm32_unique_ptr &stm) { return stm32_get_ack_timeout(stm, 0); }
stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, const uint32_t timeout) {
stm32_err_t stm32_send_command_timeout(const stm32_unique_ptr &stm, const uint8_t cmd, const uint32_t timeout) {
auto *const stream = stm->stream;
static constexpr auto BUFFER_SIZE = 2;
@@ -194,12 +194,12 @@ stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, co
return STM32_ERR_UNKNOWN;
}
stm32_err_t stm32_send_command(const stm32_t *stm, const uint8_t cmd) {
stm32_err_t stm32_send_command(const stm32_unique_ptr &stm, const uint8_t cmd) {
return stm32_send_command_timeout(stm, cmd, 0);
}
/* if we have lost sync, send a wrong command and expect a NACK */
stm32_err_t stm32_resync(const stm32_t *stm) {
stm32_err_t stm32_resync(const stm32_unique_ptr &stm) {
auto *const stream = stm->stream;
uint32_t t0 = millis();
auto t1 = t0;
@@ -238,7 +238,7 @@ stm32_err_t stm32_resync(const stm32_t *stm) {
*
* len is value of the first byte in the frame.
*/
stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t *const data, unsigned int len) {
stm32_err_t stm32_guess_len_cmd(const stm32_unique_ptr &stm, const uint8_t cmd, uint8_t *const data, unsigned int len) {
auto *const stream = stm->stream;
if (stm32_send_command(stm, cmd) != STM32_ERR_OK)
@@ -286,7 +286,7 @@ stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t *
* This function sends the init sequence and, in case of timeout, recovers
* the interface.
*/
stm32_err_t stm32_send_init_seq(const stm32_t *stm) {
stm32_err_t stm32_send_init_seq(const stm32_unique_ptr &stm) {
auto *const stream = stm->stream;
stream->write_array(&STM32_CMD_INIT, 1);
@@ -320,7 +320,7 @@ stm32_err_t stm32_send_init_seq(const stm32_t *stm) {
return STM32_ERR_UNKNOWN;
}
stm32_err_t stm32_mass_erase(const stm32_t *stm) {
stm32_err_t stm32_mass_erase(const stm32_unique_ptr &stm) {
auto *const stream = stm->stream;
if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) {
@@ -364,7 +364,7 @@ template<typename T> std::unique_ptr<T[], void (*)(T *memory)> malloc_array_raii
DELETOR};
}
stm32_err_t stm32_pages_erase(const stm32_t *stm, const uint32_t spage, const uint32_t pages) {
stm32_err_t stm32_pages_erase(const stm32_unique_ptr &stm, const uint32_t spage, const uint32_t pages) {
auto *const stream = stm->stream;
uint8_t cs = 0;
int i = 0;
@@ -474,6 +474,18 @@ template<size_t N> void populate_buffer_with_address(uint8_t (&buffer)[N], uint3
buffer[4] = static_cast<uint8_t>(buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3]);
}
template<typename T> stm32_unique_ptr make_stm32_with_deletor(T ptr) {
static const auto CLOSE = [](stm32_t *stm32) {
if (stm32) {
free(stm32->cmd); // NOLINT
}
free(stm32); // NOLINT
};
// Cleanup with RAII
return std::unique_ptr<stm32_t, decltype(CLOSE)>{ptr, CLOSE};
}
} // Anonymous namespace
} // namespace shelly_dimmer
@@ -485,48 +497,44 @@ namespace shelly_dimmer {
/* find newer command by higher code */
#define newer(prev, a) (((prev) == STM32_CMD_ERR) ? (a) : (((prev) > (a)) ? (prev) : (a)))
stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) {
stm32_unique_ptr stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) {
uint8_t buf[257];
// Could be constexpr in c++17
static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); };
// Cleanup with RAII
std::unique_ptr<stm32_t, decltype(CLOSE)> stm{static_cast<stm32_t *>(calloc(sizeof(stm32_t), 1)), // NOLINT
CLOSE};
auto stm = make_stm32_with_deletor(static_cast<stm32_t *>(calloc(sizeof(stm32_t), 1))); // NOLINT
if (!stm) {
return nullptr;
return make_stm32_with_deletor(nullptr);
}
stm->stream = stream;
stm->flags = flags;
stm->cmd = static_cast<stm32_cmd_t *>(malloc(sizeof(stm32_cmd_t))); // NOLINT
if (!stm->cmd) {
return nullptr;
return make_stm32_with_deletor(nullptr);
}
memset(stm->cmd, STM32_CMD_ERR, sizeof(stm32_cmd_t));
if ((stm->flags & STREAM_OPT_CMD_INIT) && init) {
if (stm32_send_init_seq(stm.get()) != STM32_ERR_OK)
return nullptr; // NOLINT
if (stm32_send_init_seq(stm) != STM32_ERR_OK)
return make_stm32_with_deletor(nullptr);
}
/* get the version and read protection status */
if (stm32_send_command(stm.get(), STM32_CMD_GVR) != STM32_ERR_OK) {
return nullptr; // NOLINT
if (stm32_send_command(stm, STM32_CMD_GVR) != STM32_ERR_OK) {
return make_stm32_with_deletor(nullptr);
}
/* From AN, only UART bootloader returns 3 bytes */
{
const auto len = (stm->flags & STREAM_OPT_GVR_ETX) ? 3 : 1;
if (!stream->read_array(buf, len))
return nullptr; // NOLINT
return make_stm32_with_deletor(nullptr);
stm->version = buf[0];
stm->option1 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[1] : 0;
stm->option2 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[2] : 0;
if (stm32_get_ack(stm.get()) != STM32_ERR_OK) {
return nullptr;
if (stm32_get_ack(stm) != STM32_ERR_OK) {
return make_stm32_with_deletor(nullptr);
}
}
@@ -544,8 +552,8 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in
return STM32_CMD_GET_LENGTH;
})();
if (stm32_guess_len_cmd(stm.get(), STM32_CMD_GET, buf, len) != STM32_ERR_OK)
return nullptr;
if (stm32_guess_len_cmd(stm, STM32_CMD_GET, buf, len) != STM32_ERR_OK)
return make_stm32_with_deletor(nullptr);
}
const auto stop = buf[0] + 1;
@@ -607,23 +615,23 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in
}
if (new_cmds)
ESP_LOGD(TAG, ")");
if (stm32_get_ack(stm.get()) != STM32_ERR_OK) {
return nullptr;
if (stm32_get_ack(stm) != STM32_ERR_OK) {
return make_stm32_with_deletor(nullptr);
}
if (stm->cmd->get == STM32_CMD_ERR || stm->cmd->gvr == STM32_CMD_ERR || stm->cmd->gid == STM32_CMD_ERR) {
ESP_LOGD(TAG, "Error: bootloader did not returned correct information from GET command");
return nullptr;
return make_stm32_with_deletor(nullptr);
}
/* get the device ID */
if (stm32_guess_len_cmd(stm.get(), stm->cmd->gid, buf, 1) != STM32_ERR_OK) {
return nullptr;
if (stm32_guess_len_cmd(stm, stm->cmd->gid, buf, 1) != STM32_ERR_OK) {
return make_stm32_with_deletor(nullptr);
}
const auto returned = buf[0] + 1;
if (returned < 2) {
ESP_LOGD(TAG, "Only %d bytes sent in the PID, unknown/unsupported device", returned);
return nullptr;
return make_stm32_with_deletor(nullptr);
}
stm->pid = (buf[1] << 8) | buf[2];
if (returned > 2) {
@@ -631,8 +639,8 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in
for (auto i = 2; i <= returned; i++)
ESP_LOGD(TAG, " %02x", buf[i]);
}
if (stm32_get_ack(stm.get()) != STM32_ERR_OK) {
return nullptr;
if (stm32_get_ack(stm) != STM32_ERR_OK) {
return make_stm32_with_deletor(nullptr);
}
stm->dev = DEVICES;
@@ -641,21 +649,14 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in
if (!stm->dev->id) {
ESP_LOGD(TAG, "Unknown/unsupported device (Device ID: 0x%03x)", stm->pid);
return nullptr;
return make_stm32_with_deletor(nullptr);
}
// TODO: Would be much better if the unique_ptr was returned from this function
// Release ownership of unique_ptr
return stm.release(); // NOLINT
return stm;
}
void stm32_close(stm32_t *stm) {
if (stm)
free(stm->cmd); // NOLINT
free(stm); // NOLINT
}
stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_t *data, const unsigned int len) {
stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, const uint32_t address, uint8_t *data,
const unsigned int len) {
auto *const stream = stm->stream;
if (!len)
@@ -693,7 +694,8 @@ stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_
return STM32_ERR_OK;
}
stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, const unsigned int len) {
stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data,
const unsigned int len) {
auto *const stream = stm->stream;
if (!len)
@@ -753,7 +755,7 @@ stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8
return STM32_ERR_OK;
}
stm32_err_t stm32_wunprot_memory(const stm32_t *stm) {
stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm) {
if (stm->cmd->uw == STM32_CMD_ERR) {
ESP_LOGD(TAG, "Error: WRITE UNPROTECT command not implemented in bootloader.");
return STM32_ERR_NO_CMD;
@@ -766,7 +768,7 @@ stm32_err_t stm32_wunprot_memory(const stm32_t *stm) {
[]() { ESP_LOGD(TAG, "Error: Failed to WRITE UNPROTECT"); });
}
stm32_err_t stm32_wprot_memory(const stm32_t *stm) {
stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm) {
if (stm->cmd->wp == STM32_CMD_ERR) {
ESP_LOGD(TAG, "Error: WRITE PROTECT command not implemented in bootloader.");
return STM32_ERR_NO_CMD;
@@ -779,7 +781,7 @@ stm32_err_t stm32_wprot_memory(const stm32_t *stm) {
[]() { ESP_LOGD(TAG, "Error: Failed to WRITE PROTECT"); });
}
stm32_err_t stm32_runprot_memory(const stm32_t *stm) {
stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm) {
if (stm->cmd->ur == STM32_CMD_ERR) {
ESP_LOGD(TAG, "Error: READOUT UNPROTECT command not implemented in bootloader.");
return STM32_ERR_NO_CMD;
@@ -792,7 +794,7 @@ stm32_err_t stm32_runprot_memory(const stm32_t *stm) {
[]() { ESP_LOGD(TAG, "Error: Failed to READOUT UNPROTECT"); });
}
stm32_err_t stm32_readprot_memory(const stm32_t *stm) {
stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm) {
if (stm->cmd->rp == STM32_CMD_ERR) {
ESP_LOGD(TAG, "Error: READOUT PROTECT command not implemented in bootloader.");
return STM32_ERR_NO_CMD;
@@ -805,7 +807,7 @@ stm32_err_t stm32_readprot_memory(const stm32_t *stm) {
[]() { ESP_LOGD(TAG, "Error: Failed to READOUT PROTECT"); });
}
stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages) {
stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages) {
if (!pages || spage > STM32_MAX_PAGES || ((pages != STM32_MASS_ERASE) && ((spage + pages) > STM32_MAX_PAGES)))
return STM32_ERR_OK;
@@ -847,7 +849,7 @@ stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t page
return STM32_ERR_OK;
}
static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_address, const uint8_t *code,
static stm32_err_t stm32_run_raw_code(const stm32_unique_ptr &stm, uint32_t target_address, const uint8_t *code,
uint32_t code_size) {
static constexpr uint32_t BUFFER_SIZE = 256;
@@ -893,7 +895,7 @@ static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_addres
return stm32_go(stm, target_address);
}
stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) {
stm32_err_t stm32_go(const stm32_unique_ptr &stm, const uint32_t address) {
auto *const stream = stm->stream;
if (stm->cmd->go == STM32_CMD_ERR) {
@@ -916,7 +918,7 @@ stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) {
return STM32_ERR_OK;
}
stm32_err_t stm32_reset_device(const stm32_t *stm) {
stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm) {
const auto target_address = stm->dev->ram_start;
if (stm->dev->flags & F_OBLL) {
@@ -927,7 +929,8 @@ stm32_err_t stm32_reset_device(const stm32_t *stm) {
}
}
stm32_err_t stm32_crc_memory(const stm32_t *stm, const uint32_t address, const uint32_t length, uint32_t *const crc) {
stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, const uint32_t address, const uint32_t length,
uint32_t *const crc) {
static constexpr auto BUFFER_SIZE = 5;
auto *const stream = stm->stream;
@@ -1022,7 +1025,7 @@ uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len) {
return crc;
}
stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc) {
stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc) {
static constexpr uint32_t CRC_INIT_VALUE = 0xFFFFFFFF;
static constexpr uint32_t BUFFER_SIZE = 256;

View File

@@ -23,6 +23,7 @@
#ifdef USE_SHD_FIRMWARE_DATA
#include <cstdint>
#include <memory>
#include "esphome/components/uart/uart.h"
namespace esphome {
@@ -108,19 +109,20 @@ struct VarlenCmd {
uint8_t length;
};
stm32_t *stm32_init(uart::UARTDevice *stream, uint8_t flags, char init);
void stm32_close(stm32_t *stm);
stm32_err_t stm32_read_memory(const stm32_t *stm, uint32_t address, uint8_t *data, unsigned int len);
stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, unsigned int len);
stm32_err_t stm32_wunprot_memory(const stm32_t *stm);
stm32_err_t stm32_wprot_memory(const stm32_t *stm);
stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages);
stm32_err_t stm32_go(const stm32_t *stm, uint32_t address);
stm32_err_t stm32_reset_device(const stm32_t *stm);
stm32_err_t stm32_readprot_memory(const stm32_t *stm);
stm32_err_t stm32_runprot_memory(const stm32_t *stm);
stm32_err_t stm32_crc_memory(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc);
stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc);
using stm32_unique_ptr = std::unique_ptr<stm32_t, void (*)(stm32_t *)>;
stm32_unique_ptr stm32_init(uart::UARTDevice *stream, uint8_t flags, char init);
stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, uint32_t address, uint8_t *data, unsigned int len);
stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data, unsigned int len);
stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm);
stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm);
stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages);
stm32_err_t stm32_go(const stm32_unique_ptr &stm, uint32_t address);
stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm);
stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm);
stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm);
stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc);
stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc);
uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len);
} // namespace shelly_dimmer

View File

@@ -0,0 +1,38 @@
import re
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
from esphome.const import CONF_ID
CODEOWNERS = ["@alengwenus"]
DEPENDENCIES = ["uart"]
sml_ns = cg.esphome_ns.namespace("sml")
Sml = sml_ns.class_("Sml", cg.Component, uart.UARTDevice)
MULTI_CONF = True
CONF_SML_ID = "sml_id"
CONF_OBIS_CODE = "obis_code"
CONF_SERVER_ID = "server_id"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(Sml),
}
).extend(uart.UART_DEVICE_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
def obis_code(value):
value = cv.string(value)
match = re.match(r"^\d{1,3}-\d{1,3}:\d{1,3}\.\d{1,3}\.\d{1,3}$", value)
if match is None:
raise cv.Invalid(f"{value} is not a valid OBIS code")
return value

View File

@@ -0,0 +1,48 @@
#pragma once
#include <cstdint>
namespace esphome {
namespace sml {
enum SmlType : uint8_t {
SML_OCTET = 0,
SML_BOOL = 4,
SML_INT = 5,
SML_UINT = 6,
SML_LIST = 7,
SML_HEX = 10,
SML_UNDEFINED = 255
};
enum SmlMessageType : uint16_t { SML_PUBLIC_OPEN_RES = 0x0101, SML_GET_LIST_RES = 0x701 };
enum Crc16CheckResult : uint8_t { CHECK_CRC16_FAILED, CHECK_CRC16_X25_SUCCESS, CHECK_CRC16_KERMIT_SUCCESS };
// masks with two-bit mapping 0x1b -> 0b01; 0x01 -> 0b10; 0x1a -> 0b11
const uint16_t START_MASK = 0x55aa; // 0x1b 1b 1b 1b 1b 01 01 01 01
const uint16_t END_MASK = 0x0157; // 0x1b 1b 1b 1b 1a
const uint16_t CRC16_X25_TABLE[256] = {
0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5,
0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52,
0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3,
0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9,
0x2732, 0x36bb, 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e,
0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f,
0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862,
0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb,
0x4e64, 0x5fed, 0x6d76, 0x7cff, 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948,
0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226,
0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497,
0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704,
0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb,
0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c,
0x3de3, 0x2c6a, 0x1ef1, 0x0f78};
} // namespace sml
} // namespace esphome

View File

@@ -0,0 +1,30 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import CONF_ID
from .. import CONF_OBIS_CODE, CONF_SERVER_ID, CONF_SML_ID, Sml, obis_code, sml_ns
AUTO_LOAD = ["sml"]
SmlSensor = sml_ns.class_("SmlSensor", sensor.Sensor, cg.Component)
CONFIG_SCHEMA = sensor.sensor_schema().extend(
{
cv.GenerateID(): cv.declare_id(SmlSensor),
cv.GenerateID(CONF_SML_ID): cv.use_id(Sml),
cv.Required(CONF_OBIS_CODE): obis_code,
cv.Optional(CONF_SERVER_ID, default=""): cv.string,
}
)
async def to_code(config):
var = cg.new_Pvariable(
config[CONF_ID], config[CONF_SERVER_ID], config[CONF_OBIS_CODE]
)
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
sml = await cg.get_variable(config[CONF_SML_ID])
cg.add(sml.register_sml_listener(var))

View File

@@ -0,0 +1,41 @@
#include "esphome/core/log.h"
#include "sml_sensor.h"
#include "../sml_parser.h"
namespace esphome {
namespace sml {
static const char *const TAG = "sml_sensor";
SmlSensor::SmlSensor(std::string server_id, std::string obis_code)
: SmlListener(std::move(server_id), std::move(obis_code)) {}
void SmlSensor::publish_val(const ObisInfo &obis_info) {
switch (obis_info.value_type) {
case SML_INT: {
publish_state(bytes_to_int(obis_info.value));
break;
}
case SML_BOOL:
case SML_UINT: {
publish_state(bytes_to_uint(obis_info.value));
break;
}
case SML_OCTET: {
ESP_LOGW(TAG, "No number conversion for (%s) %s. Consider using SML TextSensor instead.",
bytes_repr(obis_info.server_id).c_str(), obis_info.code_repr().c_str());
break;
}
}
}
void SmlSensor::dump_config() {
LOG_SENSOR("", "SML", this);
if (!this->server_id.empty()) {
ESP_LOGCONFIG(TAG, " Server ID: %s", this->server_id.c_str());
}
ESP_LOGCONFIG(TAG, " OBIS Code: %s", this->obis_code.c_str());
}
} // namespace sml
} // namespace esphome

View File

@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/sml/sml.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace sml {
class SmlSensor : public SmlListener, public sensor::Sensor, public Component {
public:
SmlSensor(std::string server_id, std::string obis_code);
void publish_val(const ObisInfo &obis_info) override;
void dump_config() override;
};
} // namespace sml
} // namespace esphome

View File

@@ -0,0 +1,146 @@
#include "sml.h"
#include "esphome/core/log.h"
#include "sml_parser.h"
namespace esphome {
namespace sml {
static const char *const TAG = "sml";
const char START_BYTES_DETECTED = 1;
const char END_BYTES_DETECTED = 2;
SmlListener::SmlListener(std::string server_id, std::string obis_code)
: server_id(std::move(server_id)), obis_code(std::move(obis_code)) {}
char Sml::check_start_end_bytes_(uint8_t byte) {
this->incoming_mask_ = (this->incoming_mask_ << 2) | get_code(byte);
if (this->incoming_mask_ == START_MASK)
return START_BYTES_DETECTED;
if ((this->incoming_mask_ >> 6) == END_MASK)
return END_BYTES_DETECTED;
return 0;
}
void Sml::loop() {
while (available()) {
const char c = read();
if (this->record_)
this->sml_data_.emplace_back(c);
switch (this->check_start_end_bytes_(c)) {
case START_BYTES_DETECTED: {
this->record_ = true;
this->sml_data_.clear();
break;
};
case END_BYTES_DETECTED: {
if (this->record_) {
this->record_ = false;
if (!check_sml_data(this->sml_data_))
break;
// remove footer bytes
this->sml_data_.resize(this->sml_data_.size() - 8);
this->process_sml_file_(this->sml_data_);
}
break;
};
};
}
}
void Sml::process_sml_file_(const bytes &sml_data) {
SmlFile sml_file = SmlFile(sml_data);
std::vector<ObisInfo> obis_info = sml_file.get_obis_info();
this->publish_obis_info_(obis_info);
this->log_obis_info_(obis_info);
}
void Sml::log_obis_info_(const std::vector<ObisInfo> &obis_info_vec) {
ESP_LOGD(TAG, "OBIS info:");
for (auto const &obis_info : obis_info_vec) {
std::string info;
info += " (" + bytes_repr(obis_info.server_id) + ") ";
info += obis_info.code_repr();
info += " [0x" + bytes_repr(obis_info.value) + "]";
ESP_LOGD(TAG, "%s", info.c_str());
}
}
void Sml::publish_obis_info_(const std::vector<ObisInfo> &obis_info_vec) {
for (auto const &obis_info : obis_info_vec) {
this->publish_value_(obis_info);
}
}
void Sml::publish_value_(const ObisInfo &obis_info) {
for (auto const &sml_listener : sml_listeners_) {
if ((!sml_listener->server_id.empty()) && (bytes_repr(obis_info.server_id) != sml_listener->server_id))
continue;
if (obis_info.code_repr() != sml_listener->obis_code)
continue;
sml_listener->publish_val(obis_info);
}
}
void Sml::dump_config() { ESP_LOGCONFIG(TAG, "SML:"); }
void Sml::register_sml_listener(SmlListener *listener) { sml_listeners_.emplace_back(listener); }
bool check_sml_data(const bytes &buffer) {
if (buffer.size() < 2) {
ESP_LOGW(TAG, "Checksum error in received SML data.");
return false;
}
uint16_t crc_received = (buffer.at(buffer.size() - 2) << 8) | buffer.at(buffer.size() - 1);
if (crc_received == calc_crc16_x25(buffer.begin(), buffer.end() - 2, 0x6e23)) {
ESP_LOGV(TAG, "Checksum verification successful with CRC16/X25.");
return true;
}
if (crc_received == calc_crc16_kermit(buffer.begin(), buffer.end() - 2, 0xed50)) {
ESP_LOGV(TAG, "Checksum verification successful with CRC16/KERMIT.");
return true;
}
ESP_LOGW(TAG, "Checksum error in received SML data.");
return false;
}
uint16_t calc_crc16_p1021(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum) {
for (auto it = begin; it != end; it++) {
crcsum = (crcsum >> 8) ^ CRC16_X25_TABLE[(crcsum & 0xff) ^ *it];
}
return crcsum;
}
uint16_t calc_crc16_x25(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum = 0) {
crcsum = calc_crc16_p1021(begin, end, crcsum ^ 0xffff) ^ 0xffff;
return (crcsum >> 8) | ((crcsum & 0xff) << 8);
}
uint16_t calc_crc16_kermit(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum = 0) {
return calc_crc16_p1021(begin, end, crcsum);
}
uint8_t get_code(uint8_t byte) {
switch (byte) {
case 0x1b:
return 1;
case 0x01:
return 2;
case 0x1a:
return 3;
default:
return 0;
}
}
} // namespace sml
} // namespace esphome

View File

@@ -0,0 +1,47 @@
#pragma once
#include <string>
#include <vector>
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "sml_parser.h"
namespace esphome {
namespace sml {
class SmlListener {
public:
std::string server_id;
std::string obis_code;
SmlListener(std::string server_id, std::string obis_code);
virtual void publish_val(const ObisInfo &obis_info){};
};
class Sml : public Component, public uart::UARTDevice {
public:
void register_sml_listener(SmlListener *listener);
void loop() override;
void dump_config() override;
std::vector<SmlListener *> sml_listeners_{};
protected:
void process_sml_file_(const bytes &sml_data);
void log_obis_info_(const std::vector<ObisInfo> &obis_info_vec);
void publish_obis_info_(const std::vector<ObisInfo> &obis_info_vec);
char check_start_end_bytes_(uint8_t byte);
void publish_value_(const ObisInfo &obis_info);
// Serial parser
bool record_ = false;
uint16_t incoming_mask_ = 0;
bytes sml_data_;
};
bool check_sml_data(const bytes &buffer);
uint16_t calc_crc16_p1021(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum);
uint16_t calc_crc16_x25(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum);
uint16_t calc_crc16_kermit(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum);
uint8_t get_code(uint8_t byte);
} // namespace sml
} // namespace esphome

View File

@@ -0,0 +1,131 @@
#include "esphome/core/helpers.h"
#include "constants.h"
#include "sml_parser.h"
namespace esphome {
namespace sml {
SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) {
// extract messages
this->pos_ = 0;
while (this->pos_ < this->buffer_.size()) {
if (this->buffer_[this->pos_] == 0x00)
break; // fill byte detected -> no more messages
SmlNode message = SmlNode();
if (!this->setup_node(&message))
break;
this->messages.emplace_back(message);
}
}
bool SmlFile::setup_node(SmlNode *node) {
uint8_t type = this->buffer_[this->pos_] >> 4; // type including overlength info
uint8_t length = this->buffer_[this->pos_] & 0x0f; // length including TL bytes
bool is_list = (type & 0x07) == SML_LIST;
bool has_extended_length = type & 0x08; // we have a long list/value (>15 entries)
uint8_t parse_length = length;
if (has_extended_length) {
length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f);
parse_length = length - 1;
this->pos_ += 1;
}
if (this->pos_ + parse_length >= this->buffer_.size())
return false;
node->type = type & 0x07;
node->nodes.clear();
node->value_bytes.clear();
if (this->buffer_[this->pos_] == 0x00) { // end of message
this->pos_ += 1;
} else if (is_list) { // list
this->pos_ += 1;
node->nodes.reserve(parse_length);
for (size_t i = 0; i != parse_length; i++) {
SmlNode child_node = SmlNode();
if (!this->setup_node(&child_node))
return false;
node->nodes.emplace_back(child_node);
}
} else { // value
node->value_bytes =
bytes(this->buffer_.begin() + this->pos_ + 1, this->buffer_.begin() + this->pos_ + parse_length);
this->pos_ += parse_length;
}
return true;
}
std::vector<ObisInfo> SmlFile::get_obis_info() {
std::vector<ObisInfo> obis_info;
for (auto const &message : messages) {
SmlNode message_body = message.nodes[3];
uint16_t message_type = bytes_to_uint(message_body.nodes[0].value_bytes);
if (message_type != SML_GET_LIST_RES)
continue;
SmlNode get_list_response = message_body.nodes[1];
bytes server_id = get_list_response.nodes[1].value_bytes;
SmlNode val_list = get_list_response.nodes[4];
for (auto const &val_list_entry : val_list.nodes) {
obis_info.emplace_back(server_id, val_list_entry);
}
}
return obis_info;
}
std::string bytes_repr(const bytes &buffer) {
std::string repr;
for (auto const value : buffer) {
repr += str_sprintf("%02x", value & 0xff);
}
return repr;
}
uint64_t bytes_to_uint(const bytes &buffer) {
uint64_t val = 0;
for (auto const value : buffer) {
val = (val << 8) + value;
}
return val;
}
int64_t bytes_to_int(const bytes &buffer) {
uint64_t tmp = bytes_to_uint(buffer);
int64_t val;
switch (buffer.size()) {
case 1: // int8
val = (int8_t) tmp;
break;
case 2: // int16
val = (int16_t) tmp;
break;
case 4: // int32
val = (int32_t) tmp;
break;
default: // int64
val = (int64_t) tmp;
}
return val;
}
std::string bytes_to_string(const bytes &buffer) { return std::string(buffer.begin(), buffer.end()); }
ObisInfo::ObisInfo(bytes server_id, SmlNode val_list_entry) : server_id(std::move(server_id)) {
this->code = val_list_entry.nodes[0].value_bytes;
this->status = val_list_entry.nodes[1].value_bytes;
this->unit = bytes_to_uint(val_list_entry.nodes[3].value_bytes);
this->scaler = bytes_to_int(val_list_entry.nodes[4].value_bytes);
SmlNode value_node = val_list_entry.nodes[5];
this->value = value_node.value_bytes;
this->value_type = value_node.type;
}
std::string ObisInfo::code_repr() const {
return str_sprintf("%d-%d:%d.%d.%d", this->code[0], this->code[1], this->code[2], this->code[3], this->code[4]);
}
} // namespace sml
} // namespace esphome

View File

@@ -0,0 +1,54 @@
#pragma once
#include <cstdint>
#include <cstdio>
#include <string>
#include <vector>
#include "constants.h"
namespace esphome {
namespace sml {
using bytes = std::vector<uint8_t>;
class SmlNode {
public:
uint8_t type;
bytes value_bytes;
std::vector<SmlNode> nodes;
};
class ObisInfo {
public:
ObisInfo(bytes server_id, SmlNode val_list_entry);
bytes server_id;
bytes code;
bytes status;
char unit;
char scaler;
bytes value;
uint16_t value_type;
std::string code_repr() const;
};
class SmlFile {
public:
SmlFile(bytes buffer);
bool setup_node(SmlNode *node);
std::vector<SmlNode> messages;
std::vector<ObisInfo> get_obis_info();
protected:
const bytes buffer_;
size_t pos_;
};
std::string bytes_repr(const bytes &buffer);
uint64_t bytes_to_uint(const bytes &buffer);
int64_t bytes_to_int(const bytes &buffer);
std::string bytes_to_string(const bytes &buffer);
} // namespace sml
} // namespace esphome

View File

@@ -0,0 +1,43 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor
from esphome.const import CONF_FORMAT, CONF_ID
from .. import CONF_OBIS_CODE, CONF_SERVER_ID, CONF_SML_ID, Sml, obis_code, sml_ns
AUTO_LOAD = ["sml"]
SmlType = sml_ns.enum("SmlType")
SML_TYPES = {
"text": SmlType.SML_OCTET,
"bool": SmlType.SML_BOOL,
"int": SmlType.SML_INT,
"uint": SmlType.SML_UINT,
"hex": SmlType.SML_HEX,
"": SmlType.SML_UNDEFINED,
}
SmlTextSensor = sml_ns.class_("SmlTextSensor", text_sensor.TextSensor, cg.Component)
CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(SmlTextSensor),
cv.GenerateID(CONF_SML_ID): cv.use_id(Sml),
cv.Required(CONF_OBIS_CODE): obis_code,
cv.Optional(CONF_SERVER_ID, default=""): cv.string,
cv.Optional(CONF_FORMAT, default=""): cv.enum(SML_TYPES, lower=True),
}
)
async def to_code(config):
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_SERVER_ID],
config[CONF_OBIS_CODE],
config[CONF_FORMAT],
)
await cg.register_component(var, config)
await text_sensor.register_text_sensor(var, config)
sml = await cg.get_variable(config[CONF_SML_ID])
cg.add(sml.register_sml_listener(var))

View File

@@ -0,0 +1,54 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "sml_text_sensor.h"
#include "../sml_parser.h"
namespace esphome {
namespace sml {
static const char *const TAG = "sml_text_sensor";
SmlTextSensor::SmlTextSensor(std::string server_id, std::string obis_code, SmlType format)
: SmlListener(std::move(server_id), std::move(obis_code)), format_(format) {}
void SmlTextSensor::publish_val(const ObisInfo &obis_info) {
uint8_t value_type;
if (this->format_ == SML_UNDEFINED) {
value_type = obis_info.value_type;
} else {
value_type = this->format_;
}
switch (value_type) {
case SML_HEX: {
publish_state("0x" + bytes_repr(obis_info.value));
break;
}
case SML_INT: {
publish_state(to_string(bytes_to_int(obis_info.value)));
break;
}
case SML_BOOL:
publish_state(bytes_to_uint(obis_info.value) ? "True" : "False");
break;
case SML_UINT: {
publish_state(to_string(bytes_to_uint(obis_info.value)));
break;
}
case SML_OCTET: {
publish_state(std::string(obis_info.value.begin(), obis_info.value.end()));
break;
}
}
}
void SmlTextSensor::dump_config() {
LOG_TEXT_SENSOR("", "SML", this);
if (!this->server_id.empty()) {
ESP_LOGCONFIG(TAG, " Server ID: %s", this->server_id.c_str());
}
ESP_LOGCONFIG(TAG, " OBIS Code: %s", this->obis_code.c_str());
}
} // namespace sml
} // namespace esphome

View File

@@ -0,0 +1,21 @@
#pragma once
#include "esphome/components/sml/sml.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "../constants.h"
namespace esphome {
namespace sml {
class SmlTextSensor : public SmlListener, public text_sensor::TextSensor, public Component {
public:
SmlTextSensor(std::string server_id, std::string obis_code, SmlType format);
void publish_val(const ObisInfo &obis_info) override;
void dump_config() override;
protected:
SmlType format_;
};
} // namespace sml
} // namespace esphome

View File

@@ -41,7 +41,6 @@
* O FF FF FF FF FF FF FF FF - Not used
* M 6C - CRC over bytes 2 to F (Addition)
\*********************************************************************************************/
#include <cmath>
#include "sonoff_d1.h"
namespace esphome {
@@ -263,7 +262,7 @@ void SonoffD1Output::write_state(light::LightState *state) {
state->current_values_as_brightness(&brightness);
// Convert ESPHome's brightness (0-1) to the device's internal brightness (0-100)
const uint8_t calculated_brightness = std::round(brightness * 100);
const uint8_t calculated_brightness = (uint8_t) roundf(brightness * 100);
if (calculated_brightness == 0) {
// if(binary) ESP_LOGD(TAG, "current_values_as_binary() returns true for zero brightness");

View File

@@ -1,5 +1,4 @@
#include "speed_fan.h"
#include "esphome/components/fan/fan_helpers.h"
#include "esphome/core/log.h"
namespace esphome {

View File

@@ -156,15 +156,17 @@ class SPIComponent : public Component {
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
uint8_t transfer_byte(uint8_t data) {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->miso_ != nullptr) {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
return this->hw_spi_->transfer(data);
} else {
return this->transfer_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, true, true>(data);
}
}
#endif // USE_SPI_ARDUINO_BACKEND
return this->transfer_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, true, true>(data);
#ifdef USE_SPI_ARDUINO_BACKEND
}
#endif // USE_SPI_ARDUINO_BACKEND
}
this->write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data);
return 0;
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "sps30.h"
namespace esphome {
namespace sps30 {
template<typename... Ts> class StartFanAction : public Action<Ts...> {
public:
explicit StartFanAction(SPS30Component *sps30) : sps30_(sps30) {}
void play(Ts... x) override { this->sps30_->start_fan_cleaning(); }
protected:
SPS30Component *sps30_;
};
} // namespace sps30
} // namespace esphome

View File

@@ -1,6 +1,8 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor, sensirion_common
from esphome import automation
from esphome.automation import maybe_simple_id
from esphome.const import (
CONF_ID,
CONF_PM_1_0,
@@ -25,6 +27,7 @@ from esphome.const import (
ICON_RULER,
)
CODEOWNERS = ["@martgras"]
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
@@ -33,6 +36,11 @@ SPS30Component = sps30_ns.class_(
"SPS30Component", cg.PollingComponent, sensirion_common.SensirionI2CDevice
)
# Actions
StartFanAction = sps30_ns.class_("StartFanAction", automation.Action)
CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval"
CONFIG_SCHEMA = (
cv.Schema(
{
@@ -100,6 +108,7 @@ CONFIG_SCHEMA = (
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.update_interval,
}
)
.extend(cv.polling_component_schema("60s"))
@@ -151,3 +160,21 @@ async def to_code(config):
if CONF_PM_SIZE in config:
sens = await sensor.new_sensor(config[CONF_PM_SIZE])
cg.add(var.set_pm_size_sensor(sens))
if CONF_AUTO_CLEANING_INTERVAL in config:
cg.add(var.set_auto_cleaning_interval(config[CONF_AUTO_CLEANING_INTERVAL]))
SPS30_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(SPS30Component),
}
)
@automation.register_action(
"sps30.start_fan_autoclean", StartFanAction, SPS30_ACTION_SCHEMA
)
async def sps30_fan_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)

View File

@@ -1,5 +1,6 @@
#include "sps30.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "sps30.h"
namespace esphome {
namespace sps30 {
@@ -44,6 +45,22 @@ void SPS30Component::setup() {
this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF));
}
ESP_LOGD(TAG, " Serial Number: '%s'", this->serial_number_);
bool result;
if (this->fan_interval_.has_value()) {
// override default value
result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value());
} else {
result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS);
}
if (result) {
delay(20);
uint16_t secs[2];
if (this->read_data(secs, 2)) {
fan_interval_ = secs[0] << 16 | secs[1];
}
}
this->status_clear_warning();
this->skipped_data_read_cycles_ = 0;
this->start_continuous_measurement_();
@@ -206,5 +223,16 @@ bool SPS30Component::start_continuous_measurement_() {
return true;
}
bool SPS30Component::start_fan_cleaning() {
if (!write_command(SPS30_CMD_START_FAN_CLEANING)) {
this->status_set_warning();
ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_);
return false;
} else {
ESP_LOGD(TAG, "Fan auto clean started");
}
return true;
}
} // namespace sps30
} // namespace esphome

View File

@@ -22,12 +22,14 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri
void set_pmc_10_0_sensor(sensor::Sensor *pmc_10_0) { pmc_10_0_sensor_ = pmc_10_0; }
void set_pm_size_sensor(sensor::Sensor *pm_size) { pm_size_sensor_ = pm_size; }
void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { fan_interval_ = auto_cleaning_interval; }
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
bool start_fan_cleaning();
protected:
char serial_number_[17] = {0}; /// Terminating NULL character
uint16_t raw_firmware_version_;
@@ -54,6 +56,7 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri
sensor::Sensor *pmc_4_0_sensor_{nullptr};
sensor::Sensor *pmc_10_0_sensor_{nullptr};
sensor::Sensor *pm_size_sensor_{nullptr};
optional<uint32_t> fan_interval_;
};
} // namespace sps30

View File

@@ -48,6 +48,8 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer {
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
protected:
virtual void command(uint8_t value) = 0;
virtual void write_display_data() = 0;

View File

@@ -30,6 +30,8 @@ class SSD1322 : public PollingComponent, public display::DisplayBuffer {
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_GRAYSCALE; }
protected:
virtual void command(uint8_t value) = 0;
virtual void data(uint8_t value) = 0;

View File

@@ -35,6 +35,8 @@ class SSD1325 : public PollingComponent, public display::DisplayBuffer {
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
protected:
virtual void command(uint8_t value) = 0;
virtual void write_display_data() = 0;

View File

@@ -30,6 +30,8 @@ class SSD1327 : public PollingComponent, public display::DisplayBuffer {
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_GRAYSCALE; }
protected:
virtual void command(uint8_t value) = 0;
virtual void write_display_data() = 0;

View File

@@ -25,6 +25,8 @@ class SSD1331 : public PollingComponent, public display::DisplayBuffer {
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
protected:
virtual void command(uint8_t value) = 0;
virtual void write_display_data() = 0;

View File

@@ -31,6 +31,8 @@ class SSD1351 : public PollingComponent, public display::DisplayBuffer {
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
protected:
virtual void command(uint8_t value) = 0;
virtual void data(uint8_t value) = 0;

View File

@@ -53,6 +53,8 @@ class ST7735 : public PollingComponent,
void set_dc_pin(GPIOPin *value) { dc_pin_ = value; }
size_t get_buffer_length();
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
protected:
void sendcommand_(uint8_t cmd, const uint8_t *data_bytes, uint8_t num_data_bytes);
void senddata_(const uint8_t *data_bytes, uint8_t num_data_bytes);

View File

@@ -126,6 +126,8 @@ class ST7789V : public PollingComponent,
void write_display_data();
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
protected:
GPIOPin *dc_pin_;
GPIOPin *reset_pin_{nullptr};

View File

@@ -29,6 +29,8 @@ class ST7920 : public PollingComponent,
void fill(Color color) override;
void write_display_data();
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
int get_height_internal() override;

View File

@@ -48,7 +48,7 @@ VARIABLE_PROG = re.compile(
)
def _expand_substitutions(substitutions, value, path):
def _expand_substitutions(substitutions, value, path, ignore_missing):
if "$" not in value:
return value
@@ -66,13 +66,14 @@ def _expand_substitutions(substitutions, value, path):
if name.startswith("{") and name.endswith("}"):
name = name[1:-1]
if name not in substitutions:
_LOGGER.warning(
"Found '%s' (see %s) which looks like a substitution, but '%s' was "
"not declared",
orig_value,
"->".join(str(x) for x in path),
name,
)
if not ignore_missing:
_LOGGER.warning(
"Found '%s' (see %s) which looks like a substitution, but '%s' was "
"not declared",
orig_value,
"->".join(str(x) for x in path),
name,
)
i = j
continue
@@ -92,37 +93,37 @@ def _expand_substitutions(substitutions, value, path):
return value
def _substitute_item(substitutions, item, path):
def _substitute_item(substitutions, item, path, ignore_missing):
if isinstance(item, list):
for i, it in enumerate(item):
sub = _substitute_item(substitutions, it, path + [i])
sub = _substitute_item(substitutions, it, path + [i], ignore_missing)
if sub is not None:
item[i] = sub
elif isinstance(item, dict):
replace_keys = []
for k, v in item.items():
if path or k != CONF_SUBSTITUTIONS:
sub = _substitute_item(substitutions, k, path + [k])
sub = _substitute_item(substitutions, k, path + [k], ignore_missing)
if sub is not None:
replace_keys.append((k, sub))
sub = _substitute_item(substitutions, v, path + [k])
sub = _substitute_item(substitutions, v, path + [k], ignore_missing)
if sub is not None:
item[k] = sub
for old, new in replace_keys:
item[new] = merge_config(item.get(old), item.get(new))
del item[old]
elif isinstance(item, str):
sub = _expand_substitutions(substitutions, item, path)
sub = _expand_substitutions(substitutions, item, path, ignore_missing)
if sub != item:
return sub
elif isinstance(item, core.Lambda):
sub = _expand_substitutions(substitutions, item.value, path)
sub = _expand_substitutions(substitutions, item.value, path, ignore_missing)
if sub != item:
item.value = sub
return None
def do_substitution_pass(config, command_line_substitutions):
def do_substitution_pass(config, command_line_substitutions, ignore_missing=False):
if CONF_SUBSTITUTIONS not in config and not command_line_substitutions:
return
@@ -151,4 +152,4 @@ def do_substitution_pass(config, command_line_substitutions):
config[CONF_SUBSTITUTIONS] = substitutions
# Move substitutions to the first place to replace substitutions in them correctly
config.move_to_end(CONF_SUBSTITUTIONS, False)
_substitute_item(substitutions, config, [])
_substitute_item(substitutions, config, [], ignore_missing)

View File

@@ -43,7 +43,6 @@ void Switch::add_on_state_callback(std::function<void(bool)> &&callback) {
this->state_callback_.add(std::move(callback));
}
void Switch::set_inverted(bool inverted) { this->inverted_ = inverted; }
uint32_t Switch::hash_base() { return 3129890955UL; }
bool Switch::is_inverted() const { return this->inverted_; }
std::string Switch::get_device_class() {

View File

@@ -107,8 +107,6 @@ class Switch : public EntityBase {
*/
virtual void write_state(bool state) = 0;
uint32_t hash_base() override;
CallbackManager<void(bool)> state_callback_{};
bool inverted_{false};
Deduplicator<bool> publish_dedup_;

View File

@@ -41,7 +41,7 @@ i2c::ErrorCode TCA9548AComponent::switch_to_channel(uint8_t channel) {
return i2c::ERROR_OK;
uint8_t channel_val = 1 << channel;
auto err = this->write_register(0x70, &channel_val, 1);
auto err = this->write(&channel_val, 1);
if (err == i2c::ERROR_OK) {
current_channel_ = channel;
}

View File

@@ -31,6 +31,7 @@ TCS34725Component = tcs34725_ns.class_(
TCS34725IntegrationTime = tcs34725_ns.enum("TCS34725IntegrationTime")
TCS34725_INTEGRATION_TIMES = {
"auto": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_AUTO,
"2.4ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_2_4MS,
"24ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_24MS,
"50ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_50MS,
@@ -88,7 +89,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_CLEAR_CHANNEL): color_channel_schema,
cv.Optional(CONF_ILLUMINANCE): illuminance_schema,
cv.Optional(CONF_COLOR_TEMPERATURE): color_temperature_schema,
cv.Optional(CONF_INTEGRATION_TIME, default="2.4ms"): cv.enum(
cv.Optional(CONF_INTEGRATION_TIME, default="auto"): cv.enum(
TCS34725_INTEGRATION_TIMES, lower=True
),
cv.Optional(CONF_GAIN, default="1X"): cv.enum(TCS34725_GAINS, upper=True),

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