1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-01 07:31:51 +00:00

Compare commits

...

239 Commits

Author SHA1 Message Date
Jesse Hills
a67d58948d Merge pull request #3714 from esphome/bump-2022.8.0b2
2022.8.0b2
2022-08-15 09:08:44 +12:00
Jesse Hills
84c051d097 Bump version to 2022.8.0b2 2022-08-15 08:23:35 +12:00
NP v/d Spek
b918abfd54 add gradient color V2.0 (#3709) 2022-08-15 08:23:35 +12:00
Peter Galantha
7f41b7cd93 Add state_class total (#3608)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-08-15 08:23:35 +12:00
Jesse Hills
4759b4fe2e Add vector include (#3707) 2022-08-15 08:23:35 +12:00
anatoly-savchenkov
e2c8e69d12 Improve Web view for Climate components (#3706) 2022-08-15 08:23:35 +12:00
Jesse Hills
917e8e155c Merge pull request #3705 from esphome/bump-2022.8.0b1
2022.8.0b1
2022-08-10 15:45:17 +12:00
Jesse Hills
a97e3d827d Bump version to 2022.8.0b1 2022-08-10 15:26:02 +12:00
RoboMagus
029014d9d6 Add priority to on_shutdown trigger (#3644) 2022-08-10 13:10:49 +12:00
dependabot[bot]
e4c2922536 Bump pylint from 2.14.4 to 2.14.5 (#3697)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-10 12:34:56 +12:00
dependabot[bot]
7133ae6aaa Bump flake8 from 4.0.1 to 5.0.4 (#3703)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-10 09:21:25 +12:00
dependabot[bot]
2f7f0ff3a1 Bump pytest-mock from 3.8.1 to 3.8.2 (#3622)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-10 08:03:42 +12:00
dependabot[bot]
df853bf61e Bump zeroconf from 0.38.7 to 0.39.0 (#3694)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-09 19:19:13 +12:00
dependabot[bot]
f0ac753f9b Bump pytest-asyncio from 0.18.3 to 0.19.0 (#3695)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-09 19:16:49 +12:00
Adrián Panella
4d56a975e6 Create feedback cover component (#3253) 2022-08-09 17:21:27 +12:00
dependabot[bot]
d56c53c848 Bump aioesphomeapi from 10.10.0 to 10.11.0 (#3669)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-09 13:53:30 +12:00
dependabot[bot]
f83b16320d Bump pyupgrade from 2.34.0 to 2.37.3 (#3670)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-08-09 13:53:18 +12:00
anatoly-savchenkov
ac10e27f08 [Nextion] Add on_page trigger (#3673) 2022-08-09 12:44:20 +12:00
NP v/d Spek
34df7a6072 add gradient color (#3687) 2022-08-09 12:29:41 +12:00
Keith Burzinski
e2cddf1005 Sprinkler controller component (#2249)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-08-09 12:19:42 +12:00
Ian Blais
ced423748e Adding support for Whynter ARC-14S/SH Air Conditioners (#3641) 2022-08-09 10:49:03 +12:00
davestubbs
77fb02729e Added support for setting the current animation frame (#3681) 2022-08-09 10:28:05 +12:00
Joe
fef39b9fbe Refactor BedJet climate into Hub component (#3522) 2022-08-09 10:14:37 +12:00
Jesse Hills
02810105fb Add helpers to switch python for schema and codegen (#3693)
* Add helpers to switch python for schema and codegen

* Add icon back
2022-08-07 22:01:17 -05:00
rbaron
baad92515b Introduces ble_client.ble_write Action (#3398) 2022-08-08 12:59:55 +12:00
dentra
a12c6b5f35 Fix panic abort when BLEClient reconnects (#3594) 2022-08-08 07:57:43 +12:00
Keith Burzinski
522646c64d Extend ST7789V component to support additional displays (#3651)
Co-authored-by: definitio <37266727+definitio@users.noreply.github.com>
2022-08-08 07:54:48 +12:00
anatoly-savchenkov
4791093e48 Add get_ap() method to WiFi (#3684) 2022-08-08 07:51:09 +12:00
Donovan Baarda
599a455150 Add a soft reset in setup() for bme280. (#3615) 2022-08-08 07:50:15 +12:00
Samuel Sieb
2deef16ebe fix sx1509 use of pullup and pulldown (#3689)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2022-08-08 07:39:41 +12:00
RoboMagus
989b7be99b Pass optional args to 'new_Pvariable' when creating new entities (#3690) 2022-08-08 07:31:50 +12:00
Derek M
cd473e1395 Adding option to report signal strength on pvvx_mithermometer (#3688)
Co-authored-by: doolbneerg <doolbneerg@outlook.com>
2022-08-08 07:30:30 +12:00
Jesse Hills
e246ebfb2e Remove old unused code in mcp23xxx_base (#3685) 2022-08-07 09:56:52 +12:00
Borys Pierov
8546ae56da Allow GPIO20 for ESP32 (#3680) 2022-08-04 07:30:19 +12:00
Sybren A. Stüvel
9e227b0192 Upgrade rweather/Crypto from 0.2.0 to 0.4.0 (#3593)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-08-03 07:58:03 +12:00
Samuel Sieb
ba7737e9f8 dalybms: support changing the address (#3676) 2022-08-03 07:48:02 +12:00
Jesse Hills
98aa3d51ed Show changes from python linting (#3677) 2022-08-03 07:27:45 +12:00
Samuel Sieb
e7cfb5492e Use correct struct members. (#3672) 2022-08-02 17:09:17 +12:00
Samuel Sieb
9ed136dc3a Use application/json instead of text/json (#3671) 2022-08-02 17:07:32 +12:00
Bryan Berg
9217216723 Add CO device class to binary_sensor (#3656) 2022-08-02 11:32:02 +12:00
RoboMagus
936c408a58 List webserver service on MDNS if enabled. (#3662) 2022-08-02 11:31:06 +12:00
Samuel Sieb
54427eac9a Update inkbird_ibsth1_mini.cpp (#3664) 2022-08-01 13:08:19 +12:00
Samuel Sieb
e809488cc0 Remove deprecated adc init call. (#3667) 2022-08-01 09:44:58 +12:00
DAVe3283
ed26c57d99 Rework NaN handling in sensor filters (#3610)
Co-authored-by: Otto Winter <otto@otto-winter.com>
2022-07-28 16:39:11 +12:00
piechade
2a49811f6e Add support for SMT100 Soil Moisture Sensor (#3654)
Co-authored-by: Dennis Piecha <d.piecha@ymail.com>
2022-07-28 11:22:49 +12:00
Peter Galantha
c95acd2568 update packages: nginx-light-1.18.0-6.1+deb11u2 nano-5.4-2+deb11u1 (#3631) 2022-07-25 16:04:21 +12:00
Samuel Sieb
6a4e0cf667 add option to publish initial state of binary sensors (#3636)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2022-07-25 15:55:32 +12:00
RoboMagus
04f4dd8a22 Add 'set_total_pulses' action to 'pulse_counter' sensor. (#3640) 2022-07-25 14:20:15 +12:00
Javier Peletier
f33d829ce9 FIX: NVS preferences being written even if unchanged (#3647) 2022-07-25 10:10:37 +12:00
RoboMagus
578671ea94 Fix configuration constants in core. (#3652) 2022-07-25 08:23:55 +12:00
Javier Peletier
d10300c330 modbus: fix queue deduplicator erasing custom commands (#3650) 2022-07-25 08:06:18 +12:00
Andreas Hergert
e0555e140f Improvement pipsolar crc (#3316)
Co-authored-by: Andreas <andreas.hergert@otrs.com>
2022-07-07 12:54:19 +12:00
Luca Adrian L
093989406f Added more sensor device classes (#3624) 2022-07-07 11:32:38 +12:00
dab0g
cdb16f08f6 Update dfplayer condition function name (#3619) 2022-07-04 09:22:04 +12:00
dependabot[bot]
53139c293b Bump pytest-mock from 3.7.0 to 3.8.1 (#3616) 2022-07-01 14:54:37 +02:00
dependabot[bot]
6a8bdcc315 Bump pylint from 2.14.3 to 2.14.4 (#3617) 2022-07-01 14:54:13 +02:00
dependabot[bot]
fe535939a3 Bump colorama from 0.4.4 to 0.4.5 (#3614) 2022-06-30 12:42:35 +02:00
dependabot[bot]
09e6c11d73 Bump black from 22.3.0 to 22.6.0 (#3613) 2022-06-30 12:42:28 +02:00
dependabot[bot]
8112bdfaa8 Bump pyupgrade from 2.32.1 to 2.34.0 (#3591)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Otto Winter <otto@otto-winter.com>
2022-06-30 11:59:53 +02:00
marshn
f7db9aaa9f Fix slow_pwm output glitch (#3601) 2022-06-30 11:28:19 +02:00
Javier Peletier
435f972357 fix EDict to dict mapping in helpers.py (#3599) 2022-06-30 11:24:24 +02:00
dependabot[bot]
f82b46c16b Bump zeroconf from 0.38.4 to 0.38.7 (#3562)
Bumps [zeroconf](https://github.com/jstasiak/python-zeroconf) from 0.38.4 to 0.38.7.
- [Release notes](https://github.com/jstasiak/python-zeroconf/releases)
- [Commits](https://github.com/jstasiak/python-zeroconf/compare/0.38.4...0.38.7)

---
updated-dependencies:
- dependency-name: zeroconf
  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-06-30 11:06:22 +02:00
Jesse Hills
bca96f91b2 Remove min_save_interval from intergration and total_daily_energy (#3498) 2022-06-30 16:47:56 +12:00
Benoit3
6f83a49c63 #3358 Correct temperature validity detection issue in some conditions of pu… (#3545)
Co-authored-by: Otto Winter <otto@otto-winter.com>
2022-06-29 21:30:44 +02:00
André Klitzing
72cce391ab Fix some typos found by codespell (#3598) 2022-06-27 18:02:46 -03:00
Jesse Hills
28d2949ebe Add github-actions to dependabot (#3595) 2022-06-23 07:51:05 +12:00
Sven Serlier
c4a0015997 Update actions (#3592)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-06-23 07:50:55 +12:00
Jesse Hills
f564be6aea Do two substitutions passes to allow substitutions inside substitutions (#3583) 2022-06-22 08:21:32 +12:00
dependabot[bot]
988f15e6af Bump aioesphomeapi from 10.8.2 to 10.10.0 (#3590)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-21 17:19:41 +12:00
Zimmermann Zsolt
37b6d442bd Correcting ESP32 flash save/load key calculation algorithm (#3416)
Co-authored-by: Otto Winter <otto@otto-winter.com>
Co-authored-by: Dr. Zimmermann Zsolt <cina@storage1.cina.hu>
2022-06-21 17:17:51 +12:00
Nick B
fb2467f6f0 DAC7678 support (#3441)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-06-21 17:12:15 +12:00
dependabot[bot]
29045b0435 Bump pylint from 2.13.9 to 2.14.3 (#3589)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-06-21 16:27:33 +12:00
Jesse Hills
311a48c64e Bump platformio to 6.0.2 (#3566) 2022-06-21 11:53:51 +12:00
Mateus Demboski
01b3815f27 Map LOLIN C3 mini board pins (#3587) 2022-06-21 11:33:20 +12:00
Joe
b0d1c801bd Fix 2 small issues in BLEClient (#3544) 2022-06-21 11:31:17 +12:00
RoboMagus
5aaac06f5b RestoringGlobalsComponent: Store value on shutdown (#3586) 2022-06-21 11:27:53 +12:00
RoboMagus
34adbf0588 Fix / Reverse order shutdown (#3585) 2022-06-21 11:26:34 +12:00
Jesse Hills
0a4213182e Merge branch 'release' into dev 2022-06-21 07:18:41 +12:00
Sergey Dudanov
b0c0258e70 Media Player: added play_media action (#3579)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-06-20 12:17:20 +12:00
kahrendt
8110e591d0 Fix wrong type for voc_state*_ in sgp4x component (#3581)
Co-authored-by: Martin <25747549+martgras@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-06-20 11:27:00 +12:00
ShellAddicted
fe05d7aec1 Fix: Make MQTT over TLS actually work (#3580) 2022-06-20 11:17:58 +12:00
lkomurcu
57f5884070 Move gas mbus config option being a define to being a build flag since its used in external libraries. (#3575) 2022-06-20 07:39:40 +12:00
Jesse Hills
f329c74a15 Merge pull request #3578 from esphome/bump-2022.6.1
2022.6.1
2022-06-18 22:31:45 +12:00
Jesse Hills
7c86f3fa9e Bump version to 2022.6.1 2022-06-18 17:25:52 +12:00
Sergey Dudanov
203b8b01bf Media Player: added triggers (#3576) 2022-06-18 17:25:51 +12:00
Benjamin Klotz
8a1034a92f Bugfix for ExternalRAMAllocator copy constructor (#3571) 2022-06-18 17:25:51 +12:00
Jesse Hills
aa0c2dedd9 Setup the mute pin if configured (#3568) 2022-06-18 17:25:51 +12:00
Sergey Dudanov
d045908e05 Media Player: added triggers (#3576) 2022-06-18 17:21:42 +12:00
Guillermo Ruffino
f002a23d2d Language schema 202204 (#3492) 2022-06-17 13:46:20 +12:00
gazoodle
29d6d0a906 Fix modbus user-defined function handling (#3527) 2022-06-17 13:35:25 +12:00
Benjamin Klotz
c8b58b5c23 Bugfix for ExternalRAMAllocator copy constructor (#3571) 2022-06-17 13:32:43 +12:00
Jesse Hills
01bfafc5f1 Setup the mute pin if configured (#3568) 2022-06-17 13:30:21 +12:00
Jesse Hills
8c9948bb56 Merge pull request #3567 from esphome/bump-2022.6.0
2022.6.0
2022-06-16 12:59:26 +12:00
Jesse Hills
2d1abaa68e Bump version to 2022.6.0 2022-06-16 11:31:38 +12:00
Jesse Hills
664a3df0b4 Merge branch 'beta' into bump-2022.6.0 2022-06-16 11:31:38 +12:00
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
Martin
06440d0202 Bm3xx: Fix typo (#3559) 2022-06-14 20:38:09 +12:00
André Klitzing
0ecf9f4f2f Fix compilation with ESP32-S3 (#3543)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2022-06-14 20:36:38 +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
8998c5f6dd Implement media player volume actions (#3551) 2022-06-13 13:28:55 +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
RoboMagus
3a9ab50dd2 Refactor vl53l0x to remove code from header. (#3536) 2022-06-09 16:24:56 +12:00
Viktor Nagy
5abd91d6d5 Nextion brightness setting requires an assignment (#3533) 2022-06-09 16:20:05 +12:00
Samuel Sieb
c3da42516b publish fan speed count for discovery (#3537)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2022-06-09 15:27:04 +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
ec1fae6883 Bump version to 2022.7.0-dev 2022-06-08 22:46:20 +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
Jesse Hills
77f322166e Merge pull request #3499 from esphome/bump-2022.5.1
2022.5.1
2022-05-25 07:29:42 +12:00
Jesse Hills
f3f6e54818 Bump version to 2022.5.1 2022-05-24 21:56:18 +12:00
Martin
fb0fec1f25 esp32: fix NVS (#3497) 2022-05-24 21:56:18 +12:00
Jesse Hills
b66af9fb4d Add missing import to bedjet (#3490) 2022-05-24 21:56:18 +12:00
user897943
6617d576a7 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-24 21:56:18 +12: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
420dacb22d Merge pull request #3488 from esphome/bump-2022.5.0
2022.5.0
2022-05-18 16:56:16 +12:00
Jesse Hills
ae2f6ad4d1 Bump version to 2022.5.0 2022-05-18 16:30:20 +12:00
Jesse Hills
2c28d79bf8 Merge branch 'beta' into bump-2022.5.0 2022-05-18 16:30:19 +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
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
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
f8a1bd4e79 Bump version to 2022.6.0-dev 2022-05-11 12:50:42 +12:00
Jesse Hills
993044c870 Merge pull request #3408 from esphome/bump-2022.4.0
2022.4.0
2022-04-21 07:42:58 +12:00
Jesse Hills
a8c1b63edb Bump version to 2022.4.0 2022-04-20 17:06:08 +12:00
Jesse Hills
db7d946e1b Merge branch 'beta' into bump-2022.4.0 2022-04-20 17:06:08 +12:00
Jesse Hills
fc7348d46d Merge pull request #3346 from esphome/bump-2022.3.2
2022.3.2
2022-03-30 13:28:58 +13:00
Jesse Hills
8be2456c7e Bump version to 2022.3.2 2022-03-30 08:15:50 +13:00
Jesse Hills
bb5f7249a6 Actually increase request memory for json parsing (#3331) 2022-03-30 08:15:50 +13:00
Jesse Hills
fc94a5d0ee Merge pull request #3324 from esphome/bump-2022.3.1
2022.3.1
2022-03-24 16:45:14 +13:00
Jesse Hills
24029cc918 Bump version to 2022.3.1 2022-03-23 12:26:02 +13:00
Jesse Hills
9a9d5964ee Add small delay before setting up app in safe mode (#3323) 2022-03-23 12:26:02 +13:00
Jesse Hills
4e4a512107 Reserve less memory for json (#3289) 2022-03-23 12:26:02 +13:00
Jesse Hills
0729ed538e Webserver utilize Component Iterator to not overload eventstream (#3310) 2022-03-23 12:26:01 +13:00
wysiwyng
24b75b7ed6 Fix WDT reset during dallas search algorithm (#3293) 2022-03-23 12:26:01 +13:00
Jesse Hills
ec3618ecb8 Merge pull request #3304 from esphome/bump-2022.3.0
2022.3.0
2022-03-16 23:38:01 +13:00
Jesse Hills
792a24f38d Bump version to 2022.3.0 2022-03-16 22:32:24 +13:00
Jesse Hills
652e8a015b Merge branch 'beta' into bump-2022.3.0 2022-03-16 22:32:24 +13:00
Jesse Hills
1ef6fd8fb0 Merge pull request #3261 from esphome/bump-2022.2.6
2022.2.6
2022-03-02 22:54:51 +13:00
Jesse Hills
942b0de7fd Bump version to 2022.2.6 2022-03-02 17:07:08 +13:00
Jesse Hills
859cca49d1 Only get free memory size from internal (#3259) 2022-03-02 17:07:08 +13:00
Jesse Hills
8f7ff25624 Merge pull request #3252 from esphome/bump-2022.2.5
2022.2.5
2022-02-24 07:28:56 +13:00
Jesse Hills
97aca8e54c Bump version to 2022.2.5 2022-02-23 11:20:48 +13:00
Nicholas Peters
95acf19067 Fix regression caused by TSL2591 auto gain (#3249) 2022-02-23 11:20:48 +13:00
Martin Weinelt
3d0899aa58 Respect ESPHOME_USE_SUBPROCESS in esp32 post_build script (#3246) 2022-02-23 11:20:48 +13:00
Jesse Hills
138d6e505b Merge pull request #3245 from esphome/bump-2022.2.4
2022.2.4
2022-02-21 10:54:54 +13:00
Jesse Hills
2748e6ba29 Bump version to 2022.2.4 2022-02-21 10:17:25 +13:00
Jesse Hills
dbd4e927d8 Fix fatal erroring in addon startup script (#3244) 2022-02-21 10:17:24 +13:00
Jesse Hills
e73d47918f Fix lilygo touchscreen rotation (#3221) 2022-02-21 10:17:24 +13:00
Tyler Bules
b881bc071e ESP32-C3 deep sleep fix (#3066) 2022-02-21 10:17:24 +13:00
Otto Winter
1d0395d1c7 Improve ESP8266 iram usage (#3223) 2022-02-21 10:17:24 +13:00
Otto Winter
616c787e37 Fix ESP8266 climate memaccess warning (#3226) 2022-02-21 10:17:24 +13:00
Otto Winter
0c4de2bc97 Publish NAN when dallas conversion failed (#3227) 2022-02-21 10:17:24 +13:00
Jesse Hills
c2f5ac9eba Merge pull request #3220 from esphome/bump-2022.2.3
2022.2.3
2022-02-18 12:06:19 +13:00
Jesse Hills
5764c988af Bump version to 2022.2.3 2022-02-18 11:51:56 +13:00
dependabot[bot]
ccc2fbfd67 Bump platformio from 5.2.4 to 5.2.5 (#3188)
* Bump platformio from 5.2.4 to 5.2.5

Bumps [platformio](https://github.com/platformio/platformio) from 5.2.4 to 5.2.5.
- [Release notes](https://github.com/platformio/platformio/releases)
- [Changelog](https://github.com/platformio/platformio-core/blob/develop/HISTORY.rst)
- [Commits](https://github.com/platformio/platformio/commits)

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

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

* Update requirements.txt

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Otto Winter <otto@otto-winter.com>
2022-02-18 11:51:56 +13:00
Jesse Hills
10b4adb8e6 Merge pull request #3219 from esphome/bump-2022.2.2
2022.2.2
2022-02-18 10:36:33 +13:00
Jesse Hills
83b7181bcb Bump version to 2022.2.2 2022-02-18 10:16:11 +13:00
Jesse Hills
8886b7e141 Add LONG LONG flag for arduinojson (#3212) 2022-02-18 10:16:11 +13:00
Otto Winter
7dcc4d030b Fix platformio docker version mismstch (#3215) 2022-02-18 10:16:11 +13:00
Stewart
b9398897c1 Set entity-category to diagnostic for debug component (#3209)
Co-authored-by: Stewart Morgan <stewart@arnos-vale.net>
Co-authored-by: root <root@build.servers.arnos-vale.net>
2022-02-18 10:16:11 +13:00
Jesse Hills
657b1c60ae Merge pull request #3208 from esphome/bump-2022.2.1
2022.2.1
2022-02-17 08:00:12 +13:00
Jesse Hills
dc54b17778 Bump version to 2022.2.1 2022-02-17 07:36:14 +13:00
Stewart
1fb214165b Fix missed ARDUINO_VERSION_CODE to USE_ARDUINO_VERSION_CODE changes (#3206)
Co-authored-by: Stewart Morgan <stewart@arnos-vale.net>
2022-02-17 07:36:14 +13:00
Jesse Hills
81b2fd78f5 Merge pull request #3204 from esphome/bump-2022.2.0
2022.2.0
2022-02-16 21:12:45 +13:00
Jesse Hills
69002fb1e6 Bump version to 2022.2.0 2022-02-16 20:13:14 +13:00
Jesse Hills
75332a752d Merge branch 'beta' into bump-2022.2.0 2022-02-16 20:13:13 +13:00
Jesse Hills
09ed1aed93 Merge pull request #3177 from esphome/bump-2022.1.4
2022.1.4
2022-02-09 12:42:47 +13:00
Jesse Hills
53d3718028 Bump version to 2022.1.4 2022-02-09 11:54:41 +13:00
Jesse Hills
2b5dce5232 Try fix canbus config validation (#3173) 2022-02-09 11:54:40 +13:00
Otto Winter
9ad84150aa Enable mDNS during OTA safe mode (#3146) 2022-02-09 11:54:40 +13:00
Jesse Hills
c0523590b4 Merge pull request #3154 from esphome/bump-2022.1.3
2022.1.3
2022-02-03 07:17:52 +13:00
Jesse Hills
c7f091ab10 Bump version to 2022.1.3 2022-02-02 22:16:24 +13:00
Jesse Hills
7479e0aada Fix backwards string case helpers (#3126) 2022-02-02 22:16:16 +13:00
Jesse Hills
5bbee1a1fe Merge pull request #3111 from esphome/bump-2022.1.2
2022.1.2
2022-01-25 09:36:20 +13:00
Jesse Hills
bdb9546ca3 Bump version to 2022.1.2 2022-01-25 09:11:20 +13:00
Plácido Revilla
46af4cad6e Set the wrapped single light in light partition to internal (#3092) 2022-01-25 09:11:19 +13:00
Martin
76a238912b [modbus_controller] fix incorrect start address for number write (#3073) 2022-01-25 09:11:19 +13:00
Jesse Hills
909a526967 Merge pull request #3075 from esphome/bump-2022.1.1
2022.1.1
2022-01-20 09:08:57 +13:00
Jesse Hills
cd6f4fb93f Bump version to 2022.1.1 2022-01-20 08:34:18 +13:00
Jesse Hills
c19458696e Add *.py.script files to distributions (#3074) 2022-01-20 08:34:18 +13:00
Jesse Hills
318b930e9f Merge pull request #3070 from esphome/bump-2022.1.0
2022.1.0
2022-01-19 19:43:54 +13:00
Jesse Hills
9296a078a7 Bump version to 2022.1.0 2022-01-19 16:08:27 +13:00
301 changed files with 11269 additions and 4574 deletions

View File

@@ -7,3 +7,8 @@ updates:
ignore:
# Hypotehsis is only used for testing and is updated quite often
- dependency-name: hypothesis
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

View File

@@ -30,15 +30,15 @@ jobs:
arch: [amd64, armv7, aarch64]
build_type: ["ha-addon", "docker", "lint"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set TAG
run: |

View File

@@ -75,15 +75,15 @@ jobs:
pio_cache_key: tidyesp32-idf
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
id: python
with:
python-version: '3.8'
- name: Cache virtualenv
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: .venv
key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }}
@@ -102,7 +102,7 @@ jobs:
# Use per check platformio cache because checks use different parts
- name: Cache platformio
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -131,7 +131,7 @@ jobs:
if: matrix.id == 'ci-custom'
- name: Lint Python
run: script/lint-python
run: script/lint-python -a
if: matrix.id == 'lint-python'
- run: esphome compile ${{ matrix.file }}
@@ -163,4 +163,4 @@ jobs:
- name: Suggested changes
run: script/ci-suggest-changes
if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format')
if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format' || matrix.id == 'lint-python')

View File

@@ -17,7 +17,7 @@ jobs:
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Get tag
id: tag
run: |
@@ -35,9 +35,9 @@ jobs:
if: github.repository == 'esphome/esphome' && github.event_name == 'release'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v1
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Set up python environment
@@ -65,24 +65,24 @@ jobs:
arch: [amd64, armv7, aarch64]
build_type: ["ha-addon", "docker", "lint"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Log in to docker hub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -108,9 +108,9 @@ jobs:
matrix:
build_type: ["ha-addon", "docker", "lint"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Enable experimental manifest support
@@ -119,12 +119,12 @@ jobs:
echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json
- name: Log in to docker hub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

@@ -16,7 +16,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
- uses: actions/stale@v5
with:
days-before-pr-stale: 90
days-before-pr-close: 7
@@ -35,7 +35,7 @@ jobs:
close-issues:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
- uses: actions/stale@v5
with:
days-before-pr-stale: -1
days-before-pr-close: -1

View File

@@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/ambv/black
rev: 22.3.0
rev: 22.6.0
hooks:
- id: black
args:
@@ -26,7 +26,7 @@ repos:
- --branch=release
- --branch=beta
- repo: https://github.com/asottile/pyupgrade
rev: v2.31.1
rev: v2.37.3
hooks:
- id: pyupgrade
args: [--py38-plus]

View File

@@ -52,6 +52,7 @@ esphome/components/cs5460a/* @balrog-kun
esphome/components/cse7761/* @berfenger
esphome/components/ct_clamp/* @jesserockz
esphome/components/current_based/* @djwmarcx
esphome/components/dac7678/* @NickB1
esphome/components/daly_bms/* @s1lvi0
esphome/components/dashboard_import/* @esphome/core
esphome/components/debug/* @OttoWinter
@@ -72,6 +73,7 @@ esphome/components/esp8266/* @esphome/core
esphome/components/exposure_notifications/* @OttoWinter
esphome/components/ezo/* @ssieb
esphome/components/fastled_base/* @OttoWinter
esphome/components/feedback/* @ianchi
esphome/components/fingerprint_grow/* @OnFreund @loongyh
esphome/components/globals/* @esphome/core
esphome/components/gpio/* @esphome/core
@@ -88,6 +90,7 @@ esphome/components/honeywellabp/* @RubyBailey
esphome/components/hrxl_maxsonar_wr/* @netmikey
esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/i2c/* @esphome/core
esphome/components/i2s_audio/* @jesserockz
esphome/components/improv_serial/* @esphome/core
esphome/components/ina260/* @MrEditor97
esphome/components/inkbird_ibsth1_mini/* @fkirill
@@ -102,6 +105,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/ltr390/* @sjtrny
esphome/components/max31865/* @DAVe3283
esphome/components/max44009/* @berfenger
esphome/components/max7219digit/* @rspaargaren
esphome/components/max9611/* @mckaymatthew
@@ -119,6 +123,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
@@ -178,15 +183,18 @@ 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/smt100/* @piechade
esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/spi/* @esphome/core
esphome/components/sprinkler/* @kbx81
esphome/components/sps30/* @martgras
esphome/components/ssd1322_base/* @kbx81
esphome/components/ssd1322_spi/* @kbx81
@@ -222,6 +230,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
@@ -231,6 +240,7 @@ esphome/components/version/* @esphome/core
esphome/components/wake_on_lan/* @willwill2will54
esphome/components/web_server_base/* @OttoWinter
esphome/components/whirlpool/* @glmnet
esphome/components/whynter/* @aeonsablaze
esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs

View File

@@ -46,12 +46,10 @@ RUN \
# Ubuntu python3-pip is missing wheel
pip3 install --no-cache-dir \
wheel==0.37.1 \
platformio==5.2.5 \
platformio==6.0.2 \
# Change some platformio settings
&& platformio settings set enable_telemetry No \
&& platformio settings set check_libraries_interval 1000000 \
&& platformio settings set check_platformio_interval 1000000 \
&& platformio settings set check_platforms_interval 1000000 \
&& mkdir -p /piolibs
@@ -96,7 +94,7 @@ RUN \
apt-get update \
# Use pinned versions so that we get updates with build caching
&& apt-get install -y --no-install-recommends \
nginx-light=1.18.0-6.1 \
nginx-light=1.18.0-6.1+deb11u2 \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \
@@ -136,7 +134,7 @@ RUN \
clang-tidy-11=1:11.0.1-2 \
patch=2.7.6-7 \
software-properties-common=0.96.20.2-2.1 \
nano=5.4-2 \
nano=5.4-2+deb11u1 \
build-essential=12.9 \
python3-dev=3.9.2-3 \
&& rm -rf \

View File

@@ -12,7 +12,7 @@ from esphome.const import (
CONF_TYPE_ID,
CONF_TIME,
)
from esphome.jsonschema import jschema_extractor
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.util import Registry
@@ -23,11 +23,10 @@ def maybe_simple_id(*validators):
def maybe_conf(conf, *validators):
validator = cv.All(*validators)
@jschema_extractor("maybe")
@schema_extractor("maybe")
def validate(value):
# pylint: disable=comparison-with-callable
if value == jschema_extractor:
return validator
if value == SCHEMA_EXTRACT:
return (validator, conf)
if isinstance(value, dict):
return validator(value)
@@ -111,11 +110,9 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
# This should only happen with invalid configs, but let's have a nice error message.
return [schema(value)]
@jschema_extractor("automation")
@schema_extractor("automation")
def validator(value):
# hack to get the schema
# pylint: disable=comparison-with-callable
if value == jschema_extractor:
if value == SCHEMA_EXTRACT:
return schema
value = validator_(value)

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,10 +62,6 @@ void ADCSensor::setup() {
}
}
// 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 +82,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 +136,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 +161,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

@@ -92,7 +92,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
}
if (this->codec_->has_unit()) {
this->fahrenheit_ = (this->codec_->unit_ == 'f');
ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celcius");
ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celsius");
this->current_request_++;
}
this->publish_state();

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) {}
}
@@ -472,6 +473,7 @@ enum SensorStateClass {
STATE_CLASS_NONE = 0;
STATE_CLASS_MEASUREMENT = 1;
STATE_CLASS_TOTAL_INCREASING = 2;
STATE_CLASS_TOTAL = 3;
}
enum SensorLastResetType {
@@ -991,7 +993,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 +1012,7 @@ message LockCommandRequest {
fixed32 key = 1;
LockCommand command = 2;
# Not yet implemented:
// Not yet implemented:
bool has_code = 3;
string code = 4;
}
@@ -1040,3 +1042,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

@@ -270,7 +270,7 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
*
* If the handshake is still active when this method returns and a read/write can't take place at
* the moment, returns WOULD_BLOCK.
* If an error occured, returns that error. Only returns OK if the transport is ready for data
* If an error occurred, returns that error. Only returns OK if the transport is ready for data
* traffic.
*/
APIError APINoiseFrameHelper::state_action_() {
@@ -586,7 +586,7 @@ APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
}
return APIError::OK;
} else if (sent == -1) {
// an error occured
// an error occurred
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
@@ -980,7 +980,7 @@ APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt
}
return APIError::OK;
} else if (sent == -1) {
// an error occured
// an error occurred
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;

View File

@@ -108,6 +108,8 @@ template<> const char *proto_enum_to_string<enums::SensorStateClass>(enums::Sens
return "STATE_CLASS_MEASUREMENT";
case enums::STATE_CLASS_TOTAL_INCREASING:
return "STATE_CLASS_TOTAL_INCREASING";
case enums::STATE_CLASS_TOTAL:
return "STATE_CLASS_TOTAL";
default:
return "UNKNOWN";
}
@@ -308,6 +310,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 +4606,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

@@ -53,6 +53,7 @@ enum SensorStateClass : uint32_t {
STATE_CLASS_NONE = 0,
STATE_CLASS_MEASUREMENT = 1,
STATE_CLASS_TOTAL_INCREASING = 2,
STATE_CLASS_TOTAL = 3,
};
enum SensorLastResetType : uint32_t {
LAST_RESET_NONE = 0,
@@ -141,6 +142,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 +1160,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

@@ -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

@@ -68,6 +68,9 @@ class APIServer : public Component, public Controller {
#endif
#ifdef USE_LOCK
void on_lock_update(lock::Lock *obj) override;
#endif
#ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }

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

@@ -1 +1,52 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import ble_client, time
from esphome.const import (
CONF_ID,
CONF_RECEIVE_TIMEOUT,
CONF_TIME_ID,
)
CODEOWNERS = ["@jhansche"]
DEPENDENCIES = ["ble_client"]
MULTI_CONF = True
CONF_BEDJET_ID = "bedjet_id"
bedjet_ns = cg.esphome_ns.namespace("bedjet")
BedJetHub = bedjet_ns.class_("BedJetHub", ble_client.BLEClientNode, cg.PollingComponent)
CONFIG_SCHEMA = (
cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(BedJetHub),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Optional(
CONF_RECEIVE_TIMEOUT, default="0s"
): cv.positive_time_period_milliseconds,
}
)
.extend(ble_client.BLE_CLIENT_SCHEMA)
.extend(cv.polling_component_schema("15s"))
)
BEDJET_CLIENT_SCHEMA = cv.Schema(
{
cv.Required(CONF_BEDJET_ID): cv.use_id(BedJetHub),
}
)
async def register_bedjet_child(var, config):
parent = await cg.get_variable(config[CONF_BEDJET_ID])
cg.add(parent.register_child(var))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await ble_client.register_ble_node(var, config)
if CONF_TIME_ID in config:
time_ = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_time_id(time_))
if CONF_RECEIVE_TIMEOUT in config:
cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT]))

View File

@@ -1,644 +0,0 @@
#include "bedjet.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32
namespace esphome {
namespace bedjet {
using namespace esphome::climate;
/// Converts a BedJet temp step into degrees Celsius.
float bedjet_temp_to_c(const uint8_t temp) {
// BedJet temp is "C*2"; to get C, divide by 2.
return temp / 2.0f;
}
/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%.
uint8_t bedjet_fan_step_to_speed(const uint8_t fan) {
// 0 = 5%
// 19 = 100%
return 5 * fan + 5;
}
static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
if (fan_step >= 0 && fan_step <= 19)
return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step];
return nullptr;
}
static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
for (int i = 0; i < sizeof(BEDJET_FAN_STEP_NAME_STRINGS); i++) {
if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) {
return i;
}
}
return -1;
}
void Bedjet::upgrade_firmware() {
auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE);
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
}
}
void Bedjet::dump_config() {
LOG_CLIMATE("", "BedJet Climate", this);
auto traits = this->get_traits();
ESP_LOGCONFIG(TAG, " Supported modes:");
for (auto mode : traits.get_supported_modes()) {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_mode_to_string(mode)));
}
ESP_LOGCONFIG(TAG, " Supported fan modes:");
for (const auto &mode : traits.get_supported_fan_modes()) {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
}
for (const auto &mode : traits.get_supported_custom_fan_modes()) {
ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str());
}
ESP_LOGCONFIG(TAG, " Supported presets:");
for (auto preset : traits.get_supported_presets()) {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
}
for (const auto &preset : traits.get_supported_custom_presets()) {
ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str());
}
}
void Bedjet::setup() {
this->codec_ = make_unique<BedjetCodec>();
// restore set points
auto restore = this->restore_state_();
if (restore.has_value()) {
ESP_LOGI(TAG, "Restored previous saved state.");
restore->apply(this);
} else {
// Initial status is unknown until we connect
this->reset_state_();
}
#ifdef USE_TIME
this->setup_time_();
#endif
}
/** Resets states to defaults. */
void Bedjet::reset_state_() {
this->mode = climate::CLIMATE_MODE_OFF;
this->action = climate::CLIMATE_ACTION_IDLE;
this->target_temperature = NAN;
this->current_temperature = NAN;
this->preset.reset();
this->custom_preset.reset();
this->publish_state();
}
void Bedjet::loop() {}
void Bedjet::control(const ClimateCall &call) {
ESP_LOGD(TAG, "Received Bedjet::control");
if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "Not connected, cannot handle control call yet.");
return;
}
if (call.get_mode().has_value()) {
ClimateMode mode = *call.get_mode();
BedjetPacket *pkt;
switch (mode) {
case climate::CLIMATE_MODE_OFF:
pkt = this->codec_->get_button_request(BTN_OFF);
break;
case climate::CLIMATE_MODE_HEAT:
pkt = this->codec_->get_button_request(BTN_HEAT);
break;
case climate::CLIMATE_MODE_FAN_ONLY:
pkt = this->codec_->get_button_request(BTN_COOL);
break;
case climate::CLIMATE_MODE_DRY:
pkt = this->codec_->get_button_request(BTN_DRY);
break;
default:
ESP_LOGW(TAG, "Unsupported mode: %d", mode);
return;
}
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} else {
this->force_refresh_ = true;
this->mode = mode;
// We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
this->custom_preset.reset();
this->preset.reset();
}
}
if (call.get_target_temperature().has_value()) {
auto target_temp = *call.get_target_temperature();
auto *pkt = this->codec_->get_set_target_temp_request(target_temp);
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} else {
this->target_temperature = target_temp;
}
}
if (call.get_preset().has_value()) {
ClimatePreset preset = *call.get_preset();
BedjetPacket *pkt;
if (preset == climate::CLIMATE_PRESET_BOOST) {
pkt = this->codec_->get_button_request(BTN_TURBO);
} else {
ESP_LOGW(TAG, "Unsupported preset: %d", preset);
return;
}
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} else {
// We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode.
this->mode = climate::CLIMATE_MODE_HEAT;
this->preset = preset;
this->custom_preset.reset();
this->force_refresh_ = true;
}
} else if (call.get_custom_preset().has_value()) {
std::string preset = *call.get_custom_preset();
BedjetPacket *pkt;
if (preset == "M1") {
pkt = this->codec_->get_button_request(BTN_M1);
} else if (preset == "M2") {
pkt = this->codec_->get_button_request(BTN_M2);
} else if (preset == "M3") {
pkt = this->codec_->get_button_request(BTN_M3);
} else if (preset == "EXT HT") {
pkt = this->codec_->get_button_request(BTN_EXTHT);
} else {
ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
return;
}
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} else {
this->force_refresh_ = true;
this->custom_preset = preset;
this->preset.reset();
}
}
if (call.get_fan_mode().has_value()) {
// Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments.
// We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here.
auto fan_mode = *call.get_fan_mode();
BedjetPacket *pkt;
if (fan_mode == climate::CLIMATE_FAN_LOW) {
pkt = this->codec_->get_set_fan_speed_request(3 /* = 20% */);
} else if (fan_mode == climate::CLIMATE_FAN_MEDIUM) {
pkt = this->codec_->get_set_fan_speed_request(9 /* = 50% */);
} else if (fan_mode == climate::CLIMATE_FAN_HIGH) {
pkt = this->codec_->get_set_fan_speed_request(14 /* = 75% */);
} else {
ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(),
LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
return;
}
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} else {
this->force_refresh_ = true;
}
} else if (call.get_custom_fan_mode().has_value()) {
auto fan_mode = *call.get_custom_fan_mode();
auto fan_step = bedjet_fan_speed_to_step(fan_mode);
if (fan_step >= 0 && fan_step <= 19) {
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(),
fan_step);
// The index should represent the fan_step index.
BedjetPacket *pkt = this->codec_->get_set_fan_speed_request(fan_step);
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} else {
this->force_refresh_ = true;
}
}
}
}
void Bedjet::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_DISCONNECT_EVT: {
ESP_LOGV(TAG, "Disconnected: reason=%d", param->disconnect.reason);
this->status_set_warning();
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str());
break;
}
this->char_handle_cmd_ = chr->handle;
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str());
break;
}
this->char_handle_status_ = chr->handle;
// We also need to obtain the config descriptor for this handle.
// Otherwise once we set node_state=Established, the parent will flush all handles/descriptors, and we won't be
// able to look it up.
auto *descr = this->parent_->get_config_descriptor(this->char_handle_status_);
if (descr == nullptr) {
ESP_LOGW(TAG, "No config descriptor for status handle 0x%x. Will not be able to receive status notifications",
this->char_handle_status_);
} else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 ||
descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) {
ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_,
descr->uuid.to_string().c_str());
} else {
this->config_descr_status_ = descr->handle;
}
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID);
if (chr != nullptr) {
this->char_handle_name_ = chr->handle;
auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_,
ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status);
}
}
ESP_LOGD(TAG, "Services complete: obtained char handles.");
this->node_state = espbt::ClientState::ESTABLISHED;
this->set_notify_(true);
#ifdef USE_TIME
if (this->time_id_.has_value()) {
this->send_local_time_();
}
#endif
break;
}
case ESP_GATTC_WRITE_DESCR_EVT: {
if (param->write.status != ESP_GATT_OK) {
// ESP_GATT_INVALID_ATTR_LEN
ESP_LOGW(TAG, "Error writing descr at handle 0x%04d, status=%d", param->write.handle, param->write.status);
break;
}
// [16:44:44][V][bedjet:279]: [JOENJET] Register for notify event success: h=0x002a s=0
// This might be the enable-notify descriptor? (or disable-notify)
ESP_LOGV(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle,
param->write.status);
break;
}
case ESP_GATTC_WRITE_CHAR_EVT: {
if (param->write.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error writing char at handle 0x%04d, status=%d", param->write.handle, param->write.status);
break;
}
if (param->write.handle == this->char_handle_cmd_) {
if (this->force_refresh_) {
// Command write was successful. Publish the pending state, hoping that notify will kick in.
this->publish_state();
}
}
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->parent_->conn_id)
break;
if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
break;
}
if (param->read.handle == this->char_handle_status_) {
// This is the additional packet that doesn't fit in the notify packet.
this->codec_->decode_extra(param->read.value, param->read.value_len);
} else if (param->read.handle == this->char_handle_name_) {
// The data should represent the name.
if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) {
std::string bedjet_name(reinterpret_cast<char const *>(param->read.value), param->read.value_len);
// this->set_name(bedjet_name);
ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str());
}
}
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
// This event means that ESP received the request to enable notifications on the client side. But we also have to
// tell the server that we want it to send notifications. Normally BLEClient parent would handle this
// automatically, but as soon as we set our status to Established, the parent is going to purge all the
// service/char/descriptor handles, and then get_config_descriptor() won't work anymore. There's no way to disable
// the BLEClient parent behavior, so our only option is to write the handle anyway, and hope a double-write
// doesn't break anything.
if (param->reg_for_notify.handle != this->char_handle_status_) {
ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x",
this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_);
break;
}
this->write_notify_config_descriptor_(true);
this->last_notify_ = 0;
this->force_refresh_ = true;
break;
}
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
// This event is not handled by the parent BLEClient, so we need to do this either way.
if (param->unreg_for_notify.handle != this->char_handle_status_) {
ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x",
this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_);
break;
}
this->write_notify_config_descriptor_(false);
this->last_notify_ = 0;
// Now we wait until the next update() poll to re-register notify...
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.handle != this->char_handle_status_) {
ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(),
this->char_handle_status_, param->notify.handle);
break;
}
// FIXME: notify events come in every ~200-300 ms, which is too fast to be helpful. So we
// throttle the updates to once every MIN_NOTIFY_THROTTLE (5 seconds).
// Another idea would be to keep notify off by default, and use update() as an opportunity to turn on
// notify to get enough data to update status, then turn off notify again.
uint32_t now = millis();
auto delta = now - this->last_notify_;
if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) {
bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len);
this->last_notify_ = now;
if (needs_extra) {
// this means the packet was partial, so read the status characteristic to get the second part.
auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id,
this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str());
}
}
if (this->force_refresh_) {
// If we requested an immediate update, do that now.
this->update();
this->force_refresh_ = false;
}
}
break;
}
default:
ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event);
break;
}
}
/** Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT.
*
* This is a copy of ble_client's automatic handling of `ESP_GATTC_REG_FOR_NOTIFY_EVT`, in order
* to undo the same on unregister. It also allows us to maintain the config descriptor separately,
* since the parent BLEClient is going to purge all descriptors once we set our connection status
* to `Established`.
*/
uint8_t Bedjet::write_notify_config_descriptor_(bool enable) {
auto handle = this->config_descr_status_;
if (handle == 0) {
ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", this->char_handle_status_);
return -1;
}
// NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits.
uint8_t notify_en[] = {0, 0};
notify_en[0] = enable;
auto status =
esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en),
&notify_en[0], ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status);
return status;
}
ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x", this->get_name().c_str(), enable ? "true" : "false",
handle);
return ESP_GATT_OK;
}
#ifdef USE_TIME
/** Attempts to sync the local time (via `time_id`) to the BedJet device. */
void Bedjet::send_local_time_() {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str());
return;
}
auto *time_id = *this->time_id_;
time::ESPTime now = time_id->now();
if (now.is_valid()) {
uint8_t hour = now.hour;
uint8_t minute = now.minute;
BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute);
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status);
} else {
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute);
}
}
}
/** Initializes time sync callbacks to support syncing current time to the BedJet. */
void Bedjet::setup_time_() {
if (this->time_id_.has_value()) {
this->send_local_time_();
auto *time_id = *this->time_id_;
time_id->add_on_time_sync_callback([this] { this->send_local_time_(); });
time::ESPTime now = time_id->now();
ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute);
} else {
ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
}
}
#endif
/** Writes one BedjetPacket to the BLE client on the BEDJET_COMMAND_UUID. */
uint8_t Bedjet::write_bedjet_packet_(BedjetPacket *pkt) {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
if (!this->parent_->enabled) {
ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str());
} else {
ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str());
}
return -1;
}
auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_,
pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP,
ESP_GATT_AUTH_REQ_NONE);
return status;
}
/** Configures the local ESP BLE client to register (`true`) or unregister (`false`) for status notifications. */
uint8_t Bedjet::set_notify_(const bool enable) {
uint8_t status;
if (enable) {
status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda,
this->char_handle_status_);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
}
} else {
status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda,
this->char_handle_status_);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status);
}
}
ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status);
return status;
}
/** Attempts to update the climate device from the last received BedjetStatusPacket.
*
* @return `true` if the status has been applied; `false` if there is nothing to apply.
*/
bool Bedjet::update_status_() {
if (!this->codec_->has_status())
return false;
BedjetStatusPacket status = *this->codec_->get_status_packet();
auto converted_temp = bedjet_temp_to_c(status.target_temp_step);
if (converted_temp > 0)
this->target_temperature = converted_temp;
converted_temp = bedjet_temp_to_c(status.ambient_temp_step);
if (converted_temp > 0)
this->current_temperature = converted_temp;
const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(status.fan_step);
if (fan_mode_name != nullptr) {
this->custom_fan_mode = *fan_mode_name;
}
// TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
switch (status.mode) {
case MODE_WAIT: // Biorhythm "wait" step: device is idle
case MODE_STANDBY:
this->mode = climate::CLIMATE_MODE_OFF;
this->action = climate::CLIMATE_ACTION_IDLE;
this->fan_mode = climate::CLIMATE_FAN_OFF;
this->custom_preset.reset();
this->preset.reset();
break;
case MODE_HEAT:
case MODE_EXTHT:
this->mode = climate::CLIMATE_MODE_HEAT;
this->action = climate::CLIMATE_ACTION_HEATING;
this->custom_preset.reset();
this->preset.reset();
break;
case MODE_COOL:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
this->action = climate::CLIMATE_ACTION_COOLING;
this->custom_preset.reset();
this->preset.reset();
break;
case MODE_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
this->action = climate::CLIMATE_ACTION_DRYING;
this->custom_preset.reset();
this->preset.reset();
break;
case MODE_TURBO:
this->preset = climate::CLIMATE_PRESET_BOOST;
this->custom_preset.reset();
this->mode = climate::CLIMATE_MODE_HEAT;
this->action = climate::CLIMATE_ACTION_HEATING;
break;
default:
ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), status.mode);
break;
}
if (this->is_valid_()) {
this->publish_state();
this->codec_->clear_status();
this->status_clear_warning();
}
return true;
}
void Bedjet::update() {
ESP_LOGV(TAG, "[%s] update()", this->get_name().c_str());
if (this->node_state != espbt::ClientState::ESTABLISHED) {
if (!this->parent()->enabled) {
ESP_LOGD(TAG, "[%s] Not connected, because enabled=false", this->get_name().c_str());
} else {
// Possibly still trying to connect.
ESP_LOGD(TAG, "[%s] Not connected, enabled=true", this->get_name().c_str());
}
return;
}
auto result = this->update_status_();
if (!result) {
uint32_t now = millis();
uint32_t diff = now - this->last_notify_;
if (this->last_notify_ == 0) {
// This means we're connected and haven't received a notification, so it likely means that the BedJet is off.
// However, it could also mean that it's running, but failing to send notifications.
// We can try to unregister for notifications now, and then re-register, hoping to clear it up...
// But how do we know for sure which state we're in, and how do we actually clear out the buggy state?
ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str());
this->set_notify_(false);
} else if (diff > NOTIFY_WARN_THRESHOLD) {
ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000);
}
if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) {
ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_);
this->parent()->set_enabled(false);
this->parent()->set_enabled(true);
}
}
}
} // namespace bedjet
} // namespace esphome
#endif

View File

@@ -0,0 +1,23 @@
#pragma once
#include "bedjet_codec.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace bedjet {
// Forward declare BedJetHub
class BedJetHub;
class BedJetClient : public Parented<BedJetHub> {
public:
virtual void on_status(const BedjetStatusPacket *data) = 0;
virtual void on_bedjet_state(bool is_ready) = 0;
protected:
friend BedJetHub;
virtual std::string describe() = 0;
};
} // namespace bedjet
} // namespace esphome

View File

@@ -0,0 +1,354 @@
#include "bedjet_climate.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32
namespace esphome {
namespace bedjet {
using namespace esphome::climate;
/// Converts a BedJet temp step into degrees Celsius.
float bedjet_temp_to_c(const uint8_t temp) {
// BedJet temp is "C*2"; to get C, divide by 2.
return temp / 2.0f;
}
static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
if (fan_step <= 19)
return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step];
return nullptr;
}
static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
for (int i = 0; i < sizeof(BEDJET_FAN_STEP_NAME_STRINGS); i++) {
if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) {
return i;
}
}
return -1;
}
static inline BedjetButton heat_button(BedjetHeatMode mode) {
return mode == HEAT_MODE_EXTENDED ? BTN_EXTHT : BTN_HEAT;
}
std::string BedJetClimate::describe() { return "BedJet Climate"; }
void BedJetClimate::dump_config() {
LOG_CLIMATE("", "BedJet Climate", this);
auto traits = this->get_traits();
ESP_LOGCONFIG(TAG, " Supported modes:");
for (auto mode : traits.get_supported_modes()) {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_mode_to_string(mode)));
}
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
ESP_LOGCONFIG(TAG, " - BedJet heating mode: EXT HT");
} else {
ESP_LOGCONFIG(TAG, " - BedJet heating mode: HEAT");
}
ESP_LOGCONFIG(TAG, " Supported fan modes:");
for (const auto &mode : traits.get_supported_fan_modes()) {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
}
for (const auto &mode : traits.get_supported_custom_fan_modes()) {
ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str());
}
ESP_LOGCONFIG(TAG, " Supported presets:");
for (auto preset : traits.get_supported_presets()) {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
}
for (const auto &preset : traits.get_supported_custom_presets()) {
ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str());
}
}
void BedJetClimate::setup() {
// restore set points
auto restore = this->restore_state_();
if (restore.has_value()) {
ESP_LOGI(TAG, "Restored previous saved state.");
restore->apply(this);
} else {
// Initial status is unknown until we connect
this->reset_state_();
}
}
/** Resets states to defaults. */
void BedJetClimate::reset_state_() {
this->mode = CLIMATE_MODE_OFF;
this->action = CLIMATE_ACTION_IDLE;
this->target_temperature = NAN;
this->current_temperature = NAN;
this->preset.reset();
this->custom_preset.reset();
this->publish_state();
}
void BedJetClimate::loop() {}
void BedJetClimate::control(const ClimateCall &call) {
ESP_LOGD(TAG, "Received BedJetClimate::control");
if (!this->parent_->is_connected()) {
ESP_LOGW(TAG, "Not connected, cannot handle control call yet.");
return;
}
if (call.get_mode().has_value()) {
ClimateMode mode = *call.get_mode();
bool button_result;
switch (mode) {
case CLIMATE_MODE_OFF:
button_result = this->parent_->button_off();
break;
case CLIMATE_MODE_HEAT:
button_result = this->parent_->send_button(heat_button(this->heating_mode_));
break;
case CLIMATE_MODE_FAN_ONLY:
button_result = this->parent_->button_cool();
break;
case CLIMATE_MODE_DRY:
button_result = this->parent_->button_dry();
break;
default:
ESP_LOGW(TAG, "Unsupported mode: %d", mode);
return;
}
if (button_result) {
this->mode = mode;
// We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
this->custom_preset.reset();
this->preset.reset();
}
}
if (call.get_target_temperature().has_value()) {
auto target_temp = *call.get_target_temperature();
auto result = this->parent_->set_target_temp(target_temp);
if (result) {
this->target_temperature = target_temp;
}
}
if (call.get_preset().has_value()) {
ClimatePreset preset = *call.get_preset();
bool result;
if (preset == CLIMATE_PRESET_BOOST) {
// We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode.
result = this->parent_->button_turbo();
if (result) {
this->mode = CLIMATE_MODE_HEAT;
this->preset = CLIMATE_PRESET_BOOST;
this->custom_preset.reset();
}
} else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
// We were in heat mode with Boost preset, and now preset is set to None, so revert to normal heat.
result = this->parent_->send_button(heat_button(this->heating_mode_));
if (result) {
this->preset.reset();
this->custom_preset.reset();
}
} else {
ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
LOG_STR_ARG(climate_preset_to_string(preset)), LOG_STR_ARG(climate_mode_to_string(this->mode)),
LOG_STR_ARG(climate_preset_to_string(this->preset.value_or(CLIMATE_PRESET_NONE))));
}
} else {
ESP_LOGW(TAG, "Unsupported preset: %d", preset);
return;
}
} else if (call.get_custom_preset().has_value()) {
std::string preset = *call.get_custom_preset();
bool result;
if (preset == "M1") {
result = this->parent_->button_memory1();
} else if (preset == "M2") {
result = this->parent_->button_memory2();
} else if (preset == "M3") {
result = this->parent_->button_memory3();
} else if (preset == "LTD HT") {
result = this->parent_->button_heat();
} else if (preset == "EXT HT") {
result = this->parent_->button_ext_heat();
} else {
ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
return;
}
if (result) {
this->custom_preset = preset;
this->preset.reset();
}
}
if (call.get_fan_mode().has_value()) {
// Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments.
// We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here.
auto fan_mode = *call.get_fan_mode();
bool result;
if (fan_mode == CLIMATE_FAN_LOW) {
result = this->parent_->set_fan_speed(20);
} else if (fan_mode == CLIMATE_FAN_MEDIUM) {
result = this->parent_->set_fan_speed(50);
} else if (fan_mode == CLIMATE_FAN_HIGH) {
result = this->parent_->set_fan_speed(75);
} else {
ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(),
LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
return;
}
if (result) {
this->fan_mode = fan_mode;
this->custom_fan_mode.reset();
}
} else if (call.get_custom_fan_mode().has_value()) {
auto fan_mode = *call.get_custom_fan_mode();
auto fan_index = bedjet_fan_speed_to_step(fan_mode);
if (fan_index <= 19) {
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(),
fan_index);
bool result = this->parent_->set_fan_index(fan_index);
if (result) {
this->custom_fan_mode = fan_mode;
this->fan_mode.reset();
}
}
}
}
void BedJetClimate::on_bedjet_state(bool is_ready) {}
void BedJetClimate::on_status(const BedjetStatusPacket *data) {
ESP_LOGV(TAG, "[%s] Handling on_status with data=%p", this->get_name().c_str(), (void *) data);
auto converted_temp = bedjet_temp_to_c(data->target_temp_step);
if (converted_temp > 0)
this->target_temperature = converted_temp;
converted_temp = bedjet_temp_to_c(data->ambient_temp_step);
if (converted_temp > 0)
this->current_temperature = converted_temp;
const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
if (fan_mode_name != nullptr) {
this->custom_fan_mode = *fan_mode_name;
}
// TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
switch (data->mode) {
case MODE_WAIT: // Biorhythm "wait" step: device is idle
case MODE_STANDBY:
this->mode = CLIMATE_MODE_OFF;
this->action = CLIMATE_ACTION_IDLE;
this->fan_mode = CLIMATE_FAN_OFF;
this->custom_preset.reset();
this->preset.reset();
break;
case MODE_HEAT:
this->mode = CLIMATE_MODE_HEAT;
this->action = 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_MODE_HEAT;
this->action = CLIMATE_ACTION_HEATING;
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:
this->mode = CLIMATE_MODE_FAN_ONLY;
this->action = CLIMATE_ACTION_COOLING;
this->custom_preset.reset();
this->preset.reset();
break;
case MODE_DRY:
this->mode = CLIMATE_MODE_DRY;
this->action = CLIMATE_ACTION_DRYING;
this->custom_preset.reset();
this->preset.reset();
break;
case MODE_TURBO:
this->preset = CLIMATE_PRESET_BOOST;
this->custom_preset.reset();
this->mode = CLIMATE_MODE_HEAT;
this->action = CLIMATE_ACTION_HEATING;
break;
default:
ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), data->mode);
break;
}
ESP_LOGV(TAG, "[%s] After on_status, new mode=%s", this->get_name().c_str(),
LOG_STR_ARG(climate_mode_to_string(this->mode)));
// FIXME: compare new state to previous state.
this->publish_state();
}
/** Attempts to update the climate device from the last received BedjetStatusPacket.
*
* This will be called from #on_status() when the parent dispatches new status packets,
* and from #update() when the polling interval is triggered.
*
* @return `true` if the status has been applied; `false` if there is nothing to apply.
*/
bool BedJetClimate::update_status_() {
if (!this->parent_->is_connected())
return false;
if (!this->parent_->has_status())
return false;
auto *status = this->parent_->get_status_packet();
if (status == nullptr)
return false;
this->on_status(status);
if (this->is_valid_()) {
// TODO: only if state changed?
this->publish_state();
this->status_clear_warning();
return true;
}
return false;
}
void BedJetClimate::update() {
ESP_LOGD(TAG, "[%s] update()", this->get_name().c_str());
// TODO: if the hub component is already polling, do we also need to include polling?
// We're already going to get on_status() at the hub's polling interval.
auto result = this->update_status_();
ESP_LOGD(TAG, "[%s] update_status result=%s", this->get_name().c_str(), result ? "true" : "false");
}
} // namespace bedjet
} // namespace esphome
#endif

View File

@@ -1,47 +1,33 @@
#pragma once
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/climate/climate.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "bedjet_base.h"
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#endif
#include "bedjet_child.h"
#include "bedjet_codec.h"
#include "bedjet_hub.h"
#ifdef USE_ESP32
#include <esp_gattc_api.h>
namespace esphome {
namespace bedjet {
namespace espbt = esphome::esp32_ble_tracker;
static const espbt::ESPBTUUID BEDJET_SERVICE_UUID = espbt::ESPBTUUID::from_raw("00001000-bed0-0080-aa55-4265644a6574");
static const espbt::ESPBTUUID BEDJET_STATUS_UUID = espbt::ESPBTUUID::from_raw("00002000-bed0-0080-aa55-4265644a6574");
static const espbt::ESPBTUUID BEDJET_COMMAND_UUID = espbt::ESPBTUUID::from_raw("00002004-bed0-0080-aa55-4265644a6574");
static const espbt::ESPBTUUID BEDJET_NAME_UUID = espbt::ESPBTUUID::from_raw("00002001-bed0-0080-aa55-4265644a6574");
class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent {
class BedJetClimate : public climate::Climate, public BedJetClient, public PollingComponent {
public:
void setup() override;
void loop() override;
void update() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
#ifdef USE_TIME
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
#endif
void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; }
/* BedJetClient status update */
void on_status(const BedjetStatusPacket *data) override;
void on_bedjet_state(bool is_ready) override;
std::string describe() override;
/** Attempts to check for and apply firmware updates. */
void upgrade_firmware();
/** Sets the default strategy to use for climate::CLIMATE_MODE_HEAT. */
void set_heating_mode(BedjetHeatMode mode) { this->heating_mode_ = mode; }
climate::ClimateTraits traits() override {
auto traits = climate::ClimateTraits();
@@ -73,6 +59,11 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
"M2",
"M3",
});
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
traits.add_supported_custom_preset("LTD HT");
} else {
traits.add_supported_custom_preset("EXT HT");
}
traits.set_visual_min_temperature(19.0);
traits.set_visual_max_temperature(43.0);
traits.set_visual_temperature_step(1.0);
@@ -82,20 +73,8 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
protected:
void control(const climate::ClimateCall &call) override;
#ifdef USE_TIME
void setup_time_();
void send_local_time_();
optional<time::RealTimeClock *> time_id_{};
#endif
BedjetHeatMode heating_mode_ = HEAT_MODE_HEAT;
uint32_t timeout_{DEFAULT_STATUS_TIMEOUT};
static const uint32_t MIN_NOTIFY_THROTTLE = 5000;
static const uint32_t NOTIFY_WARN_THRESHOLD = 300000;
static const uint32_t DEFAULT_STATUS_TIMEOUT = 900000;
uint8_t set_notify_(bool enable);
uint8_t write_bedjet_packet_(BedjetPacket *pkt);
void reset_state_();
bool update_status_();
@@ -104,17 +83,6 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
return !std::isnan(this->current_temperature) && !std::isnan(this->target_temperature) &&
this->current_temperature > 1 && this->target_temperature > 1;
}
uint32_t last_notify_ = 0;
bool force_refresh_ = false;
std::unique_ptr<BedjetCodec> codec_;
uint16_t char_handle_cmd_;
uint16_t char_handle_name_;
uint16_t char_handle_status_;
uint16_t config_descr_status_;
uint8_t write_notify_config_descriptor_(bool enable);
};
} // namespace bedjet

View File

@@ -1,4 +1,4 @@
#include "bedjet_base.h"
#include "bedjet_codec.h"
#include <cstdio>
#include <cstring>
@@ -48,7 +48,16 @@ BedjetPacket *BedjetCodec::get_set_fan_speed_request(const uint8_t fan_step) {
/** Returns a BedjetPacket that will set the device's current time. */
BedjetPacket *BedjetCodec::get_set_time_request(const uint8_t hour, const uint8_t minute) {
this->packet_.command = CMD_SET_TIME;
this->packet_.command = CMD_SET_CLOCK;
this->packet_.data_length = 2;
this->packet_.data[0] = hour;
this->packet_.data[1] = minute;
return this->clean_packet_();
}
/** Returns a BedjetPacket that will set the device's remaining runtime. */
BedjetPacket *BedjetCodec::get_set_runtime_remaining_request(const uint8_t hour, const uint8_t minute) {
this->packet_.command = CMD_SET_RUNTIME;
this->packet_.data_length = 2;
this->packet_.data[0] = hour;
this->packet_.data[1] = minute;
@@ -57,17 +66,17 @@ BedjetPacket *BedjetCodec::get_set_time_request(const uint8_t hour, const uint8_
/** Decodes the extra bytes that were received after being notified with a partial packet. */
void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) {
ESP_LOGV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
ESP_LOGVV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
uint8_t offset = this->last_buffer_size_;
if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) {
memcpy(((uint8_t *) (&this->buf_)) + offset, data, length);
ESP_LOGV(TAG,
"Extra bytes: skip1=0x%08x, skip2=0x%04x, skip3=0x%02x; update phase=0x%02x, "
"flags=BedjetFlags <conn=%c, leds=%c, units=%c, mute=%c, others=%02x>",
this->buf_._skip_1_, this->buf_._skip_2_, this->buf_._skip_3_, this->buf_.update_phase,
this->buf_.flags & 0x20 ? '1' : '0', this->buf_.flags & 0x10 ? '1' : '0',
this->buf_.flags & 0x04 ? '1' : '0', this->buf_.flags & 0x01 ? '1' : '0',
this->buf_.flags & ~(0x20 | 0x10 | 0x04 | 0x01));
ESP_LOGVV(TAG,
"Extra bytes: skip1=0x%08x, skip2=0x%04x, skip3=0x%02x; update phase=0x%02x, "
"flags=BedjetFlags <conn=%c, leds=%c, units=%c, mute=%c; packed=%02x>",
this->buf_.unused_1, this->buf_.unused_2, this->buf_.unused_3, this->buf_.update_phase,
this->buf_.flags.conn_test_passed ? '1' : '0', this->buf_.flags.leds_enabled ? '1' : '0',
this->buf_.flags.units_setup ? '1' : '0', this->buf_.flags.beeps_muted ? '1' : '0',
this->buf_.flags_packed);
} else {
ESP_LOGI(TAG, "Could not determine where to append to, last offset=%d, max size=%u, new size would be %d", offset,
sizeof(BedjetStatusPacket), length + offset);
@@ -82,8 +91,6 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) {
ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) {
this->status_packet_.reset();
// Clear old buffer
memset(&this->buf_, 0, sizeof(BedjetStatusPacket));
// Copy new data into buffer
@@ -91,23 +98,24 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) {
this->last_buffer_size_ = length;
// TODO: validate the packet checksum?
if (this->buf_.mode >= 0 && this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 &&
this->buf_.target_temp_step <= 86 && this->buf_.actual_temp_step > 1 && this->buf_.actual_temp_step <= 100 &&
this->buf_.ambient_temp_step > 1 && this->buf_.ambient_temp_step <= 100) {
if (this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && this->buf_.target_temp_step <= 86 &&
this->buf_.actual_temp_step > 1 && this->buf_.actual_temp_step <= 100 && this->buf_.ambient_temp_step > 1 &&
this->buf_.ambient_temp_step <= 100) {
// and save it for the update() loop
this->status_packet_ = this->buf_;
return this->buf_.is_partial == 1;
this->status_packet_ = &this->buf_;
return this->buf_.is_partial;
} else {
this->status_packet_ = nullptr;
// TODO: log a warning if we detect that we connected to a non-V3 device.
ESP_LOGW(TAG, "Received potentially invalid packet (len %d):", length);
}
} else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) {
// We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself.
ESP_LOGV(TAG,
"received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, "
"[12]=%d, [-1]=%d",
bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], data[9],
data[10], data[11], data[12], data[length - 1]);
ESP_LOGVV(TAG,
"received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, "
"[12]=%d, [-1]=%d",
bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8],
data[9], data[10], data[11], data[12], data[length - 1]);
if (this->has_status()) {
this->status_packet_->ambient_temp_step = data[6];
@@ -119,5 +127,35 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) {
return false;
}
/** @return `true` if the new packet is meaningfully different from the last seen packet. */
bool BedjetCodec::compare(const uint8_t *data, uint16_t length) {
if (data == nullptr) {
return false;
}
if (length < 17) {
// New packet looks small, skip it.
return false;
}
if (this->buf_.packet_format != PACKET_FORMAT_V3_HOME ||
this->buf_.packet_type != PACKET_TYPE_STATUS) { // No last seen packet, so take the new one.
return true;
}
if (data[1] != PACKET_FORMAT_V3_HOME || data[3] != PACKET_TYPE_STATUS) { // New packet is not a v3 status, skip it.
return false;
}
// Now coerce it to a status packet and compare some key fields
const BedjetStatusPacket *test = reinterpret_cast<const BedjetStatusPacket *>(data);
// These are fields that will only change due to explicit action.
// That is why we do not check ambient or actual temp here, because those are environmental.
bool explicit_fields_changed = this->buf_.mode != test->mode || this->buf_.fan_step != test->fan_step ||
this->buf_.target_temp_step != test->target_temp_step;
return explicit_fields_changed;
}
} // namespace bedjet
} // namespace esphome

View File

@@ -14,18 +14,6 @@ struct BedjetPacket {
uint8_t data[2];
};
struct BedjetFlags {
/* uint8_t */
int a_ : 1; // 0x80
int b_ : 1; // 0x40
int conn_test_passed : 1; ///< (0x20) Bit is set `1` if the last connection test passed.
int leds_enabled : 1; ///< (0x10) Bit is set `1` if the LEDs on the device are enabled.
int c_ : 1; // 0x08
int units_setup : 1; ///< (0x04) Bit is set `1` if the device's units have been configured.
int d_ : 1; // 0x02
int beeps_muted : 1; ///< (0x01) Bit is set `1` if the device's sound output is muted.
} __attribute__((packed));
enum BedjetPacketFormat : uint8_t {
PACKET_FORMAT_DEBUG = 0x05, // 5
PACKET_FORMAT_V3_HOME = 0x56, // 86
@@ -36,15 +24,25 @@ enum BedjetPacketType : uint8_t {
PACKET_TYPE_DEBUG = 0x2,
};
enum BedjetNotification : uint8_t {
NOTIFY_NONE = 0, ///< No notification pending
NOTIFY_FILTER = 1, ///< Clean Filter / Please check BedJet air filter and clean if necessary.
NOTIFY_UPDATE = 2, ///< Firmware Update / A newer version of firmware is available.
NOTIFY_UPDATE_FAIL = 3, ///< Firmware Update / Unable to connect to the firmware update server.
NOTIFY_BIO_FAIL_CLOCK_NOT_SET = 4, ///< The specified sequence cannot be run because the clock is not set
NOTIFY_BIO_FAIL_TOO_LONG = 5, ///< The specified sequence cannot be run because it contains steps that would be too
///< long running from the current time.
// Note: after handling a notification, send MAGIC_NOTIFY_ACK
};
/** The format of a BedJet V3 status packet. */
struct BedjetStatusPacket {
// [0]
uint8_t is_partial : 8; ///< `1` indicates that this is a partial packet, and more data can be read directly from the
///< characteristic.
bool is_partial : 8; ///< `1` indicates that this is a partial packet, and more data can be read directly from the
///< characteristic.
BedjetPacketFormat packet_format : 8; ///< BedjetPacketFormat::PACKET_FORMAT_V3_HOME for BedJet V3 status packet
///< format. BedjetPacketFormat::PACKET_FORMAT_DEBUG for debugging packets.
uint8_t
expecting_length : 8; ///< The expected total length of the status packet after merging the additional packet.
uint8_t expecting_length : 8; ///< The expected total length of the status packet after merging the extra packet.
BedjetPacketType packet_type : 8; ///< Typically BedjetPacketType::PACKET_TYPE_STATUS for BedJet V3 status packet.
// [4]
@@ -77,11 +75,26 @@ struct BedjetStatusPacket {
uint8_t shutdown_reason : 8; ///< The reason for the last device shutdown.
// [19-25]; the initial partial packet cuts off here after [19]
// Skip 7 bytes?
uint32_t _skip_1_ : 32; // Unknown 19-22 = 0x01810112
uint16_t _skip_2_ : 16; // Unknown 23-24 = 0x1310
uint8_t _skip_3_ : 8; // Unknown 25 = 0x00
uint8_t unused_1 : 8; // Unknown [19] = 0x01
uint8_t unused_2 : 8; // Unknown [20] = 0x81
uint8_t unused_3 : 8; // Unknown [21] = 0x01
// [22]: 0x2=is_dual_zone, ...?
struct {
int unused_1 : 1; // 0x80
int unused_2 : 1; // 0x40
int unused_3 : 1; // 0x20
int unused_4 : 1; // 0x10
int unused_5 : 1; // 0x8
int unused_6 : 1; // 0x4
bool is_dual_zone : 1; /// Is part of a Dual Zone configuration
int unused_7 : 1; // 0x1
} dual_zone_flags;
uint8_t unused_4 : 8; // Unknown 23-24 = 0x1310
uint8_t unused_5 : 8; // Unknown 23-24 = 0x1310
uint8_t unused_6 : 8; // Unknown 25 = 0x00
// [26]
// 0x18(24) = "Connection test has completed OK"
@@ -89,10 +102,27 @@ struct BedjetStatusPacket {
uint8_t update_phase : 8; ///< The current status/phase of a firmware update.
// [27]
// FIXME: cannot nest packed struct of matching length here?
/* BedjetFlags */ uint8_t flags : 8; /// See BedjetFlags for the packed byte flags.
// [28-31]; 20+11 bytes
uint32_t _skip_4_ : 32; // Unknown
union {
uint8_t flags_packed;
struct {
/* uint8_t */
int unused_1 : 1; // 0x80
int unused_2 : 1; // 0x40
bool conn_test_passed : 1; ///< (0x20) Bit is set `1` if the last connection test passed.
bool leds_enabled : 1; ///< (0x10) Bit is set `1` if the LEDs on the device are enabled.
int unused_3 : 1; // 0x08
bool units_setup : 1; ///< (0x04) Bit is set `1` if the device's units have been configured.
int unused_4 : 1; // 0x02
bool beeps_muted : 1; ///< (0x01) Bit is set `1` if the device's sound output is muted.
} __attribute__((packed)) flags;
};
// [28] = (biorhythm?) sequence step
uint8_t bio_sequence_step : 8; /// Biorhythm sequence step number
// [29] = notify_code:
BedjetNotification notify_code : 8; /// See BedjetNotification
uint16_t unused_7 : 16; // Unknown
} __attribute__((packed));
@@ -127,7 +157,7 @@ struct BedjetStatusPacket {
* - Set current time
* The BedJet needs to have its clock set properly in order to run the biorhythm programs, which might
* contain time-of-day based step rules.
* - BedjetPacket#command = BedjetCommand::CMD_SET_TIME
* - BedjetPacket#command = BedjetCommand::CMD_SET_CLOCK
* - BedjetPacket#data [0] is hours, [1] is minutes
*/
class BedjetCodec {
@@ -136,13 +166,15 @@ class BedjetCodec {
BedjetPacket *get_set_target_temp_request(float temperature);
BedjetPacket *get_set_fan_speed_request(uint8_t fan_step);
BedjetPacket *get_set_time_request(uint8_t hour, uint8_t minute);
BedjetPacket *get_set_runtime_remaining_request(uint8_t hour, uint8_t minute);
bool decode_notify(const uint8_t *data, uint16_t length);
void decode_extra(const uint8_t *data, uint16_t length);
bool compare(const uint8_t *data, uint16_t length);
inline bool has_status() { return this->status_packet_.has_value(); }
const optional<BedjetStatusPacket> &get_status_packet() const { return this->status_packet_; }
void clear_status() { this->status_packet_.reset(); }
inline bool has_status() { return this->status_packet_ != nullptr; }
const BedjetStatusPacket *get_status_packet() const { return this->status_packet_; }
void clear_status() { this->status_packet_ = nullptr; }
protected:
BedjetPacket *clean_packet_();
@@ -151,7 +183,7 @@ class BedjetCodec {
BedjetPacket packet_;
optional<BedjetStatusPacket> status_packet_;
BedjetStatusPacket *status_packet_;
BedjetStatusPacket buf_;
};

View File

@@ -7,6 +7,14 @@ namespace bedjet {
static const char *const TAG = "bedjet";
/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%.
inline static uint8_t bedjet_fan_step_to_speed(const uint8_t fan) {
// 0 = 5%
// 19 = 100%
return 5 * fan + 5;
}
inline static uint8_t bedjet_fan_speed_to_index(const uint8_t speed) { return speed / 5 - 1; }
enum BedjetMode : uint8_t {
/// BedJet is Off
MODE_STANDBY = 0,
@@ -24,6 +32,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,
@@ -54,20 +70,23 @@ enum BedjetButton : uint8_t {
MAGIC_CONNTEST = 0x42,
/// Request a firmware update. This will also restart the Bedjet.
MAGIC_UPDATE = 0x43,
/// Acknowledge notification handled. See BedjetNotify
MAGIC_NOTIFY_ACK = 0x52,
};
enum BedjetCommand : uint8_t {
CMD_BUTTON = 0x1,
CMD_SET_RUNTIME = 0x2,
CMD_SET_TEMP = 0x3,
CMD_STATUS = 0x6,
CMD_SET_FAN = 0x7,
CMD_SET_TIME = 0x8,
CMD_SET_CLOCK = 0x8,
};
#define BEDJET_FAN_STEP_NAMES_ \
{ \
" 5%", " 10%", " 15%", " 20%", " 25%", " 30%", " 35%", " 40%", " 45%", " 50%", " 55%", " 60%", " 65%", " 70%", \
" 75%", " 80%", " 85%", " 90%", " 95%", "100%" \
"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

@@ -0,0 +1,559 @@
#include "bedjet_hub.h"
#include "bedjet_child.h"
#include "bedjet_const.h"
namespace esphome {
namespace bedjet {
static const LogString *bedjet_button_to_string(BedjetButton button) {
switch (button) {
case BTN_OFF:
return LOG_STR("OFF");
case BTN_COOL:
return LOG_STR("COOL");
case BTN_HEAT:
return LOG_STR("HEAT");
case BTN_EXTHT:
return LOG_STR("EXT HT");
case BTN_TURBO:
return LOG_STR("TURBO");
case BTN_DRY:
return LOG_STR("DRY");
case BTN_M1:
return LOG_STR("M1");
case BTN_M2:
return LOG_STR("M2");
case BTN_M3:
return LOG_STR("M3");
default:
return LOG_STR("unknown");
}
}
/* Public */
void BedJetHub::upgrade_firmware() {
auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE);
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] MAGIC_UPDATE button failed, status=%d", this->get_name().c_str(), status);
}
}
bool BedJetHub::button_heat() { return this->send_button(BTN_HEAT); }
bool BedJetHub::button_ext_heat() { return this->send_button(BTN_EXTHT); }
bool BedJetHub::button_turbo() { return this->send_button(BTN_TURBO); }
bool BedJetHub::button_cool() { return this->send_button(BTN_COOL); }
bool BedJetHub::button_dry() { return this->send_button(BTN_DRY); }
bool BedJetHub::button_off() { return this->send_button(BTN_OFF); }
bool BedJetHub::button_memory1() { return this->send_button(BTN_M1); }
bool BedJetHub::button_memory2() { return this->send_button(BTN_M2); }
bool BedJetHub::button_memory3() { return this->send_button(BTN_M3); }
bool BedJetHub::set_fan_index(uint8_t fan_speed_index) {
if (fan_speed_index > 19) {
ESP_LOGW(TAG, "Invalid fan speed index %d, expecting 0-19.", fan_speed_index);
return false;
}
auto *pkt = this->codec_->get_set_fan_speed_request(fan_speed_index);
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] writing fan speed failed, status=%d", this->get_name().c_str(), status);
}
return status == 0;
}
uint8_t BedJetHub::get_fan_index() {
auto *status = this->codec_->get_status_packet();
if (status != nullptr) {
return status->fan_step;
}
return 0;
}
bool BedJetHub::set_target_temp(float temp_c) {
auto *pkt = this->codec_->get_set_target_temp_request(temp_c);
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] writing target temp failed, status=%d", this->get_name().c_str(), status);
}
return status == 0;
}
bool BedJetHub::set_time_remaining(uint8_t hours, uint8_t mins) {
// FIXME: this may fail depending on current mode or other restrictions enforced by the unit.
auto *pkt = this->codec_->get_set_runtime_remaining_request(hours, mins);
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] writing remaining runtime failed, status=%d", this->get_name().c_str(), status);
}
return status == 0;
}
bool BedJetHub::send_button(BedjetButton button) {
auto *pkt = this->codec_->get_button_request(button);
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] writing button %s failed, status=%d", this->get_name().c_str(),
LOG_STR_ARG(bedjet_button_to_string(button)), status);
} else {
ESP_LOGD(TAG, "[%s] writing button %s success", this->get_name().c_str(),
LOG_STR_ARG(bedjet_button_to_string(button)));
}
return status == 0;
}
uint16_t BedJetHub::get_time_remaining() {
auto *status = this->codec_->get_status_packet();
if (status != nullptr) {
return status->time_remaining_secs + status->time_remaining_mins * 60 + status->time_remaining_hrs * 3600;
}
return 0;
}
/* Bluetooth/GATT */
uint8_t BedJetHub::write_bedjet_packet_(BedjetPacket *pkt) {
if (!this->is_connected()) {
if (!this->parent_->enabled) {
ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str());
} else {
ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str());
}
return -1;
}
auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_,
pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP,
ESP_GATT_AUTH_REQ_NONE);
return status;
}
/** Configures the local ESP BLE client to register (`true`) or unregister (`false`) for status notifications. */
uint8_t BedJetHub::set_notify_(const bool enable) {
uint8_t status;
if (enable) {
status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda,
this->char_handle_status_);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
}
} else {
status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda,
this->char_handle_status_);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status);
}
}
ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status);
return status;
}
bool BedJetHub::discover_characteristics_() {
bool result = true;
esphome::ble_client::BLECharacteristic *chr;
if (!this->char_handle_cmd_) {
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str());
result = false;
} else {
this->char_handle_cmd_ = chr->handle;
}
}
if (!this->char_handle_status_) {
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str());
result = false;
} else {
this->char_handle_status_ = chr->handle;
}
}
if (!this->config_descr_status_) {
// We also need to obtain the config descriptor for this handle.
// Otherwise once we set node_state=Established, the parent will flush all handles/descriptors, and we won't be
// able to look it up.
auto *descr = this->parent_->get_config_descriptor(this->char_handle_status_);
if (descr == nullptr) {
ESP_LOGW(TAG, "No config descriptor for status handle 0x%x. Will not be able to receive status notifications",
this->char_handle_status_);
result = false;
} else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 ||
descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) {
ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_,
descr->uuid.to_string().c_str());
result = false;
} else {
this->config_descr_status_ = descr->handle;
}
}
if (!this->char_handle_name_) {
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] No name service found at device, not a BedJet..?", this->get_name().c_str());
result = false;
} else {
this->char_handle_name_ = chr->handle;
auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_,
ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status);
}
}
}
ESP_LOGI(TAG, "[%s] Discovered service characteristics: ", this->get_name().c_str());
ESP_LOGI(TAG, " - Command char: 0x%x", this->char_handle_cmd_);
ESP_LOGI(TAG, " - Status char: 0x%x", this->char_handle_status_);
ESP_LOGI(TAG, " - config descriptor: 0x%x", this->config_descr_status_);
ESP_LOGI(TAG, " - Name char: 0x%x", this->char_handle_name_);
return result;
}
void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_DISCONNECT_EVT: {
ESP_LOGV(TAG, "Disconnected: reason=%d", param->disconnect.reason);
this->status_set_warning();
this->dispatch_state_(false);
break;
}
case ESP_GATTC_OPEN_EVT: {
// FIXME: bug in BLEClient
this->parent_->conn_id = param->open.conn_id;
this->open_conn_id_ = param->open.conn_id;
break;
}
case ESP_GATTC_CONNECT_EVT: {
if (this->parent_->conn_id != param->connect.conn_id && this->open_conn_id_ != 0xff) {
// FIXME: bug in BLEClient
ESP_LOGW(TAG, "[%s] CONNECT_EVT unexpected conn_id; open=%d, parent=%d, param=%d", this->get_name().c_str(),
this->open_conn_id_, this->parent_->conn_id, param->connect.conn_id);
this->parent_->conn_id = this->open_conn_id_;
}
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto result = this->discover_characteristics_();
if (result) {
ESP_LOGD(TAG, "[%s] Services complete: obtained char handles.", this->get_name().c_str());
this->node_state = espbt::ClientState::ESTABLISHED;
this->set_notify_(true);
#ifdef USE_TIME
if (this->time_id_.has_value()) {
this->send_local_time();
}
#endif
this->dispatch_state_(true);
} else {
ESP_LOGW(TAG, "[%s] Failed discovering service characteristics.", this->get_name().c_str());
this->parent()->set_enabled(false);
this->status_set_warning();
this->dispatch_state_(false);
}
break;
}
case ESP_GATTC_WRITE_DESCR_EVT: {
if (param->write.status != ESP_GATT_OK) {
if (param->write.status == ESP_GATT_INVALID_ATTR_LEN) {
// This probably means that our hack for notify_en (8 bit vs 16 bit) didn't work right.
// Should we try to fall back to BLEClient's way?
ESP_LOGW(TAG, "[%s] Invalid attr length writing descr at handle 0x%04d, status=%d", this->get_name().c_str(),
param->write.handle, param->write.status);
} else {
ESP_LOGW(TAG, "[%s] Error writing descr at handle 0x%04d, status=%d", this->get_name().c_str(),
param->write.handle, param->write.status);
}
break;
}
ESP_LOGD(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle,
param->write.status);
break;
}
case ESP_GATTC_WRITE_CHAR_EVT: {
if (param->write.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error writing char at handle 0x%04d, status=%d", param->write.handle, param->write.status);
break;
}
if (param->write.handle == this->char_handle_cmd_) {
if (this->force_refresh_) {
// Command write was successful. Publish the pending state, hoping that notify will kick in.
// FIXME: better to wait until we know the status has changed
this->dispatch_status_();
}
}
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->parent_->conn_id)
break;
if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
break;
}
if (param->read.handle == this->char_handle_status_) {
// This is the additional packet that doesn't fit in the notify packet.
this->codec_->decode_extra(param->read.value, param->read.value_len);
this->status_packet_ready_();
} else if (param->read.handle == this->char_handle_name_) {
// The data should represent the name.
if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) {
std::string bedjet_name(reinterpret_cast<char const *>(param->read.value), param->read.value_len);
ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str());
this->set_name_(bedjet_name);
}
}
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
// This event means that ESP received the request to enable notifications on the client side. But we also have to
// tell the server that we want it to send notifications. Normally BLEClient parent would handle this
// automatically, but as soon as we set our status to Established, the parent is going to purge all the
// service/char/descriptor handles, and then get_config_descriptor() won't work anymore. There's no way to disable
// the BLEClient parent behavior, so our only option is to write the handle anyway, and hope a double-write
// doesn't break anything.
if (param->reg_for_notify.handle != this->char_handle_status_) {
ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x",
this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_);
break;
}
this->write_notify_config_descriptor_(true);
this->last_notify_ = 0;
this->force_refresh_ = true;
break;
}
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
// This event is not handled by the parent BLEClient, so we need to do this either way.
if (param->unreg_for_notify.handle != this->char_handle_status_) {
ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x",
this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_);
break;
}
this->write_notify_config_descriptor_(false);
this->last_notify_ = 0;
// Now we wait until the next update() poll to re-register notify...
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (this->processing_)
break;
if (param->notify.conn_id != this->parent_->conn_id) {
ESP_LOGW(TAG, "[%s] Received notify event for unexpected parent conn: expect %x, got %x",
this->get_name().c_str(), this->parent_->conn_id, param->notify.conn_id);
// FIXME: bug in BLEClient holding wrong conn_id.
}
if (param->notify.handle != this->char_handle_status_) {
ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(),
this->char_handle_status_, param->notify.handle);
break;
}
// FIXME: notify events come in every ~200-300 ms, which is too fast to be helpful. So we
// throttle the updates to once every MIN_NOTIFY_THROTTLE (5 seconds).
// Another idea would be to keep notify off by default, and use update() as an opportunity to turn on
// notify to get enough data to update status, then turn off notify again.
uint32_t now = millis();
auto delta = now - this->last_notify_;
if (!this->force_refresh_ && this->codec_->compare(param->notify.value, param->notify.value_len)) {
// If the packet is meaningfully different, trigger children as well
this->force_refresh_ = true;
ESP_LOGV(TAG, "[%s] Incoming packet indicates a significant change.", this->get_name().c_str());
}
if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) {
// Set reentrant flag to prevent processing multiple packets.
this->processing_ = true;
ESP_LOGVV(TAG, "[%s] Decoding packet: last=%d, delta=%d, force=%s", this->get_name().c_str(),
this->last_notify_, delta, this->force_refresh_ ? "y" : "n");
bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len);
if (needs_extra) {
// This means the packet was partial, so read the status characteristic to get the second part.
// Ideally this will complete quickly. We won't process additional notification events until it does.
auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id,
this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str());
}
} else {
this->status_packet_ready_();
}
}
break;
}
default:
ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event);
break;
}
}
inline void BedJetHub::status_packet_ready_() {
this->last_notify_ = millis();
this->processing_ = false;
if (this->force_refresh_) {
// If we requested an immediate update, do that now.
this->update();
this->force_refresh_ = false;
}
}
/** Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT.
*
* This is a copy of ble_client's automatic handling of `ESP_GATTC_REG_FOR_NOTIFY_EVT`, in order
* to undo the same on unregister. It also allows us to maintain the config descriptor separately,
* since the parent BLEClient is going to purge all descriptors once we set our connection status
* to `Established`.
*/
uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) {
auto handle = this->config_descr_status_;
if (handle == 0) {
ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", this->char_handle_status_);
return -1;
}
// NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits.
uint16_t notify_en = enable ? 1 : 0;
auto status =
esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en),
(uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status);
return status;
}
ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x, for conn %d", this->get_name().c_str(),
enable ? "true" : "false", handle, this->parent_->conn_id);
return ESP_GATT_OK;
}
/* Time Component */
#ifdef USE_TIME
void BedJetHub::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.");
}
}
void BedJetHub::setup_time_() {
if (this->time_id_.has_value()) {
this->send_local_time();
auto *time_id = *this->time_id_;
time_id->add_on_time_sync_callback([this] { this->send_local_time(); });
} else {
ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
}
}
#endif
void BedJetHub::set_clock(uint8_t hour, uint8_t minute) {
if (!this->is_connected()) {
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);
}
}
/* Internal */
void BedJetHub::loop() {}
void BedJetHub::update() { this->dispatch_status_(); }
void BedJetHub::dump_config() {
ESP_LOGCONFIG(TAG, "BedJet Hub '%s'", this->get_name().c_str());
ESP_LOGCONFIG(TAG, " ble_client.app_id: %d", this->parent()->app_id);
ESP_LOGCONFIG(TAG, " ble_client.conn_id: %d", this->parent()->conn_id);
LOG_UPDATE_INTERVAL(this)
ESP_LOGCONFIG(TAG, " Child components (%d):", this->children_.size());
for (auto *child : this->children_) {
ESP_LOGCONFIG(TAG, " - %s", child->describe().c_str());
}
}
void BedJetHub::dispatch_state_(bool is_ready) {
for (auto *child : this->children_) {
child->on_bedjet_state(is_ready);
}
}
void BedJetHub::dispatch_status_() {
auto *status = this->codec_->get_status_packet();
if (!this->is_connected()) {
ESP_LOGD(TAG, "[%s] Not connected, will not send status.", this->get_name().c_str());
} else if (status != nullptr) {
ESP_LOGD(TAG, "[%s] Notifying %d children of latest status @%p.", this->get_name().c_str(), this->children_.size(),
status);
for (auto *child : this->children_) {
child->on_status(status);
}
} else {
uint32_t now = millis();
uint32_t diff = now - this->last_notify_;
if (this->last_notify_ == 0) {
// This means we're connected and haven't received a notification, so it likely means that the BedJet is off.
// However, it could also mean that it's running, but failing to send notifications.
// We can try to unregister for notifications now, and then re-register, hoping to clear it up...
// But how do we know for sure which state we're in, and how do we actually clear out the buggy state?
ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str());
} else if (diff > NOTIFY_WARN_THRESHOLD) {
ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000);
}
if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) {
ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_);
// set_enabled(false) will only close the connection if state != IDLE.
this->parent()->set_state(espbt::ClientState::CONNECTING);
this->parent()->set_enabled(false);
this->parent()->set_enabled(true);
}
}
}
void BedJetHub::register_child(BedJetClient *obj) {
this->children_.push_back(obj);
obj->set_parent(this);
}
} // namespace bedjet
} // namespace esphome

View File

@@ -0,0 +1,178 @@
#pragma once
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "bedjet_child.h"
#include "bedjet_codec.h"
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#endif
#ifdef USE_ESP32
#include <esp_gattc_api.h>
namespace esphome {
namespace bedjet {
namespace espbt = esphome::esp32_ble_tracker;
// Forward declare BedJetClient
class BedJetClient;
static const espbt::ESPBTUUID BEDJET_SERVICE_UUID = espbt::ESPBTUUID::from_raw("00001000-bed0-0080-aa55-4265644a6574");
static const espbt::ESPBTUUID BEDJET_STATUS_UUID = espbt::ESPBTUUID::from_raw("00002000-bed0-0080-aa55-4265644a6574");
static const espbt::ESPBTUUID BEDJET_COMMAND_UUID = espbt::ESPBTUUID::from_raw("00002004-bed0-0080-aa55-4265644a6574");
static const espbt::ESPBTUUID BEDJET_NAME_UUID = espbt::ESPBTUUID::from_raw("00002001-bed0-0080-aa55-4265644a6574");
/**
* Hub component connecting to the BedJet device over Bluetooth.
*/
class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingComponent {
public:
/* BedJet functionality exposed to `BedJetClient` children and/or accessible from action lambdas. */
/** Attempts to check for and apply firmware updates. */
void upgrade_firmware();
/** Press the OFF button. */
bool button_off();
/** Press the HEAT button. */
bool button_heat();
/** Press the EXT HT button. */
bool button_ext_heat();
/** Press the TURBO button. */
bool button_turbo();
/** Press the COOL button. */
bool button_cool();
/** Press the DRY button. */
bool button_dry();
/** Press the M1 (memory recall) button. */
bool button_memory1();
/** Press the M2 (memory recall) button. */
bool button_memory2();
/** Press the M3 (memory recall) button. */
bool button_memory3();
/** Send the `button`. */
bool send_button(BedjetButton button);
/** Set the target temperature to `temp_c` in °C. */
bool set_target_temp(float temp_c);
/** Set the fan speed to a stepped index in the range 0-19. */
bool set_fan_index(uint8_t fan_speed_index);
/** Set the fan speed to a percent in the range 5% - 100%, at 5% increments. */
bool set_fan_speed(uint8_t fan_speed_pct) { return this->set_fan_index(bedjet_fan_speed_to_index(fan_speed_pct)); }
/** Return the fan speed index, in the range 0-19. */
uint8_t get_fan_index();
/** Return the fan speed as a percent in the range 5%-100%. */
uint8_t get_fan_speed() { return bedjet_fan_step_to_speed(this->get_fan_index()); }
/** Set the operational runtime remaining.
*
* The unit establishes and enforces runtime limits for some modes, so this call is not guaranteed to succeed.
*/
bool set_time_remaining(uint8_t hours, uint8_t mins);
/** Return the remaining runtime, in seconds. */
uint16_t get_time_remaining();
/** @return `true` if the `BLEClient::node_state` is `ClientState::ESTABLISHED`. */
bool is_connected() { return this->node_state == espbt::ClientState::ESTABLISHED; }
bool has_status() { return this->codec_->has_status(); }
const BedjetStatusPacket *get_status_packet() const { return this->codec_->get_status_packet(); }
/** Register a `BedJetClient` child component. */
void register_child(BedJetClient *obj);
/** Set the status timeout.
*
* This is the max time to wait for a status update before the connection is presumed unusable.
*/
void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; }
#ifdef USE_TIME
/** Set the `time::RealTimeClock` implementation. */
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
/** Attempts to sync the local time (via `time_id`) to the BedJet device. */
void send_local_time();
#endif
/** Attempt to set the BedJet device's clock to the specified time. */
void set_clock(uint8_t hour, uint8_t minute);
/* Component overrides */
void loop() override;
void update() override;
void dump_config() override;
void setup() override { this->codec_ = make_unique<BedjetCodec>(); }
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
/** @return The BedJet's configured name, or the MAC address if not discovered yet. */
std::string get_name() {
if (this->name_.empty()) {
return this->parent_->address_str();
} else {
return this->name_;
}
}
/* BLEClient overrides */
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
protected:
std::vector<BedJetClient *> children_;
void dispatch_status_();
void dispatch_state_(bool is_ready);
#ifdef USE_TIME
/** Initializes time sync callbacks to support syncing current time to the BedJet. */
void setup_time_();
optional<time::RealTimeClock *> time_id_{};
#endif
uint32_t timeout_{DEFAULT_STATUS_TIMEOUT};
static const uint32_t MIN_NOTIFY_THROTTLE = 15000;
static const uint32_t NOTIFY_WARN_THRESHOLD = 300000;
static const uint32_t DEFAULT_STATUS_TIMEOUT = 900000;
uint8_t set_notify_(bool enable);
/** Send the `BedjetPacket` to the device. */
uint8_t write_bedjet_packet_(BedjetPacket *pkt);
void set_name_(const std::string &name) { this->name_ = name; }
std::string name_;
uint32_t last_notify_ = 0;
inline void status_packet_ready_();
bool force_refresh_ = false;
bool processing_ = false;
std::unique_ptr<BedjetCodec> codec_;
bool discover_characteristics_();
uint16_t char_handle_cmd_;
uint16_t char_handle_name_;
uint16_t char_handle_status_;
uint16_t config_descr_status_;
uint8_t open_conn_id_ = -1;
uint8_t write_notify_config_descriptor_(bool enable);
};
} // namespace bedjet
} // namespace esphome
#endif

View File

@@ -1,32 +1,60 @@
import logging
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import climate, ble_client, time
from esphome.components import climate, ble_client
from esphome.const import (
CONF_HEAT_MODE,
CONF_ID,
CONF_RECEIVE_TIMEOUT,
CONF_TIME_ID,
)
from . import (
BEDJET_CLIENT_SCHEMA,
register_bedjet_child,
)
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jhansche"]
DEPENDENCIES = ["ble_client"]
bedjet_ns = cg.esphome_ns.namespace("bedjet")
Bedjet = bedjet_ns.class_(
"Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent
BedJetClimate = bedjet_ns.class_(
"BedJetClimate", 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_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Optional(
CONF_RECEIVE_TIMEOUT, default="0s"
): cv.positive_time_period_milliseconds,
cv.GenerateID(): cv.declare_id(BedJetClimate),
cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum(
BEDJET_HEAT_MODES, lower=True
),
}
)
.extend(ble_client.BLE_CLIENT_SCHEMA)
.extend(cv.polling_component_schema("30s"))
.extend(cv.polling_component_schema("60s"))
.extend(
# TODO: remove compat layer.
{
cv.Optional(ble_client.CONF_BLE_CLIENT_ID): cv.invalid(
"The 'ble_client_id' option has been removed. Please migrate "
"to the new `bedjet_id` option in the `bedjet` component.\n"
"See https://esphome.io/components/climate/bedjet.html"
),
cv.Optional(CONF_TIME_ID): cv.invalid(
"The 'time_id' option has been moved to the `bedjet` component."
),
cv.Optional(CONF_RECEIVE_TIMEOUT): cv.invalid(
"The 'receive_timeout' option has been moved to the `bedjet` component."
),
}
)
.extend(BEDJET_CLIENT_SCHEMA)
)
@@ -34,9 +62,6 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await climate.register_climate(var, config)
await ble_client.register_ble_node(var, config)
if CONF_TIME_ID in config:
time_ = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_time_id(time_))
if CONF_RECEIVE_TIMEOUT in config:
cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT]))
await register_bedjet_child(var, config)
cg.add(var.set_heating_mode(config[CONF_HEAT_MODE]))

View File

@@ -22,6 +22,7 @@ from esphome.const import (
CONF_ON_PRESS,
CONF_ON_RELEASE,
CONF_ON_STATE,
CONF_PUBLISH_INITIAL_STATE,
CONF_STATE,
CONF_TIMING,
CONF_TRIGGER_ID,
@@ -29,6 +30,7 @@ from esphome.const import (
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BATTERY_CHARGING,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_COLD,
DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_DOOR,
@@ -63,6 +65,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BATTERY_CHARGING,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_COLD,
DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_DOOR,
@@ -326,6 +329,7 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt.MQTTBinarySensorComponent
),
cv.Optional(CONF_PUBLISH_INITIAL_STATE): cv.boolean,
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
cv.Optional(CONF_FILTERS): validate_filters,
cv.Optional(CONF_ON_PRESS): automation.validate_automation(
@@ -418,6 +422,8 @@ async def setup_binary_sensor_core_(var, config):
if CONF_DEVICE_CLASS in config:
cg.add(var.set_device_class(config[CONF_DEVICE_CLASS]))
if CONF_PUBLISH_INITIAL_STATE in config:
cg.add(var.set_publish_initial_state(config[CONF_PUBLISH_INITIAL_STATE]))
if CONF_INVERTED in config:
cg.add(var.set_inverted(config[CONF_INVERTED]))
if CONF_FILTERS in config:
@@ -477,8 +483,8 @@ async def register_binary_sensor(var, config):
await setup_binary_sensor_core_(var, config)
async def new_binary_sensor(config):
var = cg.new_Pvariable(config[CONF_ID])
async def new_binary_sensor(config, *args):
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_binary_sensor(var, config)
return var

View File

@@ -37,7 +37,7 @@ void BinarySensor::send_state_internal(bool state, bool is_initial) {
}
this->has_state_ = true;
this->state = state;
if (!is_initial) {
if (!is_initial || this->publish_initial_state_) {
this->state_callback_.call(state);
}
}
@@ -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

@@ -58,6 +58,8 @@ class BinarySensor : public EntityBase {
void add_filter(Filter *filter);
void add_filters(const std::vector<Filter *> &filters);
void set_publish_initial_state(bool publish_initial_state) { this->publish_initial_state_ = publish_initial_state; }
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void send_state_internal(bool state, bool is_initial);
@@ -76,12 +78,11 @@ 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};
bool has_state_{false};
bool publish_initial_state_{false};
Deduplicator<bool> publish_dedup_;
};

View File

@@ -7,7 +7,7 @@ namespace bl0939 {
static const char *const TAG = "bl0939";
// https://www.belling.com.cn/media/file_object/bel_product/BL0939/datasheet/BL0939_V1.2_cn.pdf
// (unfortunatelly chinese, but the protocol can be understood with some translation tool)
// (unfortunately chinese, but the protocol can be understood with some translation tool)
static const uint8_t BL0939_READ_COMMAND = 0x55; // 0x5{A4,A3,A2,A1}
static const uint8_t BL0939_FULL_PACKET = 0xAA;
static const uint8_t BL0939_PACKET_HEADER = 0x55;

View File

@@ -8,7 +8,7 @@ namespace esphome {
namespace bl0939 {
// https://datasheet.lcsc.com/lcsc/2108071830_BL-Shanghai-Belling-BL0939_C2841044.pdf
// (unfortunatelly chinese, but the formulas can be easily understood)
// (unfortunately chinese, but the formulas can be easily understood)
// Sonoff Dual R3 V2 has the exact same resistor values for the current shunts (RL=1miliOhm)
// and for the voltage divider (R1=0.51kOhm, R2=5*390kOhm)
// as in the manufacturer's reference circuit, so the same formulas were used here (Vref=1.218V)

View File

@@ -2,12 +2,15 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import esp32_ble_tracker
from esphome.const import (
CONF_CHARACTERISTIC_UUID,
CONF_ID,
CONF_MAC_ADDRESS,
CONF_NAME,
CONF_ON_CONNECT,
CONF_ON_DISCONNECT,
CONF_SERVICE_UUID,
CONF_TRIGGER_ID,
CONF_VALUE,
)
from esphome import automation
@@ -27,6 +30,8 @@ BLEClientConnectTrigger = ble_client_ns.class_(
BLEClientDisconnectTrigger = ble_client_ns.class_(
"BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef)
)
# Actions
BLEWriteAction = ble_client_ns.class_("BLEClientWriteAction", automation.Action)
# Espressif platformio framework is built with MAX_BLE_CONN to 3, so
# enforce this in yaml checks.
@@ -72,6 +77,33 @@ async def register_ble_node(var, config):
cg.add(parent.register_ble_node(var))
BLE_WRITE_ACTION_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(BLEClient),
cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid,
cv.Required(CONF_VALUE): cv.ensure_list(cv.hex_uint8_t),
}
)
@automation.register_action(
"ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA
)
async def ble_write_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)
value = config[CONF_VALUE]
cg.add(var.set_value(value))
serv_uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID])
cg.add(var.set_service_uuid128(serv_uuid128))
char_uuid128 = esp32_ble_tracker.as_reversed_hex_array(
config[CONF_CHARACTERISTIC_UUID]
)
cg.add(var.set_char_uuid128(char_uuid128))
return var
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -0,0 +1,74 @@
#include "automation.h"
#include <esp_gap_ble_api.h>
#include <esp_gattc_api.h>
#include <esp_bt_defs.h>
#include "esphome/core/log.h"
namespace esphome {
namespace ble_client {
static const char *const TAG = "ble_client.automation";
void BLEWriterClientNode::write() {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "Cannot write to BLE characteristic - not connected");
return;
} else if (this->ble_char_handle_ == 0) {
ESP_LOGW(TAG, "Cannot write to BLE characteristic - characteristic not found");
return;
}
esp_gatt_write_type_t write_type;
if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) {
write_type = ESP_GATT_WRITE_TYPE_RSP;
ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP");
} else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) {
write_type = ESP_GATT_WRITE_TYPE_NO_RSP;
ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
} else {
ESP_LOGE(TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str());
return;
}
ESP_LOGVV(TAG, "Will write %d bytes: %s", this->value_.size(), format_hex_pretty(this->value_).c_str());
esp_err_t err = esp_ble_gattc_write_char(this->parent()->gattc_if, this->parent()->conn_id, this->ble_char_handle_,
value_.size(), value_.data(), write_type, ESP_GATT_AUTH_REQ_NONE);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
}
}
void BLEWriterClientNode::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_REG_EVT:
break;
case ESP_GATTC_OPEN_EVT:
this->node_state = espbt::ClientState::ESTABLISHED;
ESP_LOGD(TAG, "Connection established with %s", ble_client_->address_str().c_str());
break;
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
if (chr == nullptr) {
ESP_LOGW("ble_write_action", "Characteristic %s was not found in service %s",
this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str());
break;
}
this->ble_char_handle_ = chr->handle;
this->char_props_ = chr->properties;
this->node_state = espbt::ClientState::ESTABLISHED;
ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
ble_client_->address_str().c_str());
break;
}
case ESP_GATTC_DISCONNECT_EVT:
this->node_state = espbt::ClientState::IDLE;
this->ble_char_handle_ = 0;
ESP_LOGD(TAG, "Disconnected from %s", ble_client_->address_str().c_str());
break;
default:
break;
}
}
} // namespace ble_client
} // namespace esphome

View File

@@ -1,5 +1,7 @@
#pragma once
#include <utility>
#include "esphome/core/automation.h"
#include "esphome/components/ble_client/ble_client.h"
@@ -33,6 +35,41 @@ class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode {
}
};
class BLEWriterClientNode : public BLEClientNode {
public:
BLEWriterClientNode(BLEClient *ble_client) {
ble_client->register_ble_node(this);
ble_client_ = ble_client;
}
void set_value(std::vector<uint8_t> value) { value_ = std::move(value); }
// Attempts to write the contents of value_ to char_uuid_.
void write();
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
private:
BLEClient *ble_client_;
int ble_char_handle_ = 0;
esp_gatt_char_prop_t char_props_;
espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_;
std::vector<uint8_t> value_;
};
template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, public BLEWriterClientNode {
public:
BLEClientWriteAction(BLEClient *ble_client) : BLEWriterClientNode(ble_client) {}
void play(Ts... x) override { return write(); }
};
} // namespace ble_client
} // namespace esphome

View File

@@ -113,6 +113,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
}
case ESP_GATTC_OPEN_EVT: {
ESP_LOGV(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str());
this->conn_id = param->open.conn_id;
if (param->open.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "connect to %s failed, status=%d", this->address_str().c_str(), param->open.status);
this->set_states_(espbt::ClientState::IDLE);
@@ -122,7 +123,10 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
}
case ESP_GATTC_CONNECT_EVT: {
ESP_LOGV(TAG, "[%s] ESP_GATTC_CONNECT_EVT", this->address_str().c_str());
this->conn_id = param->connect.conn_id;
if (this->conn_id != param->connect.conn_id) {
ESP_LOGD(TAG, "[%s] Unexpected conn_id in CONNECT_EVT: param conn=%d, open conn=%d",
this->address_str().c_str(), param->connect.conn_id, this->conn_id);
}
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->connect.conn_id);
if (ret) {
ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%x", ret);
@@ -183,9 +187,10 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
descr->uuid.to_string().c_str());
break;
}
uint8_t notify_en = 1;
auto status = esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en),
&notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
uint16_t notify_en = 1;
auto status =
esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en),
(uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status);
}

View File

@@ -1,13 +1,12 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import ble_client, esp32_ble_tracker, output
from esphome.const import CONF_ID, CONF_SERVICE_UUID
from esphome.const import CONF_CHARACTERISTIC_UUID, CONF_ID, CONF_SERVICE_UUID
from .. import ble_client_ns
DEPENDENCIES = ["ble_client"]
CONF_CHARACTERISTIC_UUID = "characteristic_uuid"
CONF_REQUIRE_RESPONSE = "require_response"
BLEBinaryOutput = ble_client_ns.class_(

View File

@@ -2,6 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, ble_client, esp32_ble_tracker
from esphome.const import (
CONF_CHARACTERISTIC_UUID,
CONF_LAMBDA,
CONF_TRIGGER_ID,
CONF_SERVICE_UUID,
@@ -11,7 +12,6 @@ from .. import ble_client_ns
DEPENDENCIES = ["ble_client"]
CONF_CHARACTERISTIC_UUID = "characteristic_uuid"
CONF_DESCRIPTOR_UUID = "descriptor_uuid"
CONF_NOTIFY = "notify"

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

@@ -2,6 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor, ble_client, esp32_ble_tracker
from esphome.const import (
CONF_CHARACTERISTIC_UUID,
CONF_ID,
CONF_TRIGGER_ID,
CONF_SERVICE_UUID,
@@ -11,7 +12,6 @@ from .. import ble_client_ns
DEPENDENCIES = ["ble_client"]
CONF_CHARACTERISTIC_UUID = "characteristic_uuid"
CONF_DESCRIPTOR_UUID = "descriptor_uuid"
CONF_NOTIFY = "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

@@ -1,4 +1,5 @@
#include "bme280.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -28,6 +29,7 @@ static const uint8_t BME280_REGISTER_DIG_H5 = 0xE5;
static const uint8_t BME280_REGISTER_DIG_H6 = 0xE7;
static const uint8_t BME280_REGISTER_CHIPID = 0xD0;
static const uint8_t BME280_REGISTER_RESET = 0xE0;
static const uint8_t BME280_REGISTER_CONTROLHUMID = 0xF2;
static const uint8_t BME280_REGISTER_STATUS = 0xF3;
@@ -39,6 +41,8 @@ static const uint8_t BME280_REGISTER_TEMPDATA = 0xFA;
static const uint8_t BME280_REGISTER_HUMIDDATA = 0xFD;
static const uint8_t BME280_MODE_FORCED = 0b01;
static const uint8_t BME280_SOFT_RESET = 0xB6;
static const uint8_t BME280_STATUS_IM_UPDATE = 0b01;
inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); }
@@ -97,6 +101,28 @@ void BME280Component::setup() {
return;
}
// Send a soft reset.
if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) {
this->mark_failed();
return;
}
// Wait until the NVM data has finished loading.
uint8_t status;
uint8_t retry = 5;
do {
delay(2);
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
ESP_LOGW(TAG, "Error reading status register.");
this->mark_failed();
return;
}
} while ((status & BME280_STATUS_IM_UPDATE) && (--retry));
if (status & BME280_STATUS_IM_UPDATE) {
ESP_LOGW(TAG, "Timeout loading NVM.");
this->mark_failed();
return;
}
// Read calibration
this->calibration_.t1 = read_u16_le_(BME280_REGISTER_DIG_T1);
this->calibration_.t2 = read_s16_le_(BME280_REGISTER_DIG_T2);

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

@@ -102,8 +102,8 @@ async def register_button(var, config):
await setup_button_core_(var, config)
async def new_button(config):
var = cg.new_Pvariable(config[CONF_ID])
async def new_button(config, *args):
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_button(var, config)
return var

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

@@ -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

@@ -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

@@ -131,7 +131,7 @@ void CurrentBasedCover::dump_config() {
ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f);
ESP_LOGCONFIG(TAG, "Obstacle Rollback: %.1f%%", this->obstacle_rollback_ * 100);
if (this->max_duration_ != UINT32_MAX) {
ESP_LOGCONFIG(TAG, "Maximun duration: %.1fs", this->max_duration_ / 1e3f);
ESP_LOGCONFIG(TAG, "Maximum duration: %.1fs", this->max_duration_ / 1e3f);
}
ESP_LOGCONFIG(TAG, "Start sensing delay: %.1fs", this->start_sensing_delay_ / 1e3f);
ESP_LOGCONFIG(TAG, "Malfunction detection: %s", YESNO(this->malfunction_detection_));

View File

@@ -0,0 +1,32 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import CONF_ID
AUTO_LOAD = ["output"]
CODEOWNERS = ["@NickB1"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
dac7678_ns = cg.esphome_ns.namespace("dac7678")
DAC7678Output = dac7678_ns.class_("DAC7678Output", cg.Component, i2c.I2CDevice)
CONF_INTERNAL_REFERENCE = "internal_reference"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DAC7678Output),
cv.Optional(CONF_INTERNAL_REFERENCE, default=False): cv.boolean,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x48))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_internal_reference(config[CONF_INTERNAL_REFERENCE]))
await i2c.register_i2c_device(var, config)
return var

View File

@@ -0,0 +1,83 @@
#include "dac7678_output.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace dac7678 {
static const char *const TAG = "dac7678";
static const uint8_t DAC7678_REG_INPUT_N = 0x00;
static const uint8_t DAC7678_REG_SELECT_UPDATE_N = 0x10;
static const uint8_t DAC7678_REG_WRITE_N_UPDATE_ALL = 0x20;
static const uint8_t DAC7678_REG_WRITE_N_UPDATE_N = 0x30;
static const uint8_t DAC7678_REG_POWER = 0x40;
static const uint8_t DAC7678_REG_CLEAR_CODE = 0x50;
static const uint8_t DAC7678_REG_LDAC = 0x60;
static const uint8_t DAC7678_REG_SOFTWARE_RESET = 0x70;
static const uint8_t DAC7678_REG_INTERNAL_REF_0 = 0x80;
static const uint8_t DAC7678_REG_INTERNAL_REF_1 = 0x90;
void DAC7678Output::setup() {
ESP_LOGCONFIG(TAG, "Setting up DAC7678OutputComponent...");
ESP_LOGV(TAG, "Resetting device...");
// Reset device
if (!this->write_byte_16(DAC7678_REG_SOFTWARE_RESET, 0x0000)) {
ESP_LOGE(TAG, "Reset failed");
this->mark_failed();
return;
} else
ESP_LOGV(TAG, "Reset succeeded");
delayMicroseconds(1000);
// Set internal reference
if (this->internal_reference_) {
if (!this->write_byte_16(DAC7678_REG_INTERNAL_REF_0, 1 << 4)) {
ESP_LOGE(TAG, "Set internal reference failed");
this->mark_failed();
return;
} else
ESP_LOGV(TAG, "Internal reference enabled");
}
}
void DAC7678Output::dump_config() {
if (this->is_failed()) {
ESP_LOGE(TAG, "Setting up DAC7678 failed!");
} else
ESP_LOGCONFIG(TAG, "DAC7678 initialised");
}
void DAC7678Output::register_channel(DAC7678Channel *channel) {
auto c = channel->channel_;
this->min_channel_ = std::min(this->min_channel_, c);
this->max_channel_ = std::max(this->max_channel_, c);
channel->set_parent(this);
ESP_LOGV(TAG, "Registered channel: %01u", channel->channel_);
}
void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) {
if (this->dac_input_reg_[channel] != value) {
ESP_LOGV(TAG, "Channel %01u: input_reg=%04u ", channel, value);
if (!this->write_byte_16(DAC7678_REG_WRITE_N_UPDATE_N | channel, value << 4)) {
this->status_set_warning();
return;
}
}
this->dac_input_reg_[channel] = value;
this->status_clear_warning();
}
void DAC7678Channel::write_state(float state) {
const float input_rounded = roundf(state * this->full_scale_);
auto input = static_cast<uint16_t>(input_rounded);
this->parent_->set_channel_value_(this->channel_, input);
}
} // namespace dac7678
} // namespace esphome

View File

@@ -0,0 +1,55 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace dac7678 {
class DAC7678Output;
class DAC7678Channel : public output::FloatOutput, public Parented<DAC7678Output> {
public:
void set_channel(uint8_t channel) { channel_ = channel; }
protected:
friend class DAC7678Output;
const uint16_t full_scale_ = 0xFFF;
void write_state(float state) override;
uint8_t channel_;
};
/// DAC7678 float output component.
class DAC7678Output : public Component, public i2c::I2CDevice {
public:
DAC7678Output() {}
void register_channel(DAC7678Channel *channel);
void set_internal_reference(const bool value) { this->internal_reference_ = value; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
protected:
friend DAC7678Channel;
bool internal_reference_;
void set_channel_value_(uint8_t channel, uint16_t value);
uint8_t min_channel_{0xFF};
uint8_t max_channel_{0x00};
uint16_t dac_input_reg_[8] = {
0,
};
};
} // namespace dac7678
} // namespace esphome

View File

@@ -0,0 +1,27 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import output
from esphome.const import CONF_CHANNEL, CONF_ID
from . import DAC7678Output, dac7678_ns
DEPENDENCIES = ["dac7678"]
DAC7678Channel = dac7678_ns.class_("DAC7678Channel", output.FloatOutput)
CONF_DAC7678_ID = "dac7678_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(DAC7678Channel),
cv.GenerateID(CONF_DAC7678_ID): cv.use_id(DAC7678Output),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7),
}
)
async def to_code(config):
paren = await cg.get_variable(config[CONF_DAC7678_ID])
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
cg.add(paren.register_channel(var))
await output.register_output(var, config)
return var

View File

@@ -134,7 +134,6 @@ void DallasComponent::update() {
return;
}
if (!sensor->check_scratch_pad()) {
ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", sensor->get_name().c_str());
sensor->publish_state(NAN);
this->status_set_warning();
return;
@@ -241,13 +240,29 @@ bool DallasTemperatureSensor::setup_sensor() {
return true;
}
bool DallasTemperatureSensor::check_scratch_pad() {
bool chksum_validity = (crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]);
bool config_validity = false;
switch (this->get_address8()[0]) {
case DALLAS_MODEL_DS18B20:
config_validity = ((this->scratch_pad_[4] & 0x9F) == 0x1F);
break;
default:
config_validity = ((this->scratch_pad_[4] & 0x10) == 0x10);
}
#ifdef ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGVV(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0],
this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4],
this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8],
crc8(this->scratch_pad_, 8));
#endif
return crc8(this->scratch_pad_, 8) == this->scratch_pad_[8];
if (!chksum_validity) {
ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str());
} else if (!config_validity) {
ESP_LOGW(TAG, "'%s' - Scratch pad config register invalid!", this->get_name().c_str());
}
return chksum_validity && config_validity;
}
float DallasTemperatureSensor::get_temp_c() {
int16_t temp = (int16_t(this->scratch_pad_[1]) << 11) | (int16_t(this->scratch_pad_[0]) << 3);

View File

@@ -1,7 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
from esphome.const import CONF_ID
from esphome.const import CONF_ID, CONF_ADDRESS
CODEOWNERS = ["@s1lvi0"]
DEPENDENCIES = ["uart"]
@@ -15,7 +15,12 @@ DalyBmsComponent = daly_bms.class_(
)
CONFIG_SCHEMA = (
cv.Schema({cv.GenerateID(): cv.declare_id(DalyBmsComponent)})
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DalyBmsComponent),
cv.Optional(CONF_ADDRESS, default=0x80): cv.positive_int,
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.polling_component_schema("30s"))
)
@@ -25,3 +30,4 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
cg.add(var.set_address(config[CONF_ADDRESS]))

View File

@@ -50,7 +50,7 @@ void DalyBmsComponent::request_data_(uint8_t data_id) {
uint8_t request_message[DALY_FRAME_SIZE];
request_message[0] = 0xA5; // Start Flag
request_message[1] = 0x80; // Communication Module Address
request_message[1] = addr_; // Communication Module Address
request_message[2] = data_id; // Data ID
request_message[3] = 0x08; // Data Length (Fixed)
request_message[4] = 0x00; // Empty Data

View File

@@ -69,11 +69,14 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice {
void update() override;
float get_setup_priority() const override;
void set_address(uint8_t address) { this->addr_ = address; }
protected:
void request_data_(uint8_t data_id);
void decode_data_(std::vector<uint8_t> data);
uint8_t addr_;
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *battery_level_sensor_{nullptr};

View File

@@ -348,7 +348,7 @@ async def dfplayer_random_to_code(config, action_id, template_arg, args):
}
),
)
async def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args):
async def dfplayer_is_playing_to_code(config, condition_id, template_arg, args):
var = cg.new_Pvariable(condition_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var

View File

@@ -591,6 +591,18 @@ void Animation::prev_frame() {
}
}
void Animation::set_frame(int frame) {
unsigned abs_frame = abs(frame);
if (abs_frame < this->animation_frame_count_) {
if (frame >= 0) {
this->current_frame_ = frame;
} else {
this->current_frame_ = this->animation_frame_count_ - abs_frame;
}
}
}
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
void DisplayPage::show() { this->parent_->show_page(this); }
void DisplayPage::show_next() { this->next_->show(); }

View File

@@ -85,6 +85,12 @@ enum ImageType {
IMAGE_TYPE_RGB565 = 4,
};
enum DisplayType {
DISPLAY_TYPE_BINARY = 1,
DISPLAY_TYPE_GRAYSCALE = 2,
DISPLAY_TYPE_COLOR = 3,
};
enum DisplayRotation {
DISPLAY_ROTATION_0_DEGREES = 0,
DISPLAY_ROTATION_90_DEGREES = 90,
@@ -361,6 +367,11 @@ class DisplayBuffer {
virtual int get_width_internal() = 0;
DisplayRotation get_rotation() const { return this->rotation_; }
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
* returns the type the display is currently configured to.
*/
virtual DisplayType get_display_type() = 0;
protected:
void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg);
@@ -480,6 +491,12 @@ class Animation : public Image {
void next_frame();
void prev_frame();
/** Selects a specific frame within the animation.
*
* @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame.
*/
void set_frame(int frame);
protected:
int current_frame_;
int animation_frame_count_;

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 euclidean distance (linear distance in rgb cube).
x = (uint32_t) std::abs(tgt_r - plt_r);
y = (uint32_t) std::abs(tgt_g - plt_g);
z = (uint32_t) std::abs(tgt_b - plt_b);
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

@@ -79,10 +79,10 @@ async def to_code(config):
cg.add(var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds))
cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds))
cg.add_define("DSMR_GAS_MBUS_ID", config[CONF_GAS_MBUS_ID])
cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID]))
# DSMR Parser
cg.add_library("glmnet/Dsmr", "0.5")
# Crypto
cg.add_library("rweather/Crypto", "0.2.0")
cg.add_library("rweather/Crypto", "0.4.0")

View File

@@ -171,7 +171,7 @@ void Dsmr::receive_telegram_() {
this->telegram_[this->bytes_read_] = c;
this->bytes_read_++;
// Check for a footer, i.e. exlamation mark, followed by a hex checksum.
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
if (c == '!') {
ESP_LOGV(TAG, "Footer of telegram found");
this->footer_found_ = true;

View File

@@ -4,9 +4,10 @@
#include "esphome/core/component.h"
#include <map>
#include <memory>
#include <set>
#include <map>
#include <vector>
class UDP;

View File

@@ -199,7 +199,7 @@ void ENS210Component::update() {
});
}
// Extracts measurement 'data' and 'status' from a 'val' obtained from measurment.
// Extracts measurement 'data' and 'status' from a 'val' obtained from measurement.
void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status) {
*data = (val >> 0) & 0xffff;
int valid = (val >> 16) & 0x1;

View File

@@ -521,6 +521,33 @@ ESP32_BOARD_PINS = {
},
"lolin32": {"LED": 5},
"lolin32_lite": {"LED": 22},
"lolin_c3_mini": {
"TX": 21,
"RX": 20,
"SDA": 8,
"SCL": 10,
"SS": 5,
"MOSI": 4,
"MISO": 3,
"SCK": 2,
"A0": 0,
"A1": 1,
"A2": 2,
"A3": 3,
"A4": 4,
"A5": 5,
"D0": 1,
"D1": 10,
"D2": 8,
"D3": 7,
"D4": 6,
"D5": 2,
"D6": 3,
"D7": 4,
"D8": 5,
"LED": 7,
"BUTTON": 9,
},
"lolin_d32": {"LED": 5, "_VBAT": 35},
"lolin_d32_pro": {"LED": 5, "_VBAT": 35},
"lopy": {
@@ -1026,6 +1053,7 @@ BOARD_TO_VARIANT = {
"labplus_mpython": VARIANT_ESP32,
"lolin32_lite": VARIANT_ESP32,
"lolin32": VARIANT_ESP32,
"lolin_c3_mini": VARIANT_ESP32C3,
"lolin_d32_pro": VARIANT_ESP32,
"lolin_d32": VARIANT_ESP32,
"lopy4": VARIANT_ESP32,

View File

@@ -42,7 +42,7 @@ def esp32_validate_gpio_pin(value):
"See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins",
value,
)
if value in (20, 24, 28, 29, 30, 31):
if value in (24, 28, 29, 30, 31):
# These pins are not exposed in GPIO mux (reason unknown)
# but they're missing from IO_MUX list in datasheet
raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.")

View File

@@ -36,6 +36,7 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
save.key = key;
save.data.assign(data, data + len);
s_pending_save.emplace_back(save);
ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %d", key.c_str(), len);
return true;
}
bool load(uint8_t *data, size_t len) override {
@@ -65,6 +66,8 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key.c_str(), esp_err_to_name(err));
return false;
} else {
ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %d", key.c_str(), len);
}
return true;
}
@@ -73,7 +76,6 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
class ESP32Preferences : public ESPPreferences {
public:
uint32_t nvs_handle;
uint32_t current_offset = 0;
void open() {
nvs_flash_init();
@@ -97,12 +99,9 @@ class ESP32Preferences : public ESPPreferences {
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = nvs_handle;
current_offset += length;
uint32_t keyval = current_offset ^ type;
char keybuf[16];
snprintf(keybuf, sizeof(keybuf), "%d", keyval);
pref->key = keybuf; // copied to std::string
uint32_t keyval = type;
pref->key = str_sprintf("%u", keyval);
return ESPPreferenceObject(pref);
}
@@ -111,22 +110,40 @@ class ESP32Preferences : public ESPPreferences {
if (s_pending_save.empty())
return true;
ESP_LOGD(TAG, "Saving preferences to flash...");
ESP_LOGD(TAG, "Saving %d preferences to flash...", s_pending_save.size());
// goal try write all pending saves even if one fails
bool any_failed = false;
int cached = 0, written = 0, failed = 0;
esp_err_t last_err = ESP_OK;
std::string last_key{};
// 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());
ESP_LOGV(TAG, "sync: key: %s, len: %d", save.key.c_str(), save.data.size());
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(),
esp_err_to_name(err));
failed++;
last_err = err;
last_key = save.key;
continue;
}
written++;
} else {
ESP_LOGV(TAG, "NVS data not changed skipping %s len=%u", save.key.c_str(), save.data.size());
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
}
ESP_LOGD(TAG, "Saving %d preferences to flash: %d cached, %d written, %d failed", cached + written + failed, cached,
written, failed);
if (failed > 0) {
ESP_LOGD(TAG, "Error saving %d preferences to flash. Last error=%s for key=%s", failed, esp_err_to_name(last_err),
last_key.c_str());
}
// note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
esp_err_t err = nvs_commit(nvs_handle);
@@ -135,7 +152,23 @@ class ESP32Preferences : public ESPPreferences {
return false;
}
return !any_failed;
return failed == 0;
}
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.resize(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;
}
};

View File

@@ -100,7 +100,12 @@ void ESP32BLETracker::loop() {
found = true;
if (client->state() == ClientState::DISCOVERED) {
esp_ble_gap_stop_scanning();
if (xSemaphoreTake(this->scan_end_lock_, 10L / portTICK_PERIOD_MS)) {
#ifdef USE_ARDUINO
constexpr TickType_t block_time = 10L / portTICK_PERIOD_MS;
#else
constexpr TickType_t block_time = 0L; // PR #3594
#endif
if (xSemaphoreTake(this->scan_end_lock_, block_time)) {
xSemaphoreGive(this->scan_end_lock_);
}
}

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();
@@ -305,12 +317,11 @@ void ESP32Camera::update_camera_parameters() {
s->set_gainceiling(s, (gainceiling_t) this->agc_gain_ceiling_);
/* update white balance mode */
s->set_wb_mode(s, (int) this->wb_mode_); // 0 to 4
/* update test patern */
/* update test pattern */
s->set_colorbar(s, this->test_pattern_);
}
/* ---------------- Internal methods ---------------- */
uint32_t ESP32Camera::hash_base() { return 3010542557UL; }
bool ESP32Camera::has_requested_image_() const { return this->single_requesters_ || this->stream_requesters_; }
bool ESP32Camera::can_return_image_() const { return this->current_image_.use_count() == 1; }
void ESP32Camera::framebuffer_task(void *pv) {

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

@@ -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

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

View File

@@ -0,0 +1,157 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import binary_sensor, cover
from esphome.const import (
CONF_ASSUMED_STATE,
CONF_CLOSE_ACTION,
CONF_CLOSE_DURATION,
CONF_CLOSE_ENDSTOP,
CONF_ID,
CONF_OPEN_ACTION,
CONF_OPEN_DURATION,
CONF_OPEN_ENDSTOP,
CONF_STOP_ACTION,
CONF_MAX_DURATION,
CONF_UPDATE_INTERVAL,
)
CONF_OPEN_SENSOR = "open_sensor"
CONF_CLOSE_SENSOR = "close_sensor"
CONF_OPEN_OBSTACLE_SENSOR = "open_obstacle_sensor"
CONF_CLOSE_OBSTACLE_SENSOR = "close_obstacle_sensor"
CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop"
CONF_INFER_ENDSTOP_FROM_MOVEMENT = "infer_endstop_from_movement"
CONF_DIRECTION_CHANGE_WAIT_TIME = "direction_change_wait_time"
CONF_ACCELERATION_WAIT_TIME = "acceleration_wait_time"
CONF_OBSTACLE_ROLLBACK = "obstacle_rollback"
endstop_ns = cg.esphome_ns.namespace("feedback")
FeedbackCover = endstop_ns.class_("FeedbackCover", cover.Cover, cg.Component)
def validate_infer_endstop(config):
if config[CONF_INFER_ENDSTOP_FROM_MOVEMENT] is True:
if config[CONF_HAS_BUILT_IN_ENDSTOP] is False:
raise cv.Invalid(
f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} can only be set if {CONF_HAS_BUILT_IN_ENDSTOP} is also set"
)
if CONF_OPEN_SENSOR not in config:
raise cv.Invalid(
f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} cannot be set if movement sensors are not supplied"
)
if CONF_OPEN_ENDSTOP in config or CONF_CLOSE_ENDSTOP in config:
raise cv.Invalid(
f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} cannot be set if endstop sensors are supplied"
)
return config
CONFIG_FEEDBACK_COVER_BASE_SCHEMA = cover.COVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(FeedbackCover),
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
cv.Optional(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
cv.Optional(CONF_OPEN_SENSOR): cv.use_id(binary_sensor.BinarySensor),
cv.Optional(CONF_OPEN_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor),
cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
cv.Optional(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
cv.Optional(CONF_CLOSE_SENSOR): cv.use_id(binary_sensor.BinarySensor),
cv.Optional(CONF_CLOSE_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor),
cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean,
cv.Optional(CONF_ASSUMED_STATE): cv.boolean,
cv.Optional(
CONF_UPDATE_INTERVAL, "1000ms"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_INFER_ENDSTOP_FROM_MOVEMENT, False): cv.boolean,
cv.Optional(
CONF_DIRECTION_CHANGE_WAIT_TIME
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_ACCELERATION_WAIT_TIME, "0s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage,
},
).extend(cv.COMPONENT_SCHEMA)
CONFIG_SCHEMA = cv.All(
CONFIG_FEEDBACK_COVER_BASE_SCHEMA,
cv.has_none_or_all_keys(CONF_OPEN_SENSOR, CONF_CLOSE_SENSOR),
validate_infer_endstop,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await cover.register_cover(var, config)
# STOP
await automation.build_automation(
var.get_stop_trigger(), [], config[CONF_STOP_ACTION]
)
# OPEN
await automation.build_automation(
var.get_open_trigger(), [], config[CONF_OPEN_ACTION]
)
cg.add(var.set_open_duration(config[CONF_OPEN_DURATION]))
if CONF_OPEN_ENDSTOP in config:
bin = await cg.get_variable(config[CONF_OPEN_ENDSTOP])
cg.add(var.set_open_endstop(bin))
if CONF_OPEN_SENSOR in config:
bin = await cg.get_variable(config[CONF_OPEN_SENSOR])
cg.add(var.set_open_sensor(bin))
if CONF_OPEN_OBSTACLE_SENSOR in config:
bin = await cg.get_variable(config[CONF_OPEN_OBSTACLE_SENSOR])
cg.add(var.set_open_obstacle_sensor(bin))
# CLOSE
await automation.build_automation(
var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]
)
cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION]))
if CONF_CLOSE_ENDSTOP in config:
bin = await cg.get_variable(config[CONF_CLOSE_ENDSTOP])
cg.add(var.set_close_endstop(bin))
if CONF_CLOSE_SENSOR in config:
bin = await cg.get_variable(config[CONF_CLOSE_SENSOR])
cg.add(var.set_close_sensor(bin))
if CONF_CLOSE_OBSTACLE_SENSOR in config:
bin = await cg.get_variable(config[CONF_CLOSE_OBSTACLE_SENSOR])
cg.add(var.set_close_obstacle_sensor(bin))
# OTHER
if CONF_MAX_DURATION in config:
cg.add(var.set_max_duration(config[CONF_MAX_DURATION]))
cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP]))
if CONF_ASSUMED_STATE in config:
cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE]))
else:
cg.add(
var.set_assumed_state(
not (
(CONF_CLOSE_ENDSTOP in config and CONF_OPEN_ENDSTOP in config)
or config[CONF_INFER_ENDSTOP_FROM_MOVEMENT]
)
)
)
cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
cg.add(var.set_infer_endstop(config[CONF_INFER_ENDSTOP_FROM_MOVEMENT]))
if CONF_DIRECTION_CHANGE_WAIT_TIME in config:
cg.add(
var.set_direction_change_waittime(config[CONF_DIRECTION_CHANGE_WAIT_TIME])
)
cg.add(var.set_acceleration_wait_time(config[CONF_ACCELERATION_WAIT_TIME]))
cg.add(var.set_obstacle_rollback(config[CONF_OBSTACLE_ROLLBACK]))

View File

@@ -0,0 +1,445 @@
#include "feedback_cover.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace feedback {
static const char *const TAG = "feedback.cover";
using namespace esphome::cover;
void FeedbackCover::setup() {
auto restore = this->restore_state_();
if (restore.has_value()) {
restore->apply(this);
} else {
// if no other information, assume half open
this->position = 0.5f;
}
this->current_operation = COVER_OPERATION_IDLE;
#ifdef USE_BINARY_SENSOR
// if available, get position from endstop sensors
if (this->open_endstop_ != nullptr && this->open_endstop_->state) {
this->position = COVER_OPEN;
} else if (this->close_endstop_ != nullptr && this->close_endstop_->state) {
this->position = COVER_CLOSED;
}
// if available, get moving state from sensors
if (this->open_feedback_ != nullptr && this->open_feedback_->state) {
this->current_operation = COVER_OPERATION_OPENING;
} else if (this->close_feedback_ != nullptr && this->close_feedback_->state) {
this->current_operation = COVER_OPERATION_CLOSING;
}
#endif
this->last_recompute_time_ = this->start_dir_time_ = millis();
}
CoverTraits FeedbackCover::get_traits() {
auto traits = CoverTraits();
traits.set_supports_position(true);
traits.set_supports_toggle(true);
traits.set_is_assumed_state(this->assumed_state_);
return traits;
}
void FeedbackCover::dump_config() {
LOG_COVER("", "Endstop Cover", this);
ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f);
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Open Endstop", this->open_endstop_);
LOG_BINARY_SENSOR(" ", "Open Feedback", this->open_feedback_);
LOG_BINARY_SENSOR(" ", "Open Obstacle", this->open_obstacle_);
#endif
ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f);
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_);
LOG_BINARY_SENSOR(" ", "Close Feedback", this->close_feedback_);
LOG_BINARY_SENSOR(" ", "Close Obstacle", this->close_obstacle_);
#endif
if (this->has_built_in_endstop_) {
ESP_LOGCONFIG(TAG, " Has builtin endstop: YES");
}
if (this->infer_endstop_) {
ESP_LOGCONFIG(TAG, " Infer endstop from movement: YES");
}
if (this->max_duration_ < UINT32_MAX) {
ESP_LOGCONFIG(TAG, " Max Duration: %.1fs", this->max_duration_ / 1e3f);
}
if (this->direction_change_waittime_.has_value()) {
ESP_LOGCONFIG(TAG, " Direction change wait time: %.1fs", *this->direction_change_waittime_ / 1e3f);
}
if (this->acceleration_wait_time_) {
ESP_LOGCONFIG(TAG, " Acceleration wait time: %.1fs", this->acceleration_wait_time_ / 1e3f);
}
#ifdef USE_BINARY_SENSOR
if (this->obstacle_rollback_ && (this->open_obstacle_ != nullptr || this->close_obstacle_ != nullptr)) {
ESP_LOGCONFIG(TAG, " Obstacle rollback: %.1f%%", this->obstacle_rollback_ * 100);
}
#endif
}
#ifdef USE_BINARY_SENSOR
void FeedbackCover::set_open_sensor(binary_sensor::BinarySensor *open_feedback) {
this->open_feedback_ = open_feedback;
// setup callbacks to react to sensor changes
open_feedback->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
this->recompute_position_();
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) {
this->endstop_reached_(true);
}
this->set_current_operation_(state ? COVER_OPERATION_OPENING : COVER_OPERATION_IDLE, false);
});
}
void FeedbackCover::set_close_sensor(binary_sensor::BinarySensor *close_feedback) {
this->close_feedback_ = close_feedback;
close_feedback->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
this->recompute_position_();
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) {
this->endstop_reached_(false);
}
this->set_current_operation_(state ? COVER_OPERATION_CLOSING : COVER_OPERATION_IDLE, false);
});
}
void FeedbackCover::set_open_endstop(binary_sensor::BinarySensor *open_endstop) {
this->open_endstop_ = open_endstop;
open_endstop->add_on_state_callback([this](bool state) {
if (state) {
this->endstop_reached_(true);
}
});
}
void FeedbackCover::set_close_endstop(binary_sensor::BinarySensor *close_endstop) {
this->close_endstop_ = close_endstop;
close_endstop->add_on_state_callback([this](bool state) {
if (state) {
this->endstop_reached_(false);
}
});
}
#endif
void FeedbackCover::endstop_reached_(bool open_endstop) {
const uint32_t now = millis();
this->position = open_endstop ? COVER_OPEN : COVER_CLOSED;
// only act if endstop activated while moving in the right direction, in case we are coming back
// from a position slightly past the endpoint
if (this->current_trigger_operation_ == (open_endstop ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING)) {
float dur = (now - this->start_dir_time_) / 1e3f;
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), open_endstop ? "Open" : "Close", dur);
// if there is no external mechanism, stop the cover
if (!this->has_built_in_endstop_) {
this->start_direction_(COVER_OPERATION_IDLE);
} else {
this->set_current_operation_(COVER_OPERATION_IDLE, true);
}
}
// always sync position and publish
this->publish_state();
this->last_publish_time_ = now;
}
void FeedbackCover::set_current_operation_(cover::CoverOperation operation, bool is_triggered) {
if (is_triggered) {
this->current_trigger_operation_ = operation;
}
// if it is setting the actual operation (not triggered one) or
// if we don't have moving sensor, we operate in optimistic mode, assuming actions take place immediately
// thus, triggered operation always sets current operation.
// otherwise, current operation comes from sensor, and may differ from requested operation
// this might be from delays or complex actions, or because the movement was not trigger by the component
// but initiated externally
#ifdef USE_BINARY_SENSOR
if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr))
#endif
{
auto now = millis();
this->current_operation = operation;
this->start_dir_time_ = this->last_recompute_time_ = now;
this->publish_state();
this->last_publish_time_ = now;
}
}
#ifdef USE_BINARY_SENSOR
void FeedbackCover::set_close_obstacle_sensor(binary_sensor::BinarySensor *close_obstacle) {
this->close_obstacle_ = close_obstacle;
close_obstacle->add_on_state_callback([this](bool state) {
if (state && (this->current_operation == COVER_OPERATION_CLOSING ||
this->current_trigger_operation_ == COVER_OPERATION_CLOSING)) {
ESP_LOGD(TAG, "'%s' - Close obstacle detected.", this->name_.c_str());
this->start_direction_(COVER_OPERATION_IDLE);
if (this->obstacle_rollback_) {
this->target_position_ = clamp(this->position + this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
this->start_direction_(COVER_OPERATION_OPENING);
}
}
});
}
void FeedbackCover::set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle) {
this->open_obstacle_ = open_obstacle;
open_obstacle->add_on_state_callback([this](bool state) {
if (state && (this->current_operation == COVER_OPERATION_OPENING ||
this->current_trigger_operation_ == COVER_OPERATION_OPENING)) {
ESP_LOGD(TAG, "'%s' - Open obstacle detected.", this->name_.c_str());
this->start_direction_(COVER_OPERATION_IDLE);
if (this->obstacle_rollback_) {
this->target_position_ = clamp(this->position - this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
this->start_direction_(COVER_OPERATION_CLOSING);
}
}
});
}
#endif
void FeedbackCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = millis();
// Recompute position every loop cycle
this->recompute_position_();
// if we initiated the move, check if we reached position or max time
// (stoping from endstop sensor is handled in callback)
if (this->current_trigger_operation_ != COVER_OPERATION_IDLE) {
if (this->is_at_target_()) {
if (this->has_built_in_endstop_ &&
(this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) {
// Don't trigger stop, let the cover stop by itself.
this->set_current_operation_(COVER_OPERATION_IDLE, true);
} else {
this->start_direction_(COVER_OPERATION_IDLE);
}
} else if (now - this->start_dir_time_ > this->max_duration_) {
ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", this->name_.c_str());
this->start_direction_(COVER_OPERATION_IDLE);
}
}
// update current position at requested interval, regardless of who started the movement
// so that we also update UI if there was an external movement
// don´t save intermediate positions
if (now - this->last_publish_time_ > this->update_interval_) {
this->publish_state(false);
this->last_publish_time_ = now;
}
}
void FeedbackCover::control(const CoverCall &call) {
// stop action logic
if (call.get_stop()) {
this->start_direction_(COVER_OPERATION_IDLE);
} else if (call.get_toggle().has_value()) {
// toggle action logic: OPEN - STOP - CLOSE
if (this->current_trigger_operation_ != COVER_OPERATION_IDLE) {
this->start_direction_(COVER_OPERATION_IDLE);
} else {
if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) {
this->target_position_ = COVER_OPEN;
this->start_direction_(COVER_OPERATION_OPENING);
} else {
this->target_position_ = COVER_CLOSED;
this->start_direction_(COVER_OPERATION_CLOSING);
}
}
} else if (call.get_position().has_value()) {
// go to position action
auto pos = *call.get_position();
if (pos == this->position) {
// already at target,
// for covers with built in end stop, if we don´t have sensors we should send the command again
// to make sure the assumed state is not wrong
if (this->has_built_in_endstop_ && ((pos == COVER_OPEN
#ifdef USE_BINARY_SENSOR
&& this->open_endstop_ == nullptr
#endif
&& !this->infer_endstop_) ||
(pos == COVER_CLOSED
#ifdef USE_BINARY_SENSOR
&& this->close_endstop_ == nullptr
#endif
&& !this->infer_endstop_))) {
this->target_position_ = pos;
this->start_direction_(pos == COVER_CLOSED ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING);
} else if (this->current_operation != COVER_OPERATION_IDLE ||
this->current_trigger_operation_ != COVER_OPERATION_IDLE) {
// if we are moving, stop
this->start_direction_(COVER_OPERATION_IDLE);
}
} else {
this->target_position_ = pos;
this->start_direction_(pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING);
}
}
}
void FeedbackCover::stop_prev_trigger_() {
if (this->direction_change_waittime_.has_value()) {
this->cancel_timeout("direction_change");
}
if (this->prev_command_trigger_ != nullptr) {
this->prev_command_trigger_->stop_action();
this->prev_command_trigger_ = nullptr;
}
}
bool FeedbackCover::is_at_target_() const {
// if initiated externally, current operation might be different from
// operation that was triggered, thus evaluate position against what was asked
switch (this->current_trigger_operation_) {
case COVER_OPERATION_OPENING:
return this->position >= this->target_position_;
case COVER_OPERATION_CLOSING:
return this->position <= this->target_position_;
case COVER_OPERATION_IDLE:
return this->current_operation == COVER_OPERATION_IDLE;
default:
return true;
}
}
void FeedbackCover::start_direction_(CoverOperation dir) {
Trigger<> *trig;
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *obstacle{nullptr};
#endif
switch (dir) {
case COVER_OPERATION_IDLE:
trig = this->stop_trigger_;
break;
case COVER_OPERATION_OPENING:
this->last_operation_ = dir;
trig = this->open_trigger_;
#ifdef USE_BINARY_SENSOR
obstacle = this->open_obstacle_;
#endif
break;
case COVER_OPERATION_CLOSING:
this->last_operation_ = dir;
trig = this->close_trigger_;
#ifdef USE_BINARY_SENSOR
obstacle = this->close_obstacle_;
#endif
break;
default:
return;
}
this->stop_prev_trigger_();
#ifdef USE_BINARY_SENSOR
// check if there is an obstacle to start the new operation -> abort without any change
// the case when an obstacle appears while moving is handled in the callback
if (obstacle != nullptr && obstacle->state) {
ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? "Open" : "Close");
return;
}
#endif
// if we are moving and need to move in the opposite direction
// check if we have a wait time
if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE &&
this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) {
ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str());
this->start_direction_(COVER_OPERATION_IDLE);
this->set_timeout("direction_change", *this->direction_change_waittime_,
[this, dir]() { this->start_direction_(dir); });
} else {
this->set_current_operation_(dir, true);
this->prev_command_trigger_ = trig;
ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? "OPEN"
: dir == COVER_OPERATION_CLOSING ? "CLOSE"
: "STOP");
trig->trigger();
}
}
void FeedbackCover::recompute_position_() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = millis();
float dir;
float action_dur;
float min_pos;
float max_pos;
// endstop sensors update position from their callbacks, and sets the fully open/close value
// If we have endstop, estimation never reaches the fully open/closed state.
// but if movement continues past corresponding endstop (inertia), keep the fully open/close state
switch (this->current_operation) {
case COVER_OPERATION_OPENING:
dir = 1.0f;
action_dur = this->open_duration_;
min_pos = COVER_CLOSED;
max_pos = (
#ifdef USE_BINARY_SENSOR
this->open_endstop_ != nullptr ||
#endif
this->infer_endstop_) &&
this->position < COVER_OPEN
? 0.99f
: COVER_OPEN;
break;
case COVER_OPERATION_CLOSING:
dir = -1.0f;
action_dur = this->close_duration_;
min_pos = (
#ifdef USE_BINARY_SENSOR
this->close_endstop_ != nullptr ||
#endif
this->infer_endstop_) &&
this->position > COVER_CLOSED
? 0.01f
: COVER_CLOSED;
max_pos = COVER_OPEN;
break;
default:
return;
}
// check if we have an acceleration_wait_time, and remove from position computation
if (now > (this->start_dir_time_ + this->acceleration_wait_time_)) {
this->position +=
dir * (now - std::max(this->start_dir_time_ + this->acceleration_wait_time_, this->last_recompute_time_)) /
(action_dur - this->acceleration_wait_time_);
this->position = clamp(this->position, min_pos, max_pos);
}
this->last_recompute_time_ = now;
}
} // namespace feedback
} // namespace esphome

View File

@@ -0,0 +1,90 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#include "esphome/components/cover/cover.h"
namespace esphome {
namespace feedback {
class FeedbackCover : public cover::Cover, public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; };
Trigger<> *get_open_trigger() const { return this->open_trigger_; }
Trigger<> *get_close_trigger() const { return this->close_trigger_; }
Trigger<> *get_stop_trigger() const { return this->stop_trigger_; }
#ifdef USE_BINARY_SENSOR
void set_open_endstop(binary_sensor::BinarySensor *open_endstop);
void set_open_sensor(binary_sensor::BinarySensor *open_feedback);
void set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle);
void set_close_endstop(binary_sensor::BinarySensor *close_endstop);
void set_close_sensor(binary_sensor::BinarySensor *close_feedback);
void set_close_obstacle_sensor(binary_sensor::BinarySensor *close_obstacle);
#endif
void set_open_duration(uint32_t duration) { this->open_duration_ = duration; }
void set_close_duration(uint32_t duration) { this->close_duration_ = duration; }
void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; }
void set_assumed_state(bool value) { this->assumed_state_ = value; }
void set_max_duration(uint32_t max_duration) { this->max_duration_ = max_duration; }
void set_obstacle_rollback(float obstacle_rollback) { this->obstacle_rollback_ = obstacle_rollback; }
void set_update_interval(uint32_t interval) { this->update_interval_ = interval; }
void set_infer_endstop(bool infer_endstop) { this->infer_endstop_ = infer_endstop; }
void set_direction_change_waittime(uint32_t waittime) { this->direction_change_waittime_ = waittime; }
void set_acceleration_wait_time(uint32_t waittime) { this->acceleration_wait_time_ = waittime; }
cover::CoverTraits get_traits() override;
protected:
void control(const cover::CoverCall &call) override;
void stop_prev_trigger_();
bool is_at_target_() const;
void start_direction_(cover::CoverOperation dir);
void update_operation_(cover::CoverOperation dir);
void endstop_reached_(bool open_endstop);
void recompute_position_();
void set_current_operation_(cover::CoverOperation operation, bool is_triggered);
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *open_endstop_{nullptr};
binary_sensor::BinarySensor *close_endstop_{nullptr};
binary_sensor::BinarySensor *open_feedback_{nullptr};
binary_sensor::BinarySensor *close_feedback_{nullptr};
binary_sensor::BinarySensor *open_obstacle_{nullptr};
binary_sensor::BinarySensor *close_obstacle_{nullptr};
#endif
Trigger<> *open_trigger_{new Trigger<>()};
Trigger<> *close_trigger_{new Trigger<>()};
Trigger<> *stop_trigger_{new Trigger<>()};
uint32_t open_duration_{0};
uint32_t close_duration_{0};
uint32_t max_duration_{UINT32_MAX};
optional<uint32_t> direction_change_waittime_{};
uint32_t acceleration_wait_time_{0};
bool has_built_in_endstop_{false};
bool assumed_state_{false};
bool infer_endstop_{false};
float obstacle_rollback_{0};
cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING};
cover::CoverOperation current_trigger_operation_{cover::COVER_OPERATION_IDLE};
Trigger<> *prev_command_trigger_{nullptr};
uint32_t last_recompute_time_{0};
uint32_t start_dir_time_{0};
uint32_t last_publish_time_{0};
float target_position_{0};
uint32_t update_interval_{1000};
};
} // namespace feedback
} // namespace esphome

View File

@@ -44,7 +44,14 @@ template<typename T> class RestoringGlobalsComponent : public Component {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void loop() override {
void loop() override { store_value_(); }
void on_shutdown() override { store_value_(); }
void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; }
protected:
void store_value_() {
int diff = memcmp(&this->value_, &this->prev_value_, sizeof(T));
if (diff != 0) {
this->rtc_.save(&this->value_);
@@ -52,9 +59,6 @@ template<typename T> class RestoringGlobalsComponent : public Component {
}
}
void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; }
protected:
T value_{};
T prev_value_{};
uint32_t name_hash_{};

View File

@@ -118,7 +118,7 @@ def _relocate_fields_to_subfolder(config, subfolder, subschema):
fields = [k.schema for k in subschema.schema.keys()]
fields.remove(CONF_ID)
if subfolder in config:
# Ensure no ambigious fields in base of config
# Ensure no ambiguous fields in base of config
for f in fields:
if f in config:
raise cv.Invalid(

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",
}

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