├── .clang-format ├── .gitignore ├── README.md └── components ├── audio_player ├── CustomAudioFileSourceHTTPStream.cpp ├── CustomAudioFileSourceHTTPStream.h ├── __init__.py ├── audio_player.cpp ├── audio_player.h └── media_player.py ├── bt_presence ├── __init__.py ├── binary_sensor.py ├── bt_presence_device.cpp └── bt_presence_device.h ├── bt_rssi ├── __init__.py ├── bt_rssi_sensor.cpp ├── bt_rssi_sensor.h └── sensor.py ├── cem5855h ├── __init__.py ├── binary_sensor.py ├── cem5855h.cpp └── cem5855h.h ├── dlt645 ├── __init__.py ├── dlt645.cpp └── dlt645.h ├── esp32_ble_tracker ├── __init__.py ├── automation.h ├── esp32_ble_tracker.cpp ├── esp32_ble_tracker.h └── queue.h ├── esp32_bt_tracker ├── __init__.py ├── automation.h ├── esp32_bt_tracker.cpp ├── esp32_bt_tracker.h └── queue.h ├── fpm383c ├── __init__.py ├── fpm383c.cpp ├── fpm383c.h └── light.py ├── ptx_yk1 ├── __init__.py ├── binary_sensor.py ├── ptx_yk1_device.cpp └── ptx_yk1_device.h ├── rf_bridge_cc1101 ├── __init__.py ├── binary_sensor.py ├── cc1101_def.h ├── remote_receiver.h ├── remote_receiver_esp32.cpp ├── remote_receiver_esp8266.cpp ├── remote_transmitter.cpp ├── remote_transmitter.h ├── remote_transmitter_esp32.cpp ├── remote_transmitter_esp8266.cpp ├── rf_bridge_cc1101.cpp └── rf_bridge_cc1101.h ├── ssw_tds ├── __init__.py ├── sensor.py ├── ssw_tds.cpp └── ssw_tds.h ├── telnet ├── __init__.py ├── telnet.cpp └── telnet.h ├── xiaomi_m1st500 ├── __init__.py ├── sensor.py ├── xiaomi_m1st500.cpp ├── xiaomi_m1st500.h ├── xiaomi_toothbrush_ble.cpp └── xiaomi_toothbrush_ble.h ├── xiaomi_remote ├── __init__.py ├── xiaomi_remote.cpp ├── xiaomi_remote.h ├── xiaomi_remote_ble.cpp └── xiaomi_remote_ble.h └── xiaomi_smoke_detector ├── __init__.py ├── sensor.py ├── xiaomi_smoke_ble.cpp ├── xiaomi_smoke_ble.h ├── xiaomi_smoke_detector.cpp └── xiaomi_smoke_detector.h /.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | AccessModifierOffset: -1 3 | AlignAfterOpenBracket: Align 4 | AlignConsecutiveAssignments: false 5 | AlignConsecutiveDeclarations: false 6 | AlignEscapedNewlines: DontAlign 7 | AlignOperands: true 8 | AlignTrailingComments: true 9 | AllowAllParametersOfDeclarationOnNextLine: true 10 | AllowShortBlocksOnASingleLine: false 11 | AllowShortCaseLabelsOnASingleLine: false 12 | AllowShortFunctionsOnASingleLine: All 13 | AllowShortIfStatementsOnASingleLine: false 14 | AllowShortLoopsOnASingleLine: false 15 | AlwaysBreakAfterReturnType: None 16 | AlwaysBreakBeforeMultilineStrings: false 17 | AlwaysBreakTemplateDeclarations: MultiLine 18 | BinPackArguments: true 19 | BinPackParameters: true 20 | BraceWrapping: 21 | AfterClass: false 22 | AfterControlStatement: false 23 | AfterEnum: false 24 | AfterFunction: false 25 | AfterNamespace: false 26 | AfterObjCDeclaration: false 27 | AfterStruct: false 28 | AfterUnion: false 29 | AfterExternBlock: false 30 | BeforeCatch: false 31 | BeforeElse: false 32 | IndentBraces: false 33 | SplitEmptyFunction: true 34 | SplitEmptyRecord: true 35 | SplitEmptyNamespace: true 36 | BreakBeforeBinaryOperators: None 37 | BreakBeforeBraces: Attach 38 | BreakBeforeInheritanceComma: false 39 | BreakInheritanceList: BeforeColon 40 | BreakBeforeTernaryOperators: true 41 | BreakConstructorInitializersBeforeComma: false 42 | BreakConstructorInitializers: BeforeColon 43 | BreakAfterJavaFieldAnnotations: false 44 | BreakStringLiterals: true 45 | ColumnLimit: 120 46 | CommentPragmas: '^ IWYU pragma:' 47 | CompactNamespaces: false 48 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 49 | ConstructorInitializerIndentWidth: 4 50 | ContinuationIndentWidth: 4 51 | Cpp11BracedListStyle: true 52 | DerivePointerAlignment: false 53 | DisableFormat: false 54 | ExperimentalAutoDetectBinPacking: false 55 | FixNamespaceComments: true 56 | ForEachMacros: 57 | - foreach 58 | - Q_FOREACH 59 | - BOOST_FOREACH 60 | IncludeBlocks: Preserve 61 | IncludeCategories: 62 | - Regex: '^' 63 | Priority: 2 64 | - Regex: '^<.*\.h>' 65 | Priority: 1 66 | - Regex: '^<.*' 67 | Priority: 2 68 | - Regex: '.*' 69 | Priority: 3 70 | IncludeIsMainRegex: '([-_](test|unittest))?$' 71 | IndentCaseLabels: true 72 | IndentPPDirectives: None 73 | IndentWidth: 2 74 | IndentWrappedFunctionNames: false 75 | KeepEmptyLinesAtTheStartOfBlocks: false 76 | MacroBlockBegin: '' 77 | MacroBlockEnd: '' 78 | MaxEmptyLinesToKeep: 1 79 | NamespaceIndentation: None 80 | PenaltyBreakAssignment: 2 81 | PenaltyBreakBeforeFirstCallParameter: 1 82 | PenaltyBreakComment: 300 83 | PenaltyBreakFirstLessLess: 120 84 | PenaltyBreakString: 1000 85 | PenaltyBreakTemplateDeclaration: 10 86 | PenaltyExcessCharacter: 1000000 87 | PenaltyReturnTypeOnItsOwnLine: 2000 88 | PointerAlignment: Right 89 | RawStringFormats: 90 | - Language: Cpp 91 | Delimiters: 92 | - cc 93 | - CC 94 | - cpp 95 | - Cpp 96 | - CPP 97 | - 'c++' 98 | - 'C++' 99 | CanonicalDelimiter: '' 100 | BasedOnStyle: google 101 | - Language: TextProto 102 | Delimiters: 103 | - pb 104 | - PB 105 | - proto 106 | - PROTO 107 | EnclosingFunctions: 108 | - EqualsProto 109 | - EquivToProto 110 | - PARSE_PARTIAL_TEXT_PROTO 111 | - PARSE_TEST_PROTO 112 | - PARSE_TEXT_PROTO 113 | - ParseTextOrDie 114 | - ParseTextProtoOrDie 115 | CanonicalDelimiter: '' 116 | BasedOnStyle: google 117 | ReflowComments: true 118 | SortIncludes: false 119 | SortUsingDeclarations: false 120 | SpaceAfterCStyleCast: true 121 | SpaceAfterTemplateKeyword: false 122 | SpaceBeforeAssignmentOperators: true 123 | SpaceBeforeCpp11BracedList: false 124 | SpaceBeforeCtorInitializerColon: true 125 | SpaceBeforeInheritanceColon: true 126 | SpaceBeforeParens: ControlStatements 127 | SpaceBeforeRangeBasedForLoopColon: true 128 | SpaceInEmptyParentheses: false 129 | SpacesBeforeTrailingComments: 2 130 | SpacesInAngles: false 131 | SpacesInContainerLiterals: false 132 | SpacesInCStyleCastParentheses: false 133 | SpacesInParentheses: false 134 | SpacesInSquareBrackets: false 135 | Standard: Auto 136 | TabWidth: 2 137 | UseTab: Never 138 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /components/audio_player/CustomAudioFileSourceHTTPStream.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | CustomAudioFileSourceHTTPStream 3 | Streaming HTTP source 4 | 5 | Copyright (C) 2017 Earle F. Philhower, III 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | #include "CustomAudioFileSourceHTTPStream.h" 22 | 23 | namespace esphome { 24 | namespace audio_player { 25 | static const char *header[] = {"Content-Type", "Transfer-Encoding"}; 26 | 27 | CustomAudioFileSourceHTTPStream::CustomAudioFileSourceHTTPStream() { 28 | pos = 0; 29 | chunked = false; 30 | chunked_size = 0; 31 | reconnectTries = 0; 32 | saveURL[0] = 0; 33 | } 34 | 35 | CustomAudioFileSourceHTTPStream::CustomAudioFileSourceHTTPStream(const char *url) { 36 | CustomAudioFileSourceHTTPStream(); 37 | open(url); 38 | } 39 | 40 | bool CustomAudioFileSourceHTTPStream::open(const char *url) { 41 | pos = 0; 42 | chunked = false; 43 | chunked_size = 0; 44 | http.begin(client, url); 45 | http.collectHeaders(header, 2); 46 | http.setReuse(true); 47 | #ifndef USE_ESP32 48 | http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); 49 | #endif 50 | int code = http.GET(); 51 | if (code != HTTP_CODE_OK) { 52 | http.end(); 53 | cb.st(STATUS_HTTPFAIL, PSTR("Can't open HTTP request")); 54 | return false; 55 | } 56 | size = http.getSize(); 57 | chunked = http.header("Transfer-Encoding") == "chunked"; 58 | strncpy(saveURL, url, sizeof(saveURL)); 59 | saveURL[sizeof(saveURL) - 1] = 0; 60 | return true; 61 | } 62 | 63 | CustomAudioFileSourceHTTPStream::~CustomAudioFileSourceHTTPStream() { http.end(); } 64 | 65 | uint32_t CustomAudioFileSourceHTTPStream::read(void *data, uint32_t len) { 66 | if (data == NULL) { 67 | audioLogger->printf_P(PSTR("ERROR! CustomAudioFileSourceHTTPStream::read passed NULL data\n")); 68 | return 0; 69 | } 70 | return readInternal(data, len, false); 71 | } 72 | 73 | uint32_t CustomAudioFileSourceHTTPStream::readNonBlock(void *data, uint32_t len) { 74 | if (data == NULL) { 75 | audioLogger->printf_P(PSTR("ERROR! CustomAudioFileSourceHTTPStream::readNonBlock passed NULL data\n")); 76 | return 0; 77 | } 78 | return readInternal(data, len, true); 79 | } 80 | 81 | uint32_t CustomAudioFileSourceHTTPStream::readInternal(void *data, uint32_t len, bool nonBlock) { 82 | retry: 83 | if (!http.connected()) { 84 | cb.st(STATUS_DISCONNECTED, PSTR("Stream disconnected")); 85 | http.end(); 86 | for (int i = 0; i < reconnectTries; i++) { 87 | char buff[64]; 88 | sprintf_P(buff, PSTR("Attempting to reconnect, try %d"), i); 89 | cb.st(STATUS_RECONNECTING, buff); 90 | delay(reconnectDelayMs); 91 | if (open(saveURL)) { 92 | cb.st(STATUS_RECONNECTED, PSTR("Stream reconnected")); 93 | break; 94 | } 95 | } 96 | if (!http.connected()) { 97 | cb.st(STATUS_DISCONNECTED, PSTR("Unable to reconnect")); 98 | return 0; 99 | } 100 | } 101 | if ((size > 0) && (pos >= size)) 102 | return 0; 103 | 104 | WiFiClient *stream = http.getStreamPtr(); 105 | 106 | // Can't read past EOF... 107 | if ((size > 0) && (len > (uint32_t) (pos - size))) 108 | len = pos - size; 109 | 110 | if (!nonBlock) { 111 | int start = millis(); 112 | while ((stream->available() < (int) len) && (millis() - start < 500)) 113 | yield(); 114 | } 115 | 116 | size_t avail = stream->available(); 117 | if (!nonBlock && !avail) { 118 | cb.st(STATUS_NODATA, PSTR("No stream data available")); 119 | http.end(); 120 | goto retry; 121 | } 122 | if (avail == 0) 123 | return 0; 124 | if (avail < len) 125 | len = avail; 126 | 127 | int read = 0; 128 | if (!chunked) { 129 | read = stream->read(reinterpret_cast(data), len); 130 | pos += read; 131 | return read; 132 | } 133 | 134 | if (chunked_size < 0) { 135 | return 0; // EOF 136 | } 137 | 138 | if (chunked_size == 0) { 139 | String chunkHeader = stream->readStringUntil('\n'); 140 | if (chunkHeader.length() <= 0) { 141 | audioLogger->printf_P(PSTR("ERROR! CustomAudioFileSourceHTTPStream::no chunked len!")); 142 | return 0; 143 | } 144 | chunkHeader.trim(); 145 | chunked_size = (uint32_t) strtol((const char *) chunkHeader.c_str(), NULL, 16); 146 | if (chunked_size == 0) { 147 | http.end(); 148 | chunked_size = -1; 149 | } 150 | } 151 | 152 | if (chunked_size > 0) { 153 | if (len > chunked_size) 154 | len = chunked_size; 155 | read = stream->read(reinterpret_cast(data), len); 156 | pos += read; 157 | chunked_size -= read; 158 | } 159 | 160 | if (chunked_size == 0) { 161 | char buf[2]; 162 | auto trailing_seq_len = stream->readBytes((uint8_t *) buf, 2); 163 | if (trailing_seq_len != 2 || buf[0] != '\r' || buf[1] != '\n') { 164 | audioLogger->printf_P(PSTR("ERROR! CustomAudioFileSourceHTTPStream::chunked error!")); 165 | http.end(); 166 | chunked_size = -1; 167 | } 168 | } 169 | 170 | return read; 171 | } 172 | 173 | bool CustomAudioFileSourceHTTPStream::seek(int32_t pos, int dir) { 174 | audioLogger->printf_P(PSTR("ERROR! CustomAudioFileSourceHTTPStream::seek not implemented!")); 175 | (void) pos; 176 | (void) dir; 177 | return false; 178 | } 179 | 180 | bool CustomAudioFileSourceHTTPStream::close() { 181 | http.end(); 182 | return true; 183 | } 184 | 185 | bool CustomAudioFileSourceHTTPStream::isOpen() { return http.connected(); } 186 | 187 | uint32_t CustomAudioFileSourceHTTPStream::getSize() { return size; } 188 | 189 | uint32_t CustomAudioFileSourceHTTPStream::getPos() { return pos; } 190 | 191 | } // namespace audio_player 192 | } // namespace esphome -------------------------------------------------------------------------------- /components/audio_player/CustomAudioFileSourceHTTPStream.h: -------------------------------------------------------------------------------- 1 | /* 2 | CustomAudioFileSourceHTTPStream 3 | Connect to a HTTP based streaming service 4 | 5 | Copyright (C) 2017 Earle F. Philhower, III 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | #pragma once 22 | #include 23 | #include 24 | #ifdef USE_ESP32 25 | #include 26 | #else 27 | #include 28 | #endif 29 | #include "AudioFileSource.h" 30 | 31 | namespace esphome { 32 | namespace audio_player { 33 | 34 | class CustomAudioFileSourceHTTPStream : public AudioFileSource { 35 | friend class AudioFileSourceICYStream; 36 | 37 | public: 38 | CustomAudioFileSourceHTTPStream(); 39 | CustomAudioFileSourceHTTPStream(const char *url); 40 | virtual ~CustomAudioFileSourceHTTPStream() override; 41 | 42 | virtual bool open(const char *url) override; 43 | virtual uint32_t read(void *data, uint32_t len) override; 44 | virtual uint32_t readNonBlock(void *data, uint32_t len) override; 45 | virtual bool seek(int32_t pos, int dir) override; 46 | virtual bool close() override; 47 | virtual bool isOpen() override; 48 | virtual uint32_t getSize() override; 49 | virtual uint32_t getPos() override; 50 | bool SetReconnect(int tries, int delayms) { 51 | reconnectTries = tries; 52 | reconnectDelayMs = delayms; 53 | return true; 54 | } 55 | void useHTTP10() { http.useHTTP10(true); } 56 | std::string contentType() { return std::string(http.header("Content-Type").c_str()); } 57 | 58 | enum { STATUS_HTTPFAIL = 2, STATUS_DISCONNECTED, STATUS_RECONNECTING, STATUS_RECONNECTED, STATUS_NODATA }; 59 | 60 | private: 61 | virtual uint32_t readInternal(void *data, uint32_t len, bool nonBlock); 62 | WiFiClient client; 63 | HTTPClient http; 64 | int pos; 65 | int size; 66 | int reconnectTries; 67 | int reconnectDelayMs; 68 | char saveURL[128]; 69 | int chunked_size; 70 | bool chunked; 71 | }; 72 | 73 | } // namespace audio_player 74 | } // namespace esphome -------------------------------------------------------------------------------- /components/audio_player/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanh7/esphome-custom-components/78a9afe3c2164fee312d50f547a7c0cea73e6d86/components/audio_player/__init__.py -------------------------------------------------------------------------------- /components/audio_player/audio_player.cpp: -------------------------------------------------------------------------------- 1 | #include "audio_player.h" 2 | #include "esphome/core/log.h" 3 | #include 4 | 5 | #include "AudioGeneratorWAV.h" 6 | #include "AudioGeneratorAAC.h" 7 | #include "AudioGeneratorMP3.h" 8 | #include "AudioFileSourceHTTPStream.h" 9 | #include "CustomAudioFileSourceHTTPStream.h" 10 | 11 | namespace esphome { 12 | namespace audio_player { 13 | 14 | static const char *const TAG = "audio_player"; 15 | 16 | // Called when there's a warning or error (like a buffer underflow or decode hiccup) 17 | void StatusCallback(void *cbData, int code, const char *string) { 18 | const char *ptr = reinterpret_cast(cbData); 19 | // Note that the string may be in PROGMEM, so copy it to RAM for printf 20 | char s1[64]; 21 | strncpy_P(s1, string, sizeof(s1)); 22 | s1[sizeof(s1) - 1] = 0; 23 | ESP_LOGD(TAG, "STATUS(%s) '%d' = '%s'", ptr, code, s1); 24 | } 25 | 26 | void AudioMediaPlayer::setup() { 27 | if (out_ == nullptr) { 28 | this->mark_failed(); 29 | return; 30 | } 31 | 32 | this->volume_contorller_->set_volume(base_volume_); 33 | audioLogger = &Serial; 34 | this->state = media_player::MEDIA_PLAYER_STATE_IDLE; 35 | } 36 | 37 | void AudioMediaPlayer::dump_config() { 38 | ESP_LOGCONFIG(TAG, "Audio Player:"); 39 | ESP_LOGCONFIG(TAG, " Buffer Size: %d", this->buffer_size_); 40 | ESP_LOGCONFIG(TAG, " Base Volume: %d%%", (int) (this->base_volume_ * 100)); 41 | ESP_LOGCONFIG(TAG, " Current Volume: %d%%", (int) this->volume * 100); 42 | ESP_LOGCONFIG(TAG, " Muted: %s", this->muted_ ? "yes" : "no"); 43 | if (this->ext_info_ != nullptr) { 44 | this->ext_info_->dump_config(); 45 | } 46 | } 47 | 48 | void AudioMediaPlayer::loop() { 49 | if (generator_ == nullptr) { 50 | return; 51 | } 52 | if (!generator_->isRunning()) { 53 | return; 54 | } 55 | if (!this->pause_ && generator_->loop()) { 56 | return; 57 | } 58 | generator_->stop(); 59 | if (buffer_ != NULL) { 60 | delete buffer_; 61 | buffer_ = NULL; 62 | } 63 | if (file_ != NULL) { 64 | delete file_; 65 | file_ = NULL; 66 | } 67 | 68 | this->state = media_player::MEDIA_PLAYER_STATE_IDLE; 69 | this->publish_state(); 70 | } 71 | 72 | void AudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { 73 | if (call.get_media_url().has_value()) { 74 | this->play(call.get_media_url().value()); 75 | } 76 | if (call.get_volume().has_value()) { 77 | ESP_LOGD(TAG, "volume set %.2f", call.get_volume().value()); 78 | this->set_volume(call.get_volume().value()); 79 | this->unmute_(); 80 | } 81 | if (call.get_command().has_value()) { 82 | switch (call.get_command().value()) { 83 | /* case media_player::MEDIA_PLAYER_COMMAND_PLAY: 84 | this->pause_ = false; 85 | this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; 86 | break; 87 | case media_player::MEDIA_PLAYER_COMMAND_PAUSE: 88 | this->pause_ = true; 89 | this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; 90 | break; 91 | case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: 92 | if (this->pause_) { 93 | this->pause_ = false; 94 | this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; 95 | } else { 96 | this->pause_ = true; 97 | this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; 98 | } 99 | break; */ 100 | case media_player::MEDIA_PLAYER_COMMAND_STOP: 101 | this->stop(); 102 | this->state = media_player::MEDIA_PLAYER_STATE_IDLE; 103 | break; 104 | case media_player::MEDIA_PLAYER_COMMAND_MUTE: 105 | this->mute_(); 106 | break; 107 | case media_player::MEDIA_PLAYER_COMMAND_UNMUTE: 108 | this->unmute_(); 109 | break; 110 | case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: { 111 | float new_volume = this->volume + 0.1f; 112 | if (new_volume > 1.0f) 113 | new_volume = 1.0f; 114 | this->set_volume(new_volume); 115 | this->unmute_(); 116 | break; 117 | } 118 | case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: { 119 | float new_volume = this->volume - 0.1f; 120 | if (new_volume < 0.0f) 121 | new_volume = 0.0f; 122 | this->set_volume(new_volume); 123 | this->unmute_(); 124 | break; 125 | } 126 | } 127 | } 128 | this->publish_state(); 129 | } 130 | 131 | media_player::MediaPlayerTraits AudioMediaPlayer::get_traits() { 132 | auto traits = media_player::MediaPlayerTraits(); 133 | traits.set_supports_pause(false); 134 | return traits; 135 | }; 136 | 137 | void AudioMediaPlayer::stop() { 138 | if (generator_ != NULL) { 139 | if (generator_->isRunning()) { 140 | generator_->stop(); 141 | } 142 | delete generator_; 143 | generator_ = NULL; 144 | } 145 | 146 | if (buffer_ != NULL) { 147 | delete buffer_; 148 | buffer_ = NULL; 149 | } 150 | if (file_ != NULL) { 151 | delete file_; 152 | file_ = NULL; 153 | } 154 | } 155 | 156 | 157 | void AudioMediaPlayer::play(const std::string &url) { 158 | stop(); 159 | 160 | ESP_LOGD(TAG, "play url %s", url.c_str()); 161 | if (url.rfind("http://", 0) != 0) { 162 | ESP_LOGE(TAG, "Unsupported protocol"); 163 | return; 164 | } 165 | 166 | CustomAudioFileSourceHTTPStream *http_stream = new CustomAudioFileSourceHTTPStream(url.c_str()); 167 | 168 | std::string content_type = http_stream->contentType(); 169 | if (content_type.empty()) { 170 | ESP_LOGE(TAG, "http: no Content-Type"); 171 | generator_ = new AudioGeneratorWAV(); 172 | } else if (content_type == "audio/wav" || content_type == "audio/x-wav") { 173 | generator_ = new AudioGeneratorWAV(); 174 | } else if (content_type == "audio/mp3" || content_type == "audio/mpeg") { 175 | generator_ = new AudioGeneratorMP3(); 176 | } else if (content_type == "audio/aac") { 177 | generator_ = new AudioGeneratorAAC(); 178 | } else { 179 | ESP_LOGW(TAG, "http: Unsupported Content-type:%s", content_type.c_str()); 180 | generator_ = new AudioGeneratorWAV(); 181 | } 182 | 183 | this->file_ = http_stream; 184 | this->buffer_ = new AudioFileSourceBuffer(file_, buffer_size_); 185 | this->generator_ = new AudioGeneratorWAV(); 186 | 187 | file_->RegisterStatusCB(StatusCallback, (void *) "file"); 188 | buffer_->RegisterStatusCB(StatusCallback, (void *) "buffer"); 189 | generator_->RegisterStatusCB(StatusCallback, (void *) "decoder"); 190 | 191 | if (!generator_->begin(buffer_, out_)) { 192 | ESP_LOGE(TAG, "play failed"); 193 | stop(); 194 | this->state = media_player::MEDIA_PLAYER_STATE_IDLE; 195 | this->publish_state(); 196 | return; 197 | } 198 | 199 | this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; 200 | this->publish_state(); 201 | } 202 | 203 | void AudioMediaPlayer::mute_() { 204 | this->muted_ = true; 205 | volume_contorller_->set_volume(0); 206 | } 207 | 208 | void AudioMediaPlayer::unmute_() { 209 | this->muted_ = false; 210 | volume_contorller_->set_volume(this->volume * base_volume_); 211 | } 212 | 213 | 214 | void AudioMediaPlayer::set_volume(float volume) { 215 | this->volume = volume; 216 | if (this->muted_) { 217 | volume_contorller_->set_volume(0); 218 | } else { 219 | volume_contorller_->set_volume(this->volume * base_volume_); 220 | } 221 | } 222 | 223 | 224 | void PlayerOutputI2S::set_pins(InternalGPIOPin *bclk, InternalGPIOPin *wclk, InternalGPIOPin *dout) { 225 | this->bclk_ = bclk; 226 | this->wclk_ = wclk; 227 | this->dout_ = dout; 228 | this->SetPinout(bclk->get_pin(), wclk->get_pin(), dout->get_pin()); 229 | } 230 | 231 | void PlayerOutputI2S::dump_config() { 232 | ESP_LOGCONFIG(TAG, " OutPut: i2s"); 233 | LOG_PIN(" BCLK Pin: ", this->bclk_); 234 | LOG_PIN(" WCLK Pin: ", this->wclk_); 235 | LOG_PIN(" DOUT Pin: ", this->dout_); 236 | } 237 | 238 | void PlayerOutputI2SNoDAC::dump_config() { ESP_LOGCONFIG(TAG, " Output: i2sNoDAC"); } 239 | 240 | 241 | } // namespace audio_player 242 | } // namespace esphome 243 | -------------------------------------------------------------------------------- /components/audio_player/audio_player.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "esphome/core/component.h" 3 | #include "esphome/core/hal.h" 4 | #include "esphome/core/automation.h" 5 | #include "AudioOutputI2SNoDAC.h" 6 | #include "AudioGenerator.h" 7 | #include "AudioFileSource.h" 8 | #include "AudioFileSourceBuffer.h" 9 | #include "esphome/components/media_player/media_player.h" 10 | 11 | namespace esphome { 12 | namespace audio_player { 13 | 14 | enum status_type { 15 | STATUS_IDLE = 0, 16 | STATUS_PLAYING, 17 | STATUS_PAUSE, 18 | STATUS_MUTE, 19 | STATUS_UNMUTE, 20 | STATUS_VOLUME_ZERO = 100, 21 | STATUS_VOLUME_MAX = 200 22 | }; 23 | 24 | class Dumpable { 25 | public: 26 | virtual void dump_config() = 0; 27 | }; 28 | 29 | class Volume { 30 | public: 31 | virtual void set_volume(float volume) = 0; 32 | }; 33 | 34 | template class VolumeOutput : public T, public Volume { 35 | public: 36 | using T::T; 37 | virtual bool ConsumeSample(int16_t sample[2]) override { 38 | int16_t sample_adjust[2]; 39 | double sample_rescale = (double) sample[0] * (double) volume_; 40 | sample_rescale = (sample_rescale < -32768) ? -32768 : sample_rescale; 41 | sample_rescale = (sample_rescale > 32767) ? 32767 : sample_rescale; 42 | sample_adjust[0] = (int16_t) (sample_rescale); 43 | sample_rescale = (double) sample[1] * (double) volume_; 44 | sample_rescale = (sample_rescale < -32768) ? -32768 : sample_rescale; 45 | sample_rescale = (sample_rescale > 32767) ? 32767 : sample_rescale; 46 | sample_adjust[1] = (int16_t) (sample_rescale); 47 | return T::ConsumeSample(sample_adjust); 48 | } 49 | virtual void set_volume(float volume) { volume_ = volume; } 50 | 51 | protected: 52 | float volume_{1.0f}; 53 | }; 54 | 55 | class PlayerOutputI2S : public VolumeOutput, public Dumpable { 56 | public: 57 | void set_pins(InternalGPIOPin *bclk, InternalGPIOPin *wclk, InternalGPIOPin *dout); 58 | virtual void dump_config() override; 59 | 60 | protected: 61 | InternalGPIOPin *bclk_; 62 | InternalGPIOPin *wclk_; 63 | InternalGPIOPin *dout_; 64 | }; 65 | 66 | class PlayerOutputI2SNoDAC : public VolumeOutput, public Dumpable { 67 | public: 68 | virtual void dump_config() override; 69 | }; 70 | 71 | 72 | class AudioMediaPlayer : public Component, public media_player::MediaPlayer { 73 | public: 74 | void setup() override; 75 | void loop() override; 76 | void dump_config() override; 77 | media_player::MediaPlayerTraits get_traits() override; 78 | bool is_muted() const override { return this->muted_; } 79 | 80 | void play(const std::string &url); 81 | void stop(); 82 | 83 | void set_output(AudioOutput *out) { this->out_ = out; } 84 | void set_volume_controller(Volume *volume_contorller) { this->volume_contorller_ = volume_contorller; } 85 | void set_ext_info(Dumpable *info) { this->ext_info_ = info; } 86 | void set_buffer_size(uint32_t size) { this->buffer_size_ = size; } 87 | void set_base_volume(float base_volume) { this->base_volume_ = base_volume; } 88 | void set_volume(float volume); 89 | 90 | float get_setup_priority() const override { return setup_priority::LATE; } 91 | 92 | protected: 93 | void control(const media_player::MediaPlayerCall &call) override; 94 | void mute_(); 95 | void unmute_(); 96 | bool pause_{false}; 97 | bool muted_{false}; 98 | 99 | float base_volume_{1.0f}; 100 | uint32_t buffer_size_{1024}; 101 | 102 | AudioGenerator *generator_{NULL}; 103 | AudioFileSource *file_{NULL}; 104 | AudioFileSourceBuffer *buffer_{NULL}; 105 | AudioOutput *out_{NULL}; 106 | Volume *volume_contorller_{NULL}; 107 | Dumpable *ext_info_{NULL}; 108 | }; 109 | 110 | } // namespace audio_player 111 | } // namespace esphome 112 | -------------------------------------------------------------------------------- /components/audio_player/media_player.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | from esphome.components import media_player 3 | import esphome.config_validation as cv 4 | from esphome import pins 5 | from esphome.const import ( 6 | CONF_ID, 7 | CONF_BUFFER_SIZE 8 | ) 9 | from esphome.core import CORE 10 | 11 | CODEOWNERS = ["@ryan"] 12 | 13 | AUTO_LOAD = ["network"] 14 | 15 | CONF_VOLUME = "volume" 16 | CONF_I2S = "i2s" 17 | CONF_I2S_NO_DAC = "i2sNoDAC" 18 | CONF_BCLK = "bclk" 19 | CONF_WCLK = "wclk" 20 | CONF_DOUT = "dout" 21 | 22 | audio_player_ns = cg.esphome_ns.namespace("audio_player") 23 | AudioMediaPlayer = audio_player_ns.class_( 24 | "AudioMediaPlayer", cg.Component, media_player.MediaPlayer 25 | ) 26 | 27 | PlayerOutputI2SNoDAC = audio_player_ns.class_( 28 | "PlayerOutputI2SNoDAC" 29 | ) 30 | 31 | PlayerOutputI2S = audio_player_ns.class_( 32 | "PlayerOutputI2S" 33 | ) 34 | 35 | CONFIG_SCHEMA = cv.All( 36 | cv.Schema( 37 | { 38 | cv.GenerateID(): cv.declare_id(AudioMediaPlayer), 39 | cv.Optional(CONF_VOLUME, default=100): cv.All( 40 | cv.percentage_int, cv.Range(min=1, max=1000) 41 | ), 42 | cv.SplitDefault( 43 | CONF_BUFFER_SIZE, esp32="10240B", esp8266="1024B" 44 | ): cv.validate_bytes, 45 | cv.Optional(CONF_I2S_NO_DAC): cv.All( 46 | cv.Schema( 47 | { 48 | cv.GenerateID(): cv.declare_id(PlayerOutputI2SNoDAC), 49 | } 50 | ), 51 | ), 52 | cv.Optional(CONF_I2S): cv.All( 53 | cv.Schema( 54 | { 55 | cv.GenerateID(): cv.declare_id(PlayerOutputI2S), 56 | cv.Required(CONF_BCLK): pins.internal_gpio_output_pin_schema, 57 | cv.Required(CONF_WCLK): pins.internal_gpio_output_pin_schema, 58 | cv.Required(CONF_DOUT): pins.internal_gpio_output_pin_schema 59 | } 60 | ), 61 | ) 62 | } 63 | ) 64 | .extend(media_player.MEDIA_PLAYER_SCHEMA) 65 | .extend(cv.COMPONENT_SCHEMA), 66 | cv.has_exactly_one_key(CONF_I2S_NO_DAC, CONF_I2S) 67 | ) 68 | 69 | 70 | async def to_code(config): 71 | var = cg.new_Pvariable(config[CONF_ID]) 72 | 73 | cg.add_library("SPI", None) 74 | if CORE.is_esp32: 75 | cg.add_library("WiFi", None) 76 | cg.add_library("WiFiClientSecure", None) 77 | cg.add_library("FS", None) 78 | if CORE.is_esp8266: 79 | cg.add_library("ESP8266SdFat", None) 80 | cg.add_library("SDFS", None) 81 | cg.add_library("ESP8266Audio", "1.9.7") 82 | 83 | await cg.register_component(var, config) 84 | await media_player.register_media_player(var, config) 85 | 86 | cg.add(var.set_base_volume(config[CONF_VOLUME] / 100)) 87 | cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) 88 | 89 | if CONF_I2S_NO_DAC in config: 90 | params = config[CONF_I2S_NO_DAC] 91 | output = cg.new_Pvariable(params[CONF_ID]) 92 | 93 | elif CONF_I2S in config: 94 | params = config[CONF_I2S] 95 | output = cg.new_Pvariable(params[CONF_ID]) 96 | pin_blck = await cg.gpio_pin_expression(params[CONF_BCLK]) 97 | pin_wclk = await cg.gpio_pin_expression(params[CONF_WCLK]) 98 | pin_dout = await cg.gpio_pin_expression(params[CONF_DOUT]) 99 | cg.add(output.set_pins(pin_blck, pin_wclk, pin_dout)) 100 | 101 | cg.add(var.set_output(output)) 102 | cg.add(var.set_volume_controller(output)) 103 | cg.add(var.set_ext_info(output)) 104 | -------------------------------------------------------------------------------- /components/bt_presence/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanh7/esphome-custom-components/78a9afe3c2164fee312d50f547a7c0cea73e6d86/components/bt_presence/__init__.py -------------------------------------------------------------------------------- /components/bt_presence/binary_sensor.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import binary_sensor, esp32_bt_tracker 4 | from esphome.const import CONF_MAC_ADDRESS, CONF_ID 5 | 6 | DEPENDENCIES = ["esp32_bt_tracker"] 7 | 8 | ble_presence_ns = cg.esphome_ns.namespace("bt_presence") 9 | BLEPresenceDevice = ble_presence_ns.class_( 10 | "BTPresenceDevice", 11 | binary_sensor.BinarySensor, 12 | cg.Component, 13 | esp32_bt_tracker.ESPBTDeviceListener, 14 | ) 15 | 16 | CONFIG_SCHEMA = cv.All( 17 | binary_sensor.BINARY_SENSOR_SCHEMA.extend( 18 | { 19 | cv.GenerateID(): cv.declare_id(BLEPresenceDevice), 20 | cv.Required(CONF_MAC_ADDRESS): cv.mac_address, 21 | } 22 | ) 23 | .extend(esp32_bt_tracker.ESP_BT_DEVICE_SCHEMA) 24 | .extend(cv.COMPONENT_SCHEMA) 25 | ) 26 | 27 | 28 | async def to_code(config): 29 | var = cg.new_Pvariable(config[CONF_ID]) 30 | await cg.register_component(var, config) 31 | await esp32_bt_tracker.register_bt_device(var, config) 32 | await binary_sensor.register_binary_sensor(var, config) 33 | 34 | if CONF_MAC_ADDRESS in config: 35 | cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) 36 | 37 | -------------------------------------------------------------------------------- /components/bt_presence/bt_presence_device.cpp: -------------------------------------------------------------------------------- 1 | #include "bt_presence_device.h" 2 | #include "esphome/core/log.h" 3 | 4 | #ifdef USE_ESP32 5 | 6 | namespace esphome { 7 | namespace bt_presence { 8 | 9 | static const char *const TAG = "bt_presence"; 10 | 11 | void BTPresenceDevice::dump_config() { LOG_BINARY_SENSOR("", "BT Presence", this); } 12 | 13 | } // namespace bt_presence 14 | } // namespace esphome 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /components/bt_presence/bt_presence_device.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/esp32_bt_tracker/esp32_bt_tracker.h" 5 | #include "esphome/components/binary_sensor/binary_sensor.h" 6 | 7 | #ifdef USE_ESP32 8 | 9 | namespace esphome { 10 | namespace bt_presence { 11 | 12 | class BTPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, 13 | public esp32_bt_tracker::ESPBTDeviceListener, 14 | public Component { 15 | public: 16 | void set_address(uint64_t address) { this->address_ = address; } 17 | void on_scan_end() override { 18 | if (!this->found_) 19 | this->publish_state(false); 20 | this->found_ = false; 21 | } 22 | bool parse_device(const esp32_bt_tracker::ESPBTDevice &device) override { 23 | if (device.address_uint64() == this->address_) { 24 | this->publish_state(true); 25 | this->found_ = true; 26 | return true; 27 | } 28 | 29 | return false; 30 | } 31 | void dump_config() override; 32 | float get_setup_priority() const override { return setup_priority::DATA; } 33 | 34 | protected: 35 | bool found_{false}; 36 | uint64_t address_; 37 | }; 38 | 39 | } // namespace bt_presence 40 | } // namespace esphome 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /components/bt_rssi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanh7/esphome-custom-components/78a9afe3c2164fee312d50f547a7c0cea73e6d86/components/bt_rssi/__init__.py -------------------------------------------------------------------------------- /components/bt_rssi/bt_rssi_sensor.cpp: -------------------------------------------------------------------------------- 1 | #include "bt_rssi_sensor.h" 2 | #include "esphome/core/log.h" 3 | 4 | #ifdef USE_ESP32 5 | 6 | namespace esphome { 7 | namespace bt_rssi { 8 | 9 | static const char *const TAG = "bt_rssi"; 10 | 11 | void BTRSSISensor::dump_config() { LOG_SENSOR("", "BT RSSI Sensor", this); } 12 | 13 | } // namespace ble_rssi 14 | } // namespace esphome 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /components/bt_rssi/bt_rssi_sensor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/esp32_bt_tracker/esp32_bt_tracker.h" 5 | #include "esphome/components/sensor/sensor.h" 6 | 7 | #ifdef USE_ESP32 8 | 9 | namespace esphome { 10 | namespace bt_rssi { 11 | 12 | class BTRSSISensor : public sensor::Sensor, public esp32_bt_tracker::ESPBTDeviceListener, public Component { 13 | public: 14 | void set_address(uint64_t address) { 15 | this->address_ = address; 16 | } 17 | 18 | void on_scan_end() override { 19 | if (!this->found_) 20 | this->publish_state(NAN); 21 | this->found_ = false; 22 | } 23 | 24 | bool parse_device(const esp32_bt_tracker::ESPBTDevice &device) override { 25 | if (device.address_uint64() == this->address_) { 26 | this->publish_state(device.get_rssi()); 27 | this->found_ = true; 28 | return true; 29 | } 30 | return false; 31 | } 32 | void dump_config() override; 33 | float get_setup_priority() const override { return setup_priority::DATA; } 34 | 35 | protected: 36 | bool found_{false}; 37 | uint64_t address_; 38 | }; 39 | 40 | } // namespace bt_rssi 41 | } // namespace esphome 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /components/bt_rssi/sensor.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import sensor, esp32_bt_tracker 4 | from esphome.const import ( 5 | CONF_MAC_ADDRESS, 6 | CONF_ID, 7 | DEVICE_CLASS_SIGNAL_STRENGTH, 8 | STATE_CLASS_MEASUREMENT, 9 | UNIT_DECIBEL, 10 | ICON_EMPTY, 11 | ) 12 | 13 | DEPENDENCIES = ["esp32_bt_tracker"] 14 | 15 | ble_rssi_ns = cg.esphome_ns.namespace("bt_rssi") 16 | BLERSSISensor = ble_rssi_ns.class_( 17 | "BTRSSISensor", sensor.Sensor, cg.Component, esp32_bt_tracker.ESPBTDeviceListener 18 | ) 19 | 20 | CONFIG_SCHEMA = cv.All( 21 | sensor.sensor_schema( 22 | UNIT_DECIBEL, 23 | ICON_EMPTY, 24 | 0, 25 | DEVICE_CLASS_SIGNAL_STRENGTH, 26 | STATE_CLASS_MEASUREMENT, 27 | ) 28 | .extend( 29 | { 30 | cv.GenerateID(): cv.declare_id(BLERSSISensor), 31 | cv.Required(CONF_MAC_ADDRESS): cv.mac_address, 32 | } 33 | ) 34 | .extend(esp32_bt_tracker.ESP_BT_DEVICE_SCHEMA) 35 | .extend(cv.COMPONENT_SCHEMA), 36 | ) 37 | 38 | 39 | async def to_code(config): 40 | var = cg.new_Pvariable(config[CONF_ID]) 41 | await cg.register_component(var, config) 42 | await esp32_bt_tracker.register_bt_device(var, config) 43 | await sensor.register_sensor(var, config) 44 | 45 | if CONF_MAC_ADDRESS in config: 46 | cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) 47 | 48 | -------------------------------------------------------------------------------- /components/cem5855h/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanh7/esphome-custom-components/78a9afe3c2164fee312d50f547a7c0cea73e6d86/components/cem5855h/__init__.py -------------------------------------------------------------------------------- /components/cem5855h/binary_sensor.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import uart, binary_sensor, number 4 | from esphome.const import ( 5 | CONF_ID, 6 | DEVICE_CLASS_OCCUPANCY, 7 | DEVICE_CLASS_MOVING, 8 | DEVICE_CLASS_MOTION, 9 | CONF_THRESHOLD, 10 | CONF_MOTION, 11 | CONF_MIN_VALUE, 12 | CONF_MAX_VALUE, 13 | CONF_VALUE, 14 | CONF_STEP, 15 | ) 16 | 17 | CONF_MOV = "moving" 18 | CONF_OCC = "occupancy" 19 | 20 | AUTO_LOAD = ["number"] 21 | DEPENDENCIES = ["uart"] 22 | 23 | cem5855h_ns = cg.esphome_ns.namespace("cem5855h") 24 | CEM5855hComponent = cem5855h_ns.class_( 25 | "CEM5855hComponent", cg.Component, uart.UARTDevice 26 | ) 27 | ThresholdSensor = cem5855h_ns.class_( 28 | "ThresholdSensor", binary_sensor.BinarySensor 29 | ) 30 | ThresholdMovingNumber = cem5855h_ns.class_( 31 | "ThresholdMovingNumber", number.Number 32 | ) 33 | ThresholdOccupancyNumber = cem5855h_ns.class_( 34 | "ThresholdOccupancyNumber", number.Number 35 | ) 36 | 37 | 38 | def validate_min_max(config): 39 | if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: 40 | raise cv.Invalid("max_value must be greater than min_value") 41 | return config 42 | 43 | 44 | MOVING_NUMBER_SCHEMA = cv.Any( 45 | cv.int_range(min=0, max=99999), 46 | number.NUMBER_SCHEMA.extend( 47 | { 48 | cv.GenerateID(): cv.declare_id(ThresholdMovingNumber), 49 | cv.Optional(CONF_VALUE, default=250): cv.float_, 50 | cv.Optional(CONF_MAX_VALUE, default=3000): cv.float_, 51 | cv.Optional(CONF_MIN_VALUE, default=10): cv.float_, 52 | cv.Optional(CONF_STEP, default=10): cv.positive_float, 53 | } 54 | ).extend(cv.COMPONENT_SCHEMA), 55 | validate_min_max, 56 | ) 57 | 58 | OCCUPANCY_NUMBER_SCHEMA = cv.Any( 59 | cv.int_range(min=0, max=99999), 60 | number.NUMBER_SCHEMA.extend( 61 | { 62 | cv.GenerateID(): cv.declare_id(ThresholdOccupancyNumber), 63 | cv.Optional(CONF_VALUE, default=250): cv.float_, 64 | cv.Optional(CONF_MAX_VALUE, default=3000): cv.float_, 65 | cv.Optional(CONF_MIN_VALUE, default=10): cv.float_, 66 | cv.Optional(CONF_STEP, default=10): cv.positive_float, 67 | } 68 | ).extend(cv.COMPONENT_SCHEMA), 69 | validate_min_max, 70 | ) 71 | 72 | 73 | CONFIG_SCHEMA = cv.All( 74 | cv.Schema( 75 | { 76 | cv.GenerateID(): cv.declare_id(CEM5855hComponent), 77 | cv.Optional(CONF_MOV): cv.ensure_list( 78 | binary_sensor.binary_sensor_schema( 79 | ThresholdSensor, 80 | device_class=DEVICE_CLASS_MOVING, 81 | ).extend( 82 | cv.Schema({ 83 | cv.Optional(CONF_THRESHOLD, default=250): MOVING_NUMBER_SCHEMA 84 | }) 85 | ) 86 | ), 87 | cv.Optional(CONF_OCC): cv.ensure_list( 88 | binary_sensor.binary_sensor_schema( 89 | ThresholdSensor, 90 | device_class=DEVICE_CLASS_OCCUPANCY, 91 | ).extend( 92 | cv.Schema({ 93 | cv.Optional(CONF_THRESHOLD, default=250): OCCUPANCY_NUMBER_SCHEMA 94 | }) 95 | ) 96 | ), 97 | cv.Optional(CONF_MOTION): cv.ensure_list( 98 | binary_sensor.binary_sensor_schema( 99 | ThresholdSensor, 100 | device_class=DEVICE_CLASS_MOTION, 101 | ).extend( 102 | cv.Schema({ 103 | cv.Optional(CONF_THRESHOLD, default={}): cv.All( 104 | cv.Schema( 105 | { 106 | cv.Optional(CONF_MOV, default=250): MOVING_NUMBER_SCHEMA, 107 | cv.Optional(CONF_OCC, default=250): OCCUPANCY_NUMBER_SCHEMA, 108 | } 109 | ) 110 | ), 111 | }) 112 | ) 113 | ), 114 | } 115 | ) 116 | .extend(cv.COMPONENT_SCHEMA) 117 | .extend(uart.UART_DEVICE_SCHEMA) 118 | ) 119 | 120 | 121 | async def moving_threshold_to_code(sens, config_number): 122 | if isinstance(config_number, int): 123 | cg.add(sens.set_moving_threshold(config_number)) 124 | else: 125 | cg.add(sens.set_moving_threshold(config_number[CONF_VALUE])) 126 | number_moving = cg.new_Pvariable(config_number[CONF_ID]) 127 | cg.add(number_moving.set_parent(sens)) 128 | await number.register_number( 129 | number_moving, 130 | config_number, 131 | min_value=config_number[CONF_MIN_VALUE], 132 | max_value=config_number[CONF_MAX_VALUE], 133 | step=config_number[CONF_STEP], 134 | ) 135 | cg.add(number_moving.publish_state(config_number[CONF_VALUE])) 136 | 137 | 138 | async def occupancy_threshold_to_code(sens, config_number): 139 | if isinstance(config_number, int): 140 | cg.add(sens.set_occupancy_threshold(config_number)) 141 | else: 142 | cg.add(sens.set_occupancy_threshold(config_number[CONF_VALUE])) 143 | number_occupancy = cg.new_Pvariable(config_number[CONF_ID]) 144 | cg.add(number_occupancy.set_parent(sens)) 145 | await number.register_number( 146 | number_occupancy, 147 | config_number, 148 | min_value=config_number[CONF_MIN_VALUE], 149 | max_value=config_number[CONF_MAX_VALUE], 150 | step=config_number[CONF_STEP], 151 | ) 152 | cg.add(number_occupancy.publish_state(config_number[CONF_VALUE])) 153 | 154 | 155 | async def to_code(config): 156 | var = cg.new_Pvariable(config[CONF_ID]) 157 | await cg.register_component(var, config) 158 | await uart.register_uart_device(var, config) 159 | 160 | if CONF_MOV in config: 161 | for sensor_config in config[CONF_MOV]: 162 | sens = await binary_sensor.new_binary_sensor(sensor_config) 163 | cg.add(var.register_moving_sensor(sens)) 164 | 165 | await moving_threshold_to_code(sens, sensor_config[CONF_THRESHOLD]) 166 | 167 | if CONF_OCC in config: 168 | for sensor_config in config[CONF_OCC]: 169 | sens = await binary_sensor.new_binary_sensor(sensor_config) 170 | cg.add(var.register_occupancy_sensor(sens)) 171 | 172 | await occupancy_threshold_to_code(sens, sensor_config[CONF_THRESHOLD]) 173 | 174 | if CONF_MOTION in config: 175 | for sensor_config in config[CONF_MOTION]: 176 | sens = await binary_sensor.new_binary_sensor(sensor_config) 177 | cg.add(var.register_motion_sensor(sens)) 178 | 179 | threshold = sensor_config[CONF_THRESHOLD] 180 | 181 | await moving_threshold_to_code(sens, threshold[CONF_MOV]) 182 | await occupancy_threshold_to_code(sens, threshold[CONF_OCC]) 183 | -------------------------------------------------------------------------------- /components/cem5855h/cem5855h.cpp: -------------------------------------------------------------------------------- 1 | #include "cem5855h.h" 2 | #include "esphome/core/log.h" 3 | #include 4 | 5 | namespace esphome { 6 | namespace cem5855h { 7 | 8 | static const char *const TAG = "cem5855h"; 9 | 10 | void CEM5855hComponent::dump_config() { 11 | ESP_LOGCONFIG(TAG, "CEM5855H:"); 12 | this->check_uart_settings(115200); 13 | if (!this->moving_sensors_.empty()) { 14 | ESP_LOGCONFIG(TAG, " Moving Sensors:"); 15 | for (auto *sensor : this->moving_sensors_) { 16 | LOG_BINARY_SENSOR(" ", "Moving", sensor); 17 | ESP_LOGCONFIG(TAG, " Threshold: %d", sensor->get_moving_threshold()); 18 | } 19 | } 20 | if (!this->occupancy_sensors_.empty()) { 21 | ESP_LOGCONFIG(TAG, " Occupancy Sensors:"); 22 | for (auto *sensor : this->occupancy_sensors_) { 23 | LOG_BINARY_SENSOR(" ", "Occupancy", sensor); 24 | ESP_LOGCONFIG(TAG, " Threshold: %d", sensor->get_occupancy_threshold()); 25 | } 26 | } 27 | if (!this->motion_sensors_.empty()) { 28 | ESP_LOGCONFIG(TAG, " Motion Sensors:"); 29 | for (auto *sensor : this->motion_sensors_) { 30 | LOG_BINARY_SENSOR(" ", "Motion", sensor); 31 | ESP_LOGCONFIG(TAG, " Threshold:"); 32 | ESP_LOGCONFIG(TAG, " Moving: %d", sensor->get_moving_threshold()); 33 | ESP_LOGCONFIG(TAG, " Occupancy: %d", sensor->get_occupancy_threshold()); 34 | } 35 | } 36 | } 37 | 38 | void CEM5855hComponent::loop() { 39 | while (this->available()) { 40 | uint8_t byte; 41 | this->read_byte(&byte); 42 | if (this->parse_(byte)) { 43 | ESP_LOGVV(TAG, "Parsed: 0x%02X", byte); 44 | } else { 45 | this->rx_buffer_.clear(); 46 | } 47 | } 48 | } 49 | 50 | bool CEM5855hComponent::parse_(uint8_t byte) { 51 | size_t at = this->rx_buffer_.size(); 52 | 53 | if (byte == '\r') { 54 | return true; 55 | } 56 | 57 | if (byte != '\n' && at < 30) { 58 | this->rx_buffer_.push_back(byte); 59 | return true; 60 | } 61 | 62 | this->rx_buffer_.push_back('\0'); 63 | 64 | const char *raw = (const char *) &this->rx_buffer_[0]; 65 | 66 | ESP_LOGV(TAG, "recv: %s", raw); 67 | 68 | if (strncmp("mov, ", raw, 5) == 0) { 69 | for (int i = 5; i < at; i++) { 70 | if (raw[i] != ' ') 71 | continue; 72 | i++; 73 | int moving = atoi(&raw[i]); 74 | for (auto *sensor : this->moving_sensors_) { 75 | sensor->update_moving(moving); 76 | } 77 | for (auto *sensor : this->motion_sensors_) { 78 | sensor->update_moving(moving); 79 | } 80 | break; 81 | } 82 | } else if (strncmp("occ, ", raw, 5) == 0) { 83 | for (int i = 5; i < at; i++) { 84 | if (raw[i] != ' ') 85 | continue; 86 | i++; 87 | int occupancy = atoi(&raw[i]); 88 | for (auto *sensor : this->occupancy_sensors_) { 89 | sensor->update_occupancy(occupancy); 90 | } 91 | for (auto *sensor : this->motion_sensors_) { 92 | sensor->update_occupancy(occupancy); 93 | } 94 | break; 95 | } 96 | } 97 | 98 | return false; 99 | } 100 | 101 | } // namespace cem5855h 102 | } // namespace esphome 103 | -------------------------------------------------------------------------------- /components/cem5855h/cem5855h.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "esphome/core/component.h" 6 | #include "esphome/components/number/number.h" 7 | #include "esphome/components/uart/uart.h" 8 | #include "esphome/components/binary_sensor/binary_sensor.h" 9 | 10 | namespace esphome { 11 | namespace cem5855h { 12 | 13 | class ThresholdSensor : public binary_sensor::BinarySensor { 14 | public: 15 | void set_moving_threshold(int moving) { this->moving_threshold_ = moving; } 16 | int get_moving_threshold() { return this->moving_threshold_; } 17 | void set_occupancy_threshold(int occupancy) { this->occupancy_threshold_ = occupancy; } 18 | int get_occupancy_threshold() { return this->occupancy_threshold_; } 19 | void update_moving(int value) { 20 | if (value >= this->moving_threshold_) { 21 | this->publish_state(true); 22 | this->publish_state(false); 23 | } 24 | } 25 | void update_occupancy(int value) { 26 | if (value >= this->occupancy_threshold_) { 27 | this->publish_state(true); 28 | this->publish_state(false); 29 | } 30 | } 31 | 32 | protected: 33 | int moving_threshold_{250}, occupancy_threshold_{250}; 34 | }; 35 | 36 | class CEM5855hComponent : public uart::UARTDevice, public Component { 37 | public: 38 | void dump_config() override; 39 | void loop() override; 40 | float get_setup_priority() const override { return setup_priority::DATA; } 41 | 42 | void register_moving_sensor(ThresholdSensor *moving_sensor) { moving_sensors_.push_back(moving_sensor); } 43 | void register_occupancy_sensor(ThresholdSensor *occupancy_sensor) { occupancy_sensors_.push_back(occupancy_sensor); } 44 | void register_motion_sensor(ThresholdSensor *motion_sensor) { motion_sensors_.push_back(motion_sensor); } 45 | 46 | protected: 47 | bool parse_(uint8_t byte); 48 | 49 | std::vector rx_buffer_; 50 | 51 | std::vector moving_sensors_; 52 | std::vector occupancy_sensors_; 53 | std::vector motion_sensors_; 54 | }; 55 | 56 | class ThresholdMovingNumber : public number::Number { 57 | public: 58 | void set_parent(ThresholdSensor *parent) { this->parent_ = parent; }; 59 | 60 | protected: 61 | void control(float value) override { 62 | if (this->parent_ != nullptr) { 63 | this->parent_->set_moving_threshold(value); 64 | } 65 | this->publish_state(value); 66 | }; 67 | ThresholdSensor *parent_{nullptr}; 68 | }; 69 | 70 | class ThresholdOccupancyNumber : public number::Number { 71 | public: 72 | void set_parent(ThresholdSensor *parent) { this->parent_ = parent; }; 73 | 74 | protected: 75 | void control(float value) override { 76 | if (this->parent_ != nullptr) { 77 | this->parent_->set_occupancy_threshold(value); 78 | } 79 | this->publish_state(value); 80 | }; 81 | ThresholdSensor *parent_{nullptr}; 82 | }; 83 | 84 | } // namespace cem5855h 85 | } // namespace esphome 86 | -------------------------------------------------------------------------------- /components/dlt645/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import ( 4 | remote_transmitter, 5 | remote_receiver, 6 | remote_base, 7 | sensor 8 | ) 9 | from esphome.const import ( 10 | CONF_ID, 11 | CONF_UPDATE_INTERVAL, 12 | CONF_POWER, 13 | CONF_ENERGY, 14 | UNIT_KILOWATT, 15 | UNIT_KILOWATT_HOURS, 16 | DEVICE_CLASS_POWER, 17 | DEVICE_CLASS_ENERGY, 18 | STATE_CLASS_MEASUREMENT, 19 | STATE_CLASS_TOTAL, 20 | ) 21 | from esphome.components.remote_base import CONF_RECEIVER_ID, CONF_TRANSMITTER_ID 22 | 23 | AUTO_LOAD = ["sensor", "remote_base"] 24 | 25 | dlt645_ns = cg.esphome_ns.namespace("dlt645") 26 | DLT645Component = dlt645_ns.class_( 27 | "DLT645Component", cg.Component, remote_base.RemoteReceiverListener 28 | ) 29 | DLT645Sensor = dlt645_ns.class_( 30 | "DLT645Sensor", sensor.Sensor 31 | ) 32 | 33 | CONF_ADDRESS = "address" 34 | CONF_POWER_A = "power_a" 35 | CONF_POWER_B = "power_b" 36 | CONF_POWER_C = "power_c" 37 | CONF_ENERGY_A = "energy_a" 38 | CONF_ENERGY_B = "energy_b" 39 | CONF_ENERGY_C = "energy_c" 40 | 41 | 42 | class SensorSetting: 43 | def __init__(self, name, id, format) -> None: 44 | self.name = name, 45 | self.id = id, 46 | self.format = format 47 | 48 | 49 | Sensors = { 50 | CONF_POWER: SensorSetting("Power", 0x02030000, 1.2), 51 | CONF_POWER_A: SensorSetting("Power A", 0x02030100, 1.2), 52 | CONF_POWER_B: SensorSetting("Power B", 0x02030200, 1.2), 53 | CONF_POWER_C: SensorSetting("Power C", 0x02030300, 1.2), 54 | CONF_ENERGY: SensorSetting("Energy", 0x00000000, 3.1), 55 | CONF_ENERGY_A: SensorSetting("Energy A", 0x00150000, 3.1), 56 | CONF_ENERGY_B: SensorSetting("Energy B", 0x00290000, 3.1), 57 | CONF_ENERGY_C: SensorSetting("Energy C", 0x003D0000, 3.1), 58 | } 59 | 60 | CONFIG_POWER_SCHEMA = sensor.sensor_schema( 61 | DLT645Sensor, 62 | unit_of_measurement=UNIT_KILOWATT, 63 | accuracy_decimals=4, 64 | device_class=DEVICE_CLASS_POWER, 65 | state_class=STATE_CLASS_MEASUREMENT, 66 | ).extend( 67 | { 68 | cv.Optional(CONF_UPDATE_INTERVAL, default="10s"): cv.positive_time_period_microseconds, 69 | } 70 | ) 71 | 72 | CONFIG_ENERGY_SCHEMA = sensor.sensor_schema( 73 | DLT645Sensor, 74 | unit_of_measurement=UNIT_KILOWATT_HOURS, 75 | accuracy_decimals=2, 76 | device_class=DEVICE_CLASS_ENERGY, 77 | state_class=STATE_CLASS_TOTAL, 78 | ).extend( 79 | { 80 | cv.Optional(CONF_UPDATE_INTERVAL, default="30s"): cv.positive_time_period_microseconds, 81 | } 82 | ) 83 | 84 | CONFIG_SCHEMA = cv.All( 85 | cv.Schema( 86 | { 87 | cv.GenerateID(): cv.declare_id(DLT645Component), 88 | cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id( 89 | remote_transmitter.RemoteTransmitterComponent 90 | ), 91 | cv.GenerateID(CONF_RECEIVER_ID): cv.use_id( 92 | remote_receiver.RemoteReceiverComponent 93 | ), 94 | cv.Optional(CONF_ADDRESS): cv.string, 95 | cv.Optional(CONF_POWER): CONFIG_POWER_SCHEMA, 96 | cv.Optional(CONF_POWER_A): CONFIG_POWER_SCHEMA, 97 | cv.Optional(CONF_POWER_B): CONFIG_POWER_SCHEMA, 98 | cv.Optional(CONF_POWER_C): CONFIG_POWER_SCHEMA, 99 | cv.Optional(CONF_ENERGY): CONFIG_ENERGY_SCHEMA, 100 | cv.Optional(CONF_ENERGY_A): CONFIG_ENERGY_SCHEMA, 101 | cv.Optional(CONF_ENERGY_B): CONFIG_ENERGY_SCHEMA, 102 | cv.Optional(CONF_ENERGY_C): CONFIG_ENERGY_SCHEMA, 103 | } 104 | ) 105 | .extend(cv.COMPONENT_SCHEMA) 106 | ) 107 | 108 | 109 | async def to_code(config): 110 | var = cg.new_Pvariable(config[CONF_ID]) 111 | await cg.register_component(var, config) 112 | 113 | receiver = await cg.get_variable(config[CONF_RECEIVER_ID]) 114 | cg.add(receiver.register_listener(var)) 115 | 116 | transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID]) 117 | cg.add(var.set_transmitter(transmitter)) 118 | 119 | if CONF_ADDRESS in config: 120 | cg.add(var.set_address(config[CONF_ADDRESS])) 121 | 122 | for config_type, setting in Sensors.items(): 123 | if config_type in config: 124 | sensor_config = config[config_type] 125 | var_sensor = cg.new_Pvariable( 126 | sensor_config[CONF_ID], setting.name, setting.id, setting.format) 127 | cg.add(var_sensor.set_interval(sensor_config[CONF_UPDATE_INTERVAL])) 128 | await sensor.register_sensor(var_sensor, sensor_config) 129 | cg.add(var.register_sensor(var_sensor)) 130 | -------------------------------------------------------------------------------- /components/dlt645/dlt645.cpp: -------------------------------------------------------------------------------- 1 | #include "dlt645.h" 2 | #include "esphome/core/log.h" 3 | 4 | namespace esphome { 5 | namespace dlt645 { 6 | 7 | static const char *const TAG = "DL/T 645"; 8 | 9 | #define BIT_TIME 833 10 | #define SEND_INTERVAL 500000 11 | 12 | void DLT645Component::dump_config() { 13 | ESP_LOGCONFIG(TAG, "DL/T 645:"); 14 | if (!this->address_.empty()) { 15 | ESP_LOGCONFIG(TAG, " Address: %02X%02X%02X%02X%02X%02X", this->address_[5], this->address_[4], this->address_[3], 16 | this->address_[2], this->address_[1], this->address_[0]); 17 | } else { 18 | ESP_LOGCONFIG(TAG, " Address: Unkown"); 19 | } 20 | for (auto sensor : this->sensors_) { 21 | ESP_LOGCONFIG(TAG, " %s", sensor->get_type().c_str()); 22 | LOG_SENSOR(" ", " Name:", sensor); 23 | } 24 | } 25 | 26 | void DLT645Component::setup() {} 27 | 28 | void DLT645Component::loop() { 29 | if (micros() < this->next_time_ && this->next_time_ - micros() < SEND_INTERVAL) { 30 | return; 31 | } 32 | 33 | if (this->address_.empty()) { 34 | std::vector address = {0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}; 35 | this->send(address, 0x13); 36 | return; 37 | } 38 | 39 | static int next = -1; 40 | size_t size = this->sensors_.size(); 41 | if (size == 0) { 42 | return; 43 | } 44 | for (int i = 0; i < size; i++) { 45 | next = (next + 1) % size; 46 | if (this->sensors_[next]->is_timeout()) { 47 | this->send(this->address_, 0x11, this->sensors_[next]->get_id()); 48 | break; 49 | } 50 | } 51 | } 52 | 53 | static uint8_t decode_byte(uint8_t byte) { return (byte >> 4) * 10 + (byte & 0x0F); } 54 | 55 | void DLT645Component::handle_response(std::vector &data, int index) { 56 | uint8_t opcode = data[index]; 57 | if (opcode == 0x93) { 58 | this->address_.clear(); 59 | for (int i = index + 2; i < index + 8; i++) { 60 | this->address_.push_back(data[i]); 61 | } 62 | return; 63 | } 64 | if (opcode == 0x91) { 65 | uint32_t data_type = encode_uint32(data[index + 5], data[index + 4], data[index + 3], data[index + 2]); 66 | for (auto sensor : this->sensors_) { 67 | if (data_type == sensor->get_id()) { 68 | uint32_t value = 0; 69 | uint8_t fraction = ((uint8_t) (sensor->get_format() * 10.0f)) % 10; 70 | uint8_t integer = ((uint8_t) sensor->get_format()) % 10; 71 | data[index + 5 + fraction + integer] &= 0x7F; 72 | for (uint8_t i = fraction + integer; i > 0; i--) { 73 | value = value * 100 + decode_byte(data[index + 5 + i]); 74 | } 75 | sensor->update(value * pow(0.01, fraction)); 76 | break; 77 | } 78 | } 79 | } 80 | } 81 | 82 | void DLT645Component::set_address(const std::string &address) { 83 | this->address_.clear(); 84 | if (address.size() != 12) { 85 | return; 86 | } 87 | char temp[3] = {0}; 88 | for (int i = 5; i >= 0; i--) { 89 | strncpy(temp, &(address.c_str()[i * 2]), 2); 90 | this->address_.push_back(std::strtoul(temp, nullptr, 16)); 91 | } 92 | } 93 | 94 | bool DLT645Component::on_receive(remote_base::RemoteReceiveData data) { 95 | auto &raw = data.get_raw_data(); 96 | uint8_t bits = 0; 97 | int bits_at = 0; 98 | bool parity = false; 99 | for (auto time : raw) { 100 | uint8_t bit = time > 0 ? 0 : 1; 101 | time = (time > 0 ? time : -time) + 400; 102 | for (time -= BIT_TIME; time > 0; time -= BIT_TIME, bits_at++) { 103 | if (bits_at == 0) { 104 | if (bit) { 105 | break; 106 | } 107 | } else if (bits_at < 9) { 108 | bits |= bit << (bits_at - 1); 109 | parity ^= bit; 110 | } else if (bits_at == 9) { 111 | if (bit == parity) { 112 | this->rx_buffer_.push_back(bits); 113 | } 114 | } else { 115 | bits_at = 0; 116 | bits = 0; 117 | parity = false; 118 | break; 119 | } 120 | } 121 | } 122 | 123 | if (!rx_buffer_.empty()) { 124 | for (int i = 0; i < this->rx_buffer_.size(); i++) { 125 | if (this->rx_buffer_[i] == 0x68) { 126 | if (i + 11 >= this->rx_buffer_.size()) { 127 | continue; 128 | } 129 | if (this->rx_buffer_[i + 7] != 0x68) { 130 | continue; 131 | } 132 | int end_at = i + 11 + this->rx_buffer_[i + 9]; 133 | if (end_at >= this->rx_buffer_.size() || this->rx_buffer_[end_at] != 0x16) { 134 | continue; 135 | } 136 | if (this->rx_buffer_[end_at - 1] != this->checksum_(this->rx_buffer_, i, end_at - i - 1)) { 137 | continue; 138 | } 139 | for (int j = i + 10; j < end_at - 1; j++) { 140 | this->rx_buffer_[j] -= 0x33; 141 | } 142 | this->handle_response(this->rx_buffer_, i + 8); 143 | i = end_at; 144 | this->next_time_ = micros(); 145 | } 146 | } 147 | this->rx_buffer_.clear(); 148 | } 149 | return false; 150 | } 151 | 152 | void DLT645Component::send(std::vector &address, uint8_t opcode, uint32_t data_id) { 153 | std::vector request = {0xFE, 0xFE, 0xFE, 0xFE, 0x68}; 154 | request.insert(request.end(), address.begin(), address.end()); 155 | request.push_back(0x68); 156 | request.push_back(opcode); 157 | if (data_id != 0xFFFFFFFF) { 158 | request.push_back(4); 159 | for (int i = 0; i < 4; i++) { 160 | request.push_back(((data_id >> (i * 8)) & 0xFF) + 0x33); 161 | } 162 | } else { 163 | request.push_back(0); 164 | } 165 | request.push_back(this->checksum_(request, 4, request.size() - 4)); 166 | request.push_back(0x16); 167 | 168 | auto transmit = this->transmitter_->transmit(); 169 | auto *transmit_data = transmit.get_data(); 170 | 171 | transmit_data->set_carrier_frequency(38000); 172 | bool last_bit = false; 173 | int32_t time = 0; 174 | for (auto byte : request) { 175 | bool parity_bit = true; 176 | for (int i = 0; i < 11; i++) { 177 | bool bit = false; 178 | switch (i) { 179 | case 0: 180 | bit = false; 181 | break; 182 | case 9: 183 | bit = parity_bit; 184 | break; 185 | case 10: 186 | bit = true; 187 | break; 188 | default: 189 | bit = (byte & (1 << (i - 1))); 190 | break; 191 | } 192 | if (bit == last_bit) { 193 | time += BIT_TIME; 194 | } else { 195 | if (last_bit) { 196 | transmit_data->space(time); 197 | } else { 198 | transmit_data->mark(time); 199 | } 200 | time = BIT_TIME; 201 | last_bit = bit; 202 | } 203 | } 204 | } 205 | transmit_data->space(time); 206 | 207 | transmit.perform(); 208 | this->next_time_ = micros() + SEND_INTERVAL; 209 | } 210 | 211 | uint8_t DLT645Component::checksum_(std::vector &data, int start, int length) { 212 | uint8_t sum = 0; 213 | for (int i = start; i < start + length && i < data.size(); i++) { 214 | sum += data[i]; 215 | } 216 | return sum; 217 | } 218 | 219 | bool DLT645Sensor::is_timeout() { return (micros() < last_update) || ((micros() - last_update) > update_interval); } 220 | 221 | void DLT645Sensor::update(float state) { 222 | this->publish_state(state); 223 | this->last_update = micros(); 224 | } 225 | 226 | } // namespace dlt645 227 | } // namespace esphome 228 | -------------------------------------------------------------------------------- /components/dlt645/dlt645.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "esphome/components/remote_base/remote_base.h" 5 | #include "esphome/components/sensor/sensor.h" 6 | #include "esphome/components/remote_transmitter/remote_transmitter.h" 7 | 8 | namespace esphome { 9 | namespace dlt645 { 10 | 11 | class DLT645Sensor : public sensor::Sensor { 12 | public: 13 | explicit DLT645Sensor(const std::string &type, uint32_t id, float format) 14 | : sensor::Sensor(), type_(type), data_id_(id), format_(format) {} 15 | void set_interval(uint32_t interval) { this->update_interval = interval; } 16 | float get_format() { return format_; } 17 | uint32_t get_id() { return data_id_; } 18 | const std::string &get_type() const { return this->type_; } 19 | 20 | bool is_timeout(); 21 | void update(float state); 22 | 23 | protected: 24 | std::string type_; 25 | uint32_t data_id_; 26 | float format_; 27 | uint32_t update_interval{10000000}; 28 | uint32_t last_update{0xFFFFFFFF}; 29 | }; 30 | 31 | class DLT645Component : public Component, public remote_base::RemoteReceiverListener { 32 | public: 33 | void dump_config() override; 34 | void setup() override; 35 | void loop() override; 36 | float get_setup_priority() const override { return setup_priority::DATA; } 37 | void set_address(const std::string &address); 38 | void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { 39 | this->transmitter_ = transmitter; 40 | } 41 | void register_sensor(DLT645Sensor *sensor) { this->sensors_.push_back(sensor); } 42 | 43 | protected: 44 | bool on_receive(remote_base::RemoteReceiveData data) override; 45 | void send(std::vector &address, uint8_t opcode, uint32_t data = 0xFFFFFFFF); 46 | uint8_t checksum_(std::vector &data, int start, int length); 47 | void handle_response(std::vector &data, int index); 48 | 49 | std::vector address_; 50 | std::vector rx_buffer_; 51 | uint32_t next_time_{0}; 52 | remote_transmitter::RemoteTransmitterComponent *transmitter_; 53 | 54 | std::vector sensors_; 55 | }; 56 | 57 | } // namespace dlt645 58 | } // namespace esphome 59 | -------------------------------------------------------------------------------- /components/esp32_ble_tracker/automation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/automation.h" 4 | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" 5 | 6 | #ifdef ARDUINO_ARCH_ESP32 7 | 8 | namespace esphome { 9 | namespace esp32_ble_tracker { 10 | class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { 11 | public: 12 | explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } 13 | void set_address(uint64_t address) { this->address_ = address; } 14 | 15 | bool parse_device(const ESPBTDevice &device) override { 16 | if (this->address_ && device.address_uint64() != this->address_) { 17 | return false; 18 | } 19 | this->trigger(device); 20 | return true; 21 | } 22 | 23 | protected: 24 | uint64_t address_ = 0; 25 | }; 26 | 27 | class BLEServiceDataAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { 28 | public: 29 | explicit BLEServiceDataAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } 30 | void set_address(uint64_t address) { this->address_ = address; } 31 | void set_service_uuid16(uint16_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(uuid); } 32 | void set_service_uuid32(uint32_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(uuid); } 33 | void set_service_uuid128(uint8_t *uuid) { this->uuid_ = ESPBTUUID::from_raw(uuid); } 34 | 35 | bool parse_device(const ESPBTDevice &device) override { 36 | if (this->address_ && device.address_uint64() != this->address_) { 37 | return false; 38 | } 39 | for (auto &service_data : device.get_service_datas()) { 40 | if (service_data.uuid == this->uuid_) { 41 | this->trigger(service_data.data); 42 | return true; 43 | } 44 | } 45 | return false; 46 | } 47 | 48 | protected: 49 | uint64_t address_ = 0; 50 | ESPBTUUID uuid_; 51 | }; 52 | 53 | class BLEManufacturerDataAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { 54 | public: 55 | explicit BLEManufacturerDataAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } 56 | void set_address(uint64_t address) { this->address_ = address; } 57 | void set_manufacturer_uuid16(uint16_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(uuid); } 58 | void set_manufacturer_uuid32(uint32_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(uuid); } 59 | void set_manufacturer_uuid128(uint8_t *uuid) { this->uuid_ = ESPBTUUID::from_raw(uuid); } 60 | 61 | bool parse_device(const ESPBTDevice &device) override { 62 | if (this->address_ && device.address_uint64() != this->address_) { 63 | return false; 64 | } 65 | for (auto &manufacturer_data : device.get_manufacturer_datas()) { 66 | if (manufacturer_data.uuid == this->uuid_) { 67 | this->trigger(manufacturer_data.data); 68 | return true; 69 | } 70 | } 71 | return false; 72 | } 73 | 74 | protected: 75 | uint64_t address_ = 0; 76 | ESPBTUUID uuid_; 77 | }; 78 | 79 | } // namespace esp32_ble_tracker 80 | } // namespace esphome 81 | 82 | #endif 83 | -------------------------------------------------------------------------------- /components/esp32_ble_tracker/esp32_ble_tracker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/core/helpers.h" 5 | #include "queue.h" 6 | 7 | #ifdef ARDUINO_ARCH_ESP32 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace esphome { 16 | namespace esp32_ble_tracker { 17 | 18 | class ESPBTUUID { 19 | public: 20 | ESPBTUUID(); 21 | 22 | static ESPBTUUID from_uint16(uint16_t uuid); 23 | 24 | static ESPBTUUID from_uint32(uint32_t uuid); 25 | 26 | static ESPBTUUID from_raw(const uint8_t *data); 27 | 28 | static ESPBTUUID from_uuid(esp_bt_uuid_t uuid); 29 | 30 | ESPBTUUID as_128bit() const; 31 | 32 | bool contains(uint8_t data1, uint8_t data2) const; 33 | 34 | bool operator==(const ESPBTUUID &uuid) const; 35 | bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } 36 | 37 | esp_bt_uuid_t get_uuid(); 38 | 39 | std::string to_string(); 40 | 41 | protected: 42 | esp_bt_uuid_t uuid_; 43 | }; 44 | 45 | using adv_data_t = std::vector; 46 | 47 | struct ServiceData { 48 | ESPBTUUID uuid; 49 | adv_data_t data; 50 | }; 51 | 52 | class ESPBLEiBeacon { 53 | public: 54 | ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); } 55 | ESPBLEiBeacon(const uint8_t *data); 56 | static optional from_manufacturer_data(const ServiceData &data); 57 | 58 | uint16_t get_major() { return ((this->beacon_data_.major & 0xFF) << 8) | (this->beacon_data_.major >> 8); } 59 | uint16_t get_minor() { return ((this->beacon_data_.minor & 0xFF) << 8) | (this->beacon_data_.minor >> 8); } 60 | int8_t get_signal_power() { return this->beacon_data_.signal_power; } 61 | ESPBTUUID get_uuid() { return ESPBTUUID::from_raw(this->beacon_data_.proximity_uuid); } 62 | 63 | protected: 64 | struct { 65 | uint8_t sub_type; 66 | uint8_t length; 67 | uint8_t proximity_uuid[16]; 68 | uint16_t major; 69 | uint16_t minor; 70 | int8_t signal_power; 71 | } PACKED beacon_data_; 72 | }; 73 | 74 | class ESPBTDevice { 75 | public: 76 | void parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); 77 | 78 | std::string address_str() const; 79 | 80 | uint64_t address_uint64() const; 81 | 82 | const uint8_t *address() const { return address_; } 83 | 84 | esp_ble_addr_type_t get_address_type() const { return this->address_type_; } 85 | int get_rssi() const { return rssi_; } 86 | const std::string &get_name() const { return this->name_; } 87 | 88 | const std::vector &get_tx_powers() const { return tx_powers_; } 89 | 90 | const optional &get_appearance() const { return appearance_; } 91 | const optional &get_ad_flag() const { return ad_flag_; } 92 | const std::vector &get_service_uuids() const { return service_uuids_; } 93 | 94 | const std::vector &get_manufacturer_datas() const { return manufacturer_datas_; } 95 | 96 | const std::vector &get_service_datas() const { return service_datas_; } 97 | 98 | optional get_ibeacon() const { 99 | for (auto &it : this->manufacturer_datas_) { 100 | auto res = ESPBLEiBeacon::from_manufacturer_data(it); 101 | if (res.has_value()) 102 | return *res; 103 | } 104 | return {}; 105 | } 106 | 107 | protected: 108 | void parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); 109 | 110 | esp_bd_addr_t address_{ 111 | 0, 112 | }; 113 | esp_ble_addr_type_t address_type_{BLE_ADDR_TYPE_PUBLIC}; 114 | int rssi_{0}; 115 | std::string name_{}; 116 | std::vector tx_powers_{}; 117 | optional appearance_{}; 118 | optional ad_flag_{}; 119 | std::vector service_uuids_; 120 | std::vector manufacturer_datas_{}; 121 | std::vector service_datas_{}; 122 | }; 123 | 124 | class ESP32BLETracker; 125 | 126 | class ESPBTDeviceListener { 127 | public: 128 | virtual void on_scan_end() {} 129 | virtual bool parse_device(const ESPBTDevice &device) = 0; 130 | void set_parent(ESP32BLETracker *parent) { parent_ = parent; } 131 | 132 | protected: 133 | ESP32BLETracker *parent_{nullptr}; 134 | }; 135 | 136 | enum class ClientState { 137 | // Connection is idle, no device detected. 138 | Idle, 139 | // Device advertisement found. 140 | Discovered, 141 | // Connection in progress. 142 | Connecting, 143 | // Initial connection established. 144 | Connected, 145 | // The client and sub-clients have completed setup. 146 | Established, 147 | }; 148 | 149 | class ESPBTClient : public ESPBTDeviceListener { 150 | public: 151 | virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, 152 | esp_ble_gattc_cb_param_t *param) = 0; 153 | virtual void connect() = 0; 154 | void set_state(ClientState st) { this->state_ = st; } 155 | ClientState state() const { return state_; } 156 | int app_id; 157 | 158 | protected: 159 | ClientState state_; 160 | }; 161 | 162 | class ESP32BLETracker : public Component { 163 | public: 164 | void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } 165 | void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; } 166 | void set_scan_window(uint32_t scan_window) { scan_window_ = scan_window; } 167 | void set_scan_active(bool scan_active) { scan_active_ = scan_active; } 168 | 169 | /// Setup the FreeRTOS task and the Bluetooth stack. 170 | void setup() override; 171 | void dump_config() override; 172 | 173 | void loop() override; 174 | 175 | void register_listener(ESPBTDeviceListener *listener) { 176 | listener->set_parent(this); 177 | this->listeners_.push_back(listener); 178 | } 179 | 180 | void register_client(ESPBTClient *client); 181 | 182 | void print_bt_device_info(const ESPBTDevice &device); 183 | 184 | protected: 185 | /// The FreeRTOS task managing the bluetooth interface. 186 | static bool ble_setup(); 187 | /// Start a single scan by setting up the parameters and doing some esp-idf calls. 188 | void start_scan(bool first); 189 | /// Callback that will handle all GAP events and redistribute them to other callbacks. 190 | static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); 191 | void real_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); 192 | /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received. 193 | void gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); 194 | /// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received. 195 | void gap_scan_set_param_complete(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m); 196 | /// Called when a `ESP_GAP_BLE_SCAN_START_COMPLETE_EVT` event is received. 197 | void gap_scan_start_complete(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m); 198 | /// Called when a `ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT` event is received. 199 | void gap_scan_stop_complete(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m); 200 | 201 | int app_id_; 202 | /// Callback that will handle all GATTC events and redistribute them to other callbacks. 203 | static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); 204 | void real_gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); 205 | 206 | /// Vector of addresses that have already been printed in print_bt_device_info 207 | std::vector already_discovered_; 208 | std::vector listeners_; 209 | /// Client parameters. 210 | std::vector clients_; 211 | /// A structure holding the ESP BLE scan parameters. 212 | esp_ble_scan_params_t scan_params_; 213 | /// The interval in seconds to perform scans. 214 | uint32_t scan_duration_; 215 | uint32_t scan_interval_; 216 | uint32_t scan_window_; 217 | bool scan_active_; 218 | SemaphoreHandle_t scan_result_lock_; 219 | SemaphoreHandle_t scan_end_lock_; 220 | size_t scan_result_index_{0}; 221 | esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_buffer_[16]; 222 | esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; 223 | esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; 224 | 225 | Queue ble_events_; 226 | }; 227 | 228 | extern ESP32BLETracker *global_esp32_ble_tracker; 229 | 230 | } // namespace esp32_ble_tracker 231 | } // namespace esphome 232 | 233 | #endif 234 | -------------------------------------------------------------------------------- /components/esp32_ble_tracker/queue.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "esphome/core/component.h" 3 | #include "esphome/core/helpers.h" 4 | 5 | #include 6 | #include 7 | 8 | #ifdef ARDUINO_ARCH_ESP32 9 | 10 | #include 11 | #include 12 | 13 | /* 14 | * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather 15 | * than trying to deal with various locking strategies, all incoming GAP and GATT 16 | * events will simply be placed on a semaphore guarded queue. The next time the 17 | * component runs loop(), these events are popped off the queue and handed at 18 | * this safer time. 19 | */ 20 | 21 | namespace esphome { 22 | namespace esp32_ble_tracker { 23 | 24 | template class Queue { 25 | public: 26 | Queue() { m = xSemaphoreCreateMutex(); } 27 | 28 | void push(T *element) { 29 | if (element == nullptr) 30 | return; 31 | if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) { 32 | q.push(element); 33 | xSemaphoreGive(m); 34 | } 35 | } 36 | 37 | T *pop() { 38 | T *element = nullptr; 39 | 40 | if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) { 41 | if (!q.empty()) { 42 | element = q.front(); 43 | q.pop(); 44 | } 45 | xSemaphoreGive(m); 46 | } 47 | return element; 48 | } 49 | 50 | protected: 51 | std::queue q; 52 | SemaphoreHandle_t m; 53 | }; 54 | 55 | // Received GAP and GATTC events are only queued, and get processed in the main loop(). 56 | // This class stores each event in a single type. 57 | class BLEEvent { 58 | public: 59 | BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { 60 | this->event_.gap.gap_event = e; 61 | memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); 62 | this->type_ = 0; 63 | }; 64 | 65 | BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { 66 | this->event_.gattc.gattc_event = e; 67 | this->event_.gattc.gattc_if = i; 68 | memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); 69 | // Need to also make a copy of relevant event data. 70 | switch (e) { 71 | case ESP_GATTC_NOTIFY_EVT: 72 | memcpy(this->event_.gattc.data, p->notify.value, p->notify.value_len); 73 | this->event_.gattc.gattc_param.notify.value = this->event_.gattc.data; 74 | break; 75 | case ESP_GATTC_READ_CHAR_EVT: 76 | case ESP_GATTC_READ_DESCR_EVT: 77 | memcpy(this->event_.gattc.data, p->read.value, p->read.value_len); 78 | this->event_.gattc.gattc_param.read.value = this->event_.gattc.data; 79 | break; 80 | default: 81 | break; 82 | } 83 | this->type_ = 1; 84 | }; 85 | 86 | union { 87 | struct gap_event { 88 | esp_gap_ble_cb_event_t gap_event; 89 | esp_ble_gap_cb_param_t gap_param; 90 | } gap; 91 | 92 | struct gattc_event { 93 | esp_gattc_cb_event_t gattc_event; 94 | esp_gatt_if_t gattc_if; 95 | esp_ble_gattc_cb_param_t gattc_param; 96 | uint8_t data[64]; 97 | } gattc; 98 | } event_; 99 | uint8_t type_; // 0=gap 1=gattc 100 | }; 101 | 102 | } // namespace esp32_ble_tracker 103 | } // namespace esphome 104 | 105 | #endif 106 | -------------------------------------------------------------------------------- /components/esp32_bt_tracker/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import esphome.codegen as cg 3 | import esphome.config_validation as cv 4 | from esphome import automation 5 | from esphome.const import ( 6 | CONF_ID, 7 | ESP_PLATFORM_ESP32, 8 | CONF_TRIGGER_ID, 9 | CONF_MAC_ADDRESS, 10 | CONF_NAME, 11 | CONF_DURATION 12 | ) 13 | 14 | ESP_PLATFORMS = [ESP_PLATFORM_ESP32] 15 | 16 | CONF_ESP32_BT_ID = "esp32_bt_id" 17 | CONF_SCAN_PARAMETERS = "scan_parameters" 18 | CONF_ON_BT_DISCOVERY = "on_bt_discovery" 19 | esp32_bt_tracker_ns = cg.esphome_ns.namespace("esp32_bt_tracker") 20 | ESP32BTTracker = esp32_bt_tracker_ns.class_("ESP32BTTracker", cg.Component, cg.Nameable) 21 | ESPBTDeviceListener = esp32_bt_tracker_ns.class_("ESPBTDeviceListener") 22 | ESPBTDevice = esp32_bt_tracker_ns.class_("ESPBTDevice") 23 | ESPBTDeviceConstRef = ESPBTDevice.operator("ref").operator("const") 24 | # Triggers 25 | ESPBTAdvertiseTrigger = esp32_bt_tracker_ns.class_( 26 | "ESPBTAdvertiseTrigger", automation.Trigger.template(ESPBTDeviceConstRef) 27 | ) 28 | 29 | def as_hex(value): 30 | return cg.RawExpression(f"0x{value}ULL") 31 | 32 | 33 | CONFIG_SCHEMA = cv.Schema( 34 | { 35 | cv.GenerateID(): cv.declare_id(ESP32BTTracker), 36 | cv.Optional(CONF_NAME): cv.string, 37 | cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( 38 | cv.Schema( 39 | { 40 | cv.Optional( 41 | CONF_DURATION, default="5s" 42 | ): cv.All( 43 | cv.positive_time_period_seconds, 44 | cv.Range(min=cv.TimePeriod(seconds=1), max=cv.TimePeriod(seconds=60)) 45 | ), 46 | } 47 | ), 48 | ), 49 | cv.Optional(CONF_ON_BT_DISCOVERY): automation.validate_automation( 50 | { 51 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), 52 | cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, 53 | } 54 | ) 55 | } 56 | ).extend(cv.COMPONENT_SCHEMA) 57 | 58 | 59 | async def to_code(config): 60 | var = cg.new_Pvariable(config[CONF_ID]) 61 | await cg.register_component(var, config) 62 | params = config[CONF_SCAN_PARAMETERS] 63 | cg.add(var.set_scan_duration(params[CONF_DURATION])) 64 | if CONF_NAME in config: 65 | cg.add(var.set_name(config[CONF_NAME])) 66 | for conf in config.get(CONF_ON_BT_DISCOVERY, []): 67 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) 68 | if CONF_MAC_ADDRESS in conf: 69 | cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) 70 | await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) 71 | 72 | 73 | ESP_BT_DEVICE_SCHEMA = cv.Schema( 74 | { 75 | cv.GenerateID(CONF_ESP32_BT_ID): cv.use_id(ESP32BTTracker), 76 | } 77 | ) 78 | 79 | async def register_bt_device(var, config): 80 | paren = await cg.get_variable(config[CONF_ESP32_BT_ID]) 81 | cg.add(paren.register_listener(var)) 82 | return var 83 | -------------------------------------------------------------------------------- /components/esp32_bt_tracker/automation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/automation.h" 4 | #include "esp32_bt_tracker.h" 5 | 6 | #ifdef USE_ESP32 7 | 8 | namespace esphome { 9 | namespace esp32_bt_tracker { 10 | class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { 11 | public: 12 | explicit ESPBTAdvertiseTrigger(ESP32BTTracker *parent) { parent->register_listener(this); } 13 | void set_address(uint64_t address) { this->address_ = address; } 14 | 15 | bool parse_device(const ESPBTDevice &device) override { 16 | if (this->address_ && device.address_uint64() != this->address_) { 17 | return false; 18 | } 19 | this->trigger(device); 20 | return true; 21 | } 22 | 23 | protected: 24 | uint64_t address_ = 0; 25 | }; 26 | 27 | 28 | 29 | } // namespace esp32_bt_tracker 30 | } // namespace esphome 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /components/esp32_bt_tracker/esp32_bt_tracker.cpp: -------------------------------------------------------------------------------- 1 | #include "esp32_bt_tracker.h" 2 | #include "esphome/core/log.h" 3 | #include "esphome/core/application.h" 4 | #include "esphome/core/helpers.h" 5 | 6 | #ifdef USE_ESP32 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #undef TAG 18 | 19 | namespace esphome { 20 | namespace esp32_bt_tracker { 21 | 22 | static const char *const TAG = "esp32_bt_tracker"; 23 | 24 | ESP32BTTracker *global_esp32_bt_tracker = nullptr; 25 | 26 | uint64_t bt_addr_to_uint64(const esp_bd_addr_t address) { 27 | uint64_t u = 0; 28 | u |= uint64_t(address[0] & 0xFF) << 40; 29 | u |= uint64_t(address[1] & 0xFF) << 32; 30 | u |= uint64_t(address[2] & 0xFF) << 24; 31 | u |= uint64_t(address[3] & 0xFF) << 16; 32 | u |= uint64_t(address[4] & 0xFF) << 8; 33 | u |= uint64_t(address[5] & 0xFF) << 0; 34 | return u; 35 | } 36 | 37 | void ESP32BTTracker::setup() { 38 | global_esp32_bt_tracker = this; 39 | this->scan_end_lock_ = xSemaphoreCreateMutex(); 40 | 41 | if (!bt_setup()) { 42 | this->mark_failed(); 43 | return; 44 | } 45 | 46 | global_esp32_bt_tracker->start_scan(true); 47 | } 48 | 49 | void ESP32BTTracker::set_scan_duration(uint8_t scan_duration) { 50 | uint8_t duration = scan_duration / 1.28; 51 | if (duration < 0x01) { 52 | scan_duration_ = 0x01; 53 | } else if (duration > 0x30) { 54 | scan_duration_ = 0x30; 55 | } else { 56 | scan_duration_ = duration; 57 | } 58 | } 59 | 60 | uint32_t ESP32BTTracker::hash_base() { return 2158133466UL; } 61 | 62 | bool ESP32BTTracker::bt_setup() { 63 | esp_err_t err; 64 | 65 | if (!btStarted()) { 66 | err = nvs_flash_init(); 67 | if (err != ESP_OK) { 68 | ESP_LOGE(TAG, "nvs_flash_init failed: %d", err); 69 | return false; 70 | } 71 | 72 | if (!btStart()) { 73 | ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); 74 | return false; 75 | } 76 | 77 | err = esp_bluedroid_init(); 78 | if (err != ESP_OK) { 79 | ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err); 80 | return false; 81 | } 82 | err = esp_bluedroid_enable(); 83 | if (err != ESP_OK) { 84 | ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); 85 | return false; 86 | } 87 | } 88 | 89 | /* register GAP callback function */ 90 | esp_bt_gap_register_callback(ESP32BTTracker::gap_event_handler); 91 | 92 | esp_bt_dev_set_device_name(this->get_name().c_str()); 93 | esp_bt_gap_set_scan_mode(ESP_BT_SCAN_MODE_NONE); 94 | 95 | esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_IO; 96 | err = esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &iocap, sizeof(uint8_t)); 97 | if (err != ESP_OK) { 98 | ESP_LOGE(TAG, "esp_bt_gap_set_security_param failed: %d", err); 99 | return false; 100 | } 101 | 102 | // BLE takes some time to be fully set up, 200ms should be more than enough 103 | delay(200); // NOLINT 104 | 105 | return true; 106 | } 107 | 108 | void ESP32BTTracker::loop() { 109 | // 处理gap信息 110 | ESPBTDevice *device = this->bt_devices_.pop(); 111 | while (device != nullptr) { 112 | bool found = false; 113 | for (auto *listener : this->listeners_) 114 | if (listener->parse_device(*device)) 115 | found = true; 116 | // 没有监听器处理消息,打印蓝牙设备信息 117 | if (!found) { 118 | this->print_bt_device_info(*device); 119 | } 120 | delete device; 121 | device = this->bt_devices_.pop(); 122 | } 123 | 124 | if (xSemaphoreTake(this->scan_end_lock_, 0L)) { 125 | xSemaphoreGive(this->scan_end_lock_); 126 | global_esp32_bt_tracker->start_scan(false); 127 | } 128 | } 129 | 130 | void ESP32BTTracker::start_scan(bool first) { 131 | if (!xSemaphoreTake(this->scan_end_lock_, 0L)) { 132 | ESP_LOGW(TAG, "Cannot start scan!"); 133 | return; 134 | } 135 | 136 | ESP_LOGD(TAG, "Starting scan..."); 137 | if (!first) { 138 | for (auto *listener : this->listeners_) 139 | listener->on_scan_end(); 140 | } 141 | 142 | this->already_discovered_.clear(); 143 | 144 | esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, this->scan_duration_, 0); 145 | 146 | this->set_timeout("scan", this->scan_duration_ * 3000, []() { 147 | ESP_LOGW(TAG, "ESP-IDF BT scan never terminated, rebooting to restore BT stack..."); 148 | App.reboot(); 149 | }); 150 | } 151 | 152 | void ESP32BTTracker::gap_event_handler(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) { 153 | switch (event) { 154 | case ESP_BT_GAP_DISC_RES_EVT: { 155 | ESPBTDevice *device = new ESPBTDevice(); 156 | device->parse_scan_rst(param->disc_res); 157 | global_esp32_bt_tracker->bt_devices_.push(device); 158 | } break; 159 | case ESP_BT_GAP_DISC_STATE_CHANGED_EVT: 160 | if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STARTED) { 161 | global_esp32_bt_tracker->gap_scan_start_complete(); 162 | } else if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STOPPED) { 163 | global_esp32_bt_tracker->gap_scan_stop_complete(); 164 | // 可以查询更多信息 165 | } 166 | break; 167 | default: 168 | break; 169 | } 170 | } 171 | 172 | void ESP32BTTracker::dump_config() { 173 | ESP_LOGCONFIG(TAG, "BT Tracker:"); 174 | ESP_LOGCONFIG(TAG, " Scan Duration: %u s", (uint8_t) (this->scan_duration_ * 1.28)); 175 | } 176 | 177 | void ESP32BTTracker::gap_scan_start_complete() { ESP_LOGD(TAG, "scan started"); } 178 | 179 | void ESP32BTTracker::gap_scan_stop_complete() { 180 | xSemaphoreGive(this->scan_end_lock_); 181 | ESP_LOGD(TAG, "scan stoped"); 182 | } 183 | 184 | void ESP32BTTracker::print_bt_device_info(ESPBTDevice &device) { 185 | const uint64_t address = device.address_uint64(); 186 | for (auto &disc : this->already_discovered_) { 187 | if (disc == address) 188 | return; 189 | } 190 | this->already_discovered_.push_back(address); 191 | 192 | ESP_LOGD(TAG, "Found device %s RSSI=%d", device.address_str().c_str(), device.get_rssi()); 193 | 194 | if (!device.get_name().empty()) 195 | ESP_LOGD(TAG, " Name: '%s'", device.get_name().c_str()); 196 | } 197 | 198 | 199 | void ESPBTDevice::parse_scan_rst(const esp_bt_gap_cb_param_t::disc_res_param ¶m) { 200 | esp_bt_gap_dev_prop_t *p; 201 | for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) 202 | this->address_[i] = param.bda[i]; 203 | for (int i = 0; i < param.num_prop; i++) { 204 | p = param.prop + i; 205 | switch (p->type) { 206 | case ESP_BT_GAP_DEV_PROP_RSSI: 207 | this->rssi_ = *(int8_t *) (p->val); 208 | break; 209 | case ESP_BT_GAP_DEV_PROP_BDNAME: 210 | this->name_.assign((char *) p->val, p->len); 211 | break; 212 | /* case ESP_BT_GAP_DEV_PROP_EIR: 213 | parse_eir_((uint8_t *) p->val); */ 214 | default: 215 | break; 216 | } 217 | } 218 | 219 | #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE 220 | ESP_LOGVV(TAG, "Parse Result:"); 221 | 222 | ESP_LOGVV(TAG, " Address: %02X:%02X:%02X:%02X:%02X:%02X (%s)", this->address_[0], this->address_[1], 223 | this->address_[2], this->address_[3], this->address_[4], this->address_[5], address_type); 224 | 225 | ESP_LOGVV(TAG, " RSSI: %d", this->rssi_); 226 | ESP_LOGVV(TAG, " Name: '%s'", this->name_.c_str()); 227 | #endif 228 | } 229 | /* void ESPBTDevice::parse_eir_(uint8_t *eir) { 230 | uint8_t len = 0; 231 | uint16_t *uuid16 = (uint16_t *) esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_INCMPL_16BITS_UUID, &len); 232 | for (int i = 0; i < len; i++) { 233 | this->service_uuids_.push_back(ESPBTUUID::from_uint16(uuid16[i])); 234 | } 235 | 236 | len = 0; 237 | uint32_t *uuid32 = (uint32_t *) esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_INCMPL_32BITS_UUID, &len); 238 | for (int i = 0; i < len; i++) { 239 | this->service_uuids_.push_back(ESPBTUUID::from_uint32(uuid32[i])); 240 | } 241 | 242 | len = 0; 243 | uint8_t *uuid = esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_INCMPL_128BITS_UUID, &len); 244 | for (int i = 0; i < len; i++) { 245 | this->service_uuids_.push_back(ESPBTUUID::from_raw(&uuid[i * ESP_UUID_LEN_128])); 246 | } 247 | 248 | len = 0; 249 | uint8_t *tx_power = esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_TX_POWER_LEVEL, &len); 250 | for (int i = 0; i < len; i++) { 251 | this->tx_powers_.push_back(tx_power[i]); 252 | } 253 | 254 | len = 0; 255 | uint8_t *manufacturer_data = esp_bt_gap_resolve_eir_data(eir, ESP_BT_EIR_TYPE_MANU_SPECIFIC, &len); 256 | if (len > 0) { 257 | ServiceData data{}; 258 | data.uuid = ESPBTUUID::from_uint16(*reinterpret_cast(manufacturer_data)); 259 | data.data.assign(manufacturer_data + 2UL, manufacturer_data + len); 260 | this->manufacturer_datas_.push_back(data); 261 | } 262 | } */ 263 | 264 | std::string ESPBTDevice::address_str() const { 265 | char mac[24]; 266 | snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", this->address_[0], this->address_[1], this->address_[2], 267 | this->address_[3], this->address_[4], this->address_[5]); 268 | return mac; 269 | } 270 | uint64_t ESPBTDevice::address_uint64() const { return bt_addr_to_uint64(this->address_); } 271 | 272 | } // namespace esp32_bt_tracker 273 | } // namespace esphome 274 | 275 | #endif 276 | -------------------------------------------------------------------------------- /components/esp32_bt_tracker/esp32_bt_tracker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/core/helpers.h" 5 | #include "queue.h" 6 | 7 | #ifdef USE_ESP32 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace esphome { 16 | namespace esp32_bt_tracker { 17 | 18 | class ESPBTDevice { 19 | public: 20 | void parse_scan_rst(const esp_bt_gap_cb_param_t::disc_res_param ¶m); 21 | 22 | std::string address_str() const; 23 | 24 | uint64_t address_uint64() const; 25 | 26 | const uint8_t *address() const { return address_; } 27 | 28 | int get_rssi() const { return rssi_; } 29 | const std::string &get_name() const { return this->name_; } 30 | 31 | protected: 32 | // void parse_eir_(uint8_t *eir); 33 | 34 | esp_bd_addr_t address_{ 35 | 0, 36 | }; 37 | int8_t rssi_{-128}; 38 | std::string name_{}; 39 | }; 40 | 41 | class ESP32BTTracker; 42 | 43 | class ESPBTDeviceListener { 44 | public: 45 | virtual void on_scan_end() {} 46 | virtual bool parse_device(const ESPBTDevice &device) = 0; 47 | void set_parent(ESP32BTTracker *parent) { parent_ = parent; } 48 | 49 | protected: 50 | ESP32BTTracker *parent_{nullptr}; 51 | }; 52 | 53 | class ESP32BTTracker : public Component, public Nameable { 54 | public: 55 | void setup() override; 56 | void dump_config() override; 57 | 58 | void loop() override; 59 | uint32_t hash_base() override; 60 | void set_scan_duration(uint8_t scan_duration); 61 | 62 | void register_listener(ESPBTDeviceListener *listener) { 63 | listener->set_parent(this); 64 | this->listeners_.push_back(listener); 65 | } 66 | 67 | protected: 68 | bool bt_setup(); 69 | void start_scan(bool first); 70 | static void IRAM_ATTR gap_event_handler(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param); 71 | void gap_scan_start_complete(); 72 | void gap_scan_stop_complete(); 73 | void print_bt_device_info(ESPBTDevice &device); 74 | 75 | std::vector already_discovered_; 76 | std::vector listeners_; 77 | 78 | SemaphoreHandle_t scan_end_lock_; 79 | uint8_t scan_duration_{0x10}; 80 | 81 | Queue bt_devices_; 82 | }; 83 | 84 | extern ESP32BTTracker *global_esp32_bt_tracker; 85 | 86 | } // namespace esp32_bt_tracker 87 | } // namespace esphome 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /components/esp32_bt_tracker/queue.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "esphome/core/component.h" 3 | #include "esphome/core/helpers.h" 4 | 5 | #include 6 | #include 7 | 8 | #ifdef USE_ESP32 9 | 10 | #include 11 | #include 12 | 13 | /* 14 | * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather 15 | * than trying to deal wth various locking strategies, all incoming GAP and GATT 16 | * events will simply be placed on a semaphore guarded queue. The next time the 17 | * component runs loop(), these events are popped off the queue and handed at 18 | * this safer time. 19 | */ 20 | 21 | namespace esphome { 22 | namespace esp32_bt_tracker { 23 | 24 | template class Queue { 25 | public: 26 | Queue() { m = xSemaphoreCreateMutex(); } 27 | 28 | void push(T *element) { 29 | if (element == nullptr) 30 | return; 31 | if (xSemaphoreTake(m, 0)) { 32 | q.push(element); 33 | xSemaphoreGive(m); 34 | } 35 | } 36 | 37 | T *pop() { 38 | T *element = nullptr; 39 | 40 | if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) { 41 | if (!q.empty()) { 42 | element = q.front(); 43 | q.pop(); 44 | } 45 | xSemaphoreGive(m); 46 | } 47 | return element; 48 | } 49 | 50 | protected: 51 | std::queue q; 52 | SemaphoreHandle_t m; 53 | }; 54 | 55 | 56 | 57 | } // namespace esp32_bt_tracker 58 | } // namespace esphome 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /components/fpm383c/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.core import TimePeriod 4 | from esphome.components.light.types import LightEffect 5 | from esphome.components.light.effects import register_monochromatic_effect 6 | from esphome.components import uart 7 | from esphome import automation 8 | from esphome.const import ( 9 | CONF_NAME, 10 | CONF_ID, 11 | CONF_TRIGGER_ID, 12 | CONF_ON_FINGER_SCAN_MATCHED, 13 | CONF_ON_FINGER_SCAN_UNMATCHED, 14 | CONF_MIN_BRIGHTNESS, 15 | CONF_MAX_BRIGHTNESS, 16 | CONF_RATE, 17 | CONF_COUNT, 18 | ) 19 | 20 | DEPENDENCIES = ["uart"] 21 | AUTO_LOAD = ["light"] 22 | 23 | CONF_FPM383C_ID = "fpm383c_id" 24 | 25 | CONF_ON_TOUCH = "on_touch" 26 | CONF_ON_RELEASE = "on_release" 27 | CONF_ON_RESET = "on_reset" 28 | CONF_ON_FINGER_REGISTER_PROGRESS="on_finger_register_progress" 29 | CONF_ON_FINGER_REGISTER_SUCESSED="on_finger_register_sucessed" 30 | CONF_ON_FINGER_REGISTER_FAILED="on_finger_register_failed" 31 | CONF_ON_LENGTH= "on_length" 32 | CONF_OFF_LENGTH = "off_length" 33 | CONF_AUTO_LEARNING = "auto_learning" 34 | 35 | fpm383c_ns = cg.esphome_ns.namespace("fpm383c") 36 | FPM383cComponent = fpm383c_ns.class_( 37 | "FPM383cComponent", cg.PollingComponent, uart.UARTDevice 38 | ) 39 | TouchTrigger = fpm383c_ns.class_( 40 | "TouchTrigger", automation.Trigger.template() 41 | ) 42 | ReleaseTrigger = fpm383c_ns.class_( 43 | "ReleaseTrigger", automation.Trigger.template() 44 | ) 45 | ResetTrigger = fpm383c_ns.class_( 46 | "ResetTrigger", automation.Trigger.template() 47 | ) 48 | RegisterProgress = fpm383c_ns.struct("RegisterProgress") 49 | RegisterProgressTrigger = fpm383c_ns.class_( 50 | "RegisterProgressTrigger", automation.Trigger.template(RegisterProgress) 51 | ) 52 | MatchResult = fpm383c_ns.struct("MatchResult") 53 | MatchSucessedTrigger = fpm383c_ns.class_( 54 | "MatchSucessedTrigger", automation.Trigger.template(MatchResult) 55 | ) 56 | MatchFailedTrigger = fpm383c_ns.class_( 57 | "MatchFailedTrigger", automation.Trigger.template() 58 | ) 59 | 60 | RegisterFingerprintAction = fpm383c_ns.class_( 61 | "RegisterFingerprintAction", automation.Action 62 | ) 63 | ClearFingerprintAction = fpm383c_ns.class_( 64 | "ClearFingerprintAction", automation.Action 65 | ) 66 | CancelAction = fpm383c_ns.class_( 67 | "CancelAction", automation.Action 68 | ) 69 | ResetAction = fpm383c_ns.class_( 70 | "ResetAction", automation.Action 71 | ) 72 | 73 | BreathingLightEffect = fpm383c_ns.class_( 74 | "BreathingLightEffect", LightEffect 75 | ) 76 | 77 | FlashingLightEffect = fpm383c_ns.class_( 78 | "FlashingLightEffect", LightEffect 79 | ) 80 | 81 | CONFIG_SCHEMA = cv.All( 82 | cv.Schema( 83 | { 84 | cv.GenerateID(): cv.declare_id(FPM383cComponent), 85 | cv.Optional(CONF_AUTO_LEARNING, default=True): cv.boolean, 86 | cv.Optional( 87 | CONF_ON_TOUCH 88 | ): automation.validate_automation( 89 | { 90 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( 91 | TouchTrigger 92 | ), 93 | } 94 | ), 95 | cv.Optional( 96 | CONF_ON_RELEASE 97 | ): automation.validate_automation( 98 | { 99 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( 100 | ReleaseTrigger 101 | ), 102 | } 103 | ), 104 | cv.Optional( 105 | CONF_ON_RESET 106 | ): automation.validate_automation( 107 | { 108 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( 109 | ResetTrigger 110 | ), 111 | } 112 | ), 113 | cv.Optional( 114 | CONF_ON_FINGER_SCAN_MATCHED 115 | ): automation.validate_automation( 116 | { 117 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( 118 | MatchSucessedTrigger 119 | ), 120 | } 121 | ), 122 | cv.Optional( 123 | CONF_ON_FINGER_SCAN_UNMATCHED 124 | ): automation.validate_automation( 125 | { 126 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( 127 | MatchFailedTrigger 128 | ), 129 | } 130 | ), 131 | cv.Optional( 132 | CONF_ON_FINGER_REGISTER_PROGRESS 133 | ): automation.validate_automation( 134 | { 135 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( 136 | RegisterProgressTrigger 137 | ), 138 | } 139 | ), 140 | } 141 | ) 142 | .extend(cv.polling_component_schema("200ms")) 143 | .extend(uart.UART_DEVICE_SCHEMA) 144 | ) 145 | 146 | 147 | async def to_code(config): 148 | var = cg.new_Pvariable(config[CONF_ID]) 149 | await cg.register_component(var, config) 150 | await uart.register_uart_device(var, config) 151 | cg.add(var.set_auto_learning(config[CONF_AUTO_LEARNING])) 152 | 153 | for conf in config.get(CONF_ON_TOUCH, []): 154 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 155 | cg.add(var.add_touch_listener(trigger)) 156 | await automation.build_automation(trigger, [], conf) 157 | for conf in config.get(CONF_ON_RELEASE, []): 158 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 159 | cg.add(var.add_touch_listener(trigger)) 160 | await automation.build_automation(trigger, [], conf) 161 | for conf in config.get(CONF_ON_RESET, []): 162 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 163 | cg.add(var.add_reset_listener(trigger)) 164 | await automation.build_automation(trigger, [], conf) 165 | for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []): 166 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 167 | cg.add(var.add_fingerprint_match_listener(trigger)) 168 | await automation.build_automation(trigger, [(MatchResult, "x")], conf) 169 | for conf in config.get(CONF_ON_FINGER_SCAN_UNMATCHED, []): 170 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 171 | cg.add(var.add_fingerprint_match_listener(trigger)) 172 | await automation.build_automation(trigger, [], conf) 173 | for conf in config.get(CONF_ON_FINGER_REGISTER_PROGRESS, []): 174 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 175 | cg.add(var.add_fingerprint_register_listener(trigger)) 176 | await automation.build_automation(trigger, [(RegisterProgress, "x")], conf) 177 | 178 | ACTION_SCHEMA = cv.Schema( 179 | { 180 | cv.GenerateID(): cv.use_id(FPM383cComponent), 181 | } 182 | ) 183 | 184 | @automation.register_action( 185 | "fpm383c.register", RegisterFingerprintAction, ACTION_SCHEMA 186 | ) 187 | async def register_to_code(config, action_id, template_args, args): 188 | paren = await cg.get_variable(config[CONF_ID]) 189 | var = cg.new_Pvariable(action_id, template_args, paren) 190 | return var 191 | 192 | 193 | @automation.register_action( 194 | "fpm383c.clear", ClearFingerprintAction, ACTION_SCHEMA 195 | ) 196 | async def clear_to_code(config, action_id, template_args, args): 197 | paren = await cg.get_variable(config[CONF_ID]) 198 | var = cg.new_Pvariable(action_id, template_args, paren) 199 | return var 200 | 201 | @automation.register_action( 202 | "fpm383c.cancel", CancelAction, ACTION_SCHEMA 203 | ) 204 | async def cancel_to_code(config, action_id, template_args, args): 205 | paren = await cg.get_variable(config[CONF_ID]) 206 | var = cg.new_Pvariable(action_id, template_args, paren) 207 | return var 208 | 209 | @automation.register_action( 210 | "fpm383c.reset", ResetAction, ACTION_SCHEMA 211 | ) 212 | async def reset_to_code(config, action_id, template_args, args): 213 | paren = await cg.get_variable(config[CONF_ID]) 214 | var = cg.new_Pvariable(action_id, template_args, paren) 215 | return var 216 | 217 | 218 | @register_monochromatic_effect( 219 | "breathing", 220 | BreathingLightEffect, 221 | "Breathing", 222 | { 223 | cv.Optional(CONF_RATE, default="50%"): cv.percentage_int, 224 | cv.Optional(CONF_MIN_BRIGHTNESS, default="0%"): cv.percentage_int, 225 | cv.Optional(CONF_MAX_BRIGHTNESS, default="100%"): cv.percentage_int, 226 | }, 227 | ) 228 | async def breathing_effect_to_code(config, effect_id): 229 | effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) 230 | cg.add( 231 | effect.set_min_max_rate_brightness( 232 | config[CONF_MIN_BRIGHTNESS], config[CONF_MAX_BRIGHTNESS], config[CONF_RATE] 233 | ) 234 | ) 235 | return effect 236 | 237 | @register_monochromatic_effect( 238 | "flashing", 239 | FlashingLightEffect, 240 | "Flashing", 241 | { 242 | cv.Optional(CONF_ON_LENGTH, default="200ms"): cv.All( 243 | cv.positive_time_period_milliseconds, 244 | cv.Range(max=TimePeriod(milliseconds=2550)), 245 | ), 246 | cv.Optional(CONF_OFF_LENGTH, default="200ms"): cv.All( 247 | cv.positive_time_period_milliseconds, 248 | cv.Range(max=TimePeriod(milliseconds=2550)), 249 | ), 250 | cv.Optional(CONF_COUNT, default="3"): cv.int_range(min=1, max=255), 251 | }, 252 | ) 253 | async def flashing_effect_to_code(config, effect_id): 254 | effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) 255 | cg.add( 256 | effect.set_on_off_count_brightness( 257 | config[CONF_ON_LENGTH], config[CONF_OFF_LENGTH], config[CONF_COUNT] 258 | ) 259 | ) 260 | return effect -------------------------------------------------------------------------------- /components/fpm383c/fpm383c.cpp: -------------------------------------------------------------------------------- 1 | #include "fpm383c.h" 2 | #include "esphome/core/log.h" 3 | #include "esphome/core/helpers.h" 4 | #include 5 | 6 | namespace esphome { 7 | namespace fpm383c { 8 | 9 | static const char *const TAG = "fpm383x"; 10 | 11 | #define UART_FRAME_FLAG 0xF1, 0x1F, 0xE2, 0x2E, 0xB6, 0x6B, 0xA8, 0x8A 12 | #define DEFAULT_PASSWORD 0x00, 0x00, 0x00, 0x00 13 | #define UART_WAIT_MS 100 14 | 15 | static const uint8_t UART_FRAME_START[8] = {UART_FRAME_FLAG}; 16 | 17 | void FPM383cComponent::setup() { this->command_(0x03, 0x01); } 18 | 19 | void FPM383cComponent::dump_config() { 20 | ESP_LOGCONFIG(TAG, "fpm383x:"); 21 | this->check_uart_settings(57600); 22 | ESP_LOGCONFIG(TAG, " Model ID: %s", this->model_id_); 23 | } 24 | 25 | void FPM383cComponent::loop() { 26 | if (!this->available()) { 27 | return; 28 | } 29 | 30 | while (this->available()) { 31 | uint8_t byte; 32 | this->read_byte(&byte); 33 | if (this->parse_(byte)) { 34 | ESP_LOGVV(TAG, "Parsed: 0x%02X", byte); 35 | } else { 36 | this->rx_buffer_.clear(); 37 | } 38 | } 39 | 40 | this->wait_at_ = millis() + (this->rx_buffer_.empty() ? 0 : UART_WAIT_MS); 41 | } 42 | 43 | void FPM383cComponent::update() { 44 | switch (this->status_) { 45 | case STATUS_REGISTING: { 46 | uint32_t current_time = millis(); 47 | if (current_time - this->last_register_progress_time_ > 15000) { 48 | this->status_ = STATUS_IDLE; 49 | } 50 | break; 51 | } 52 | case STATUS_MATCHING: { 53 | // 查询匹配结果 54 | if (!this->have_wait_()) { 55 | this->command_(0x01, 0x22); 56 | this->status_ = STATUS_IDLE; 57 | } 58 | break; 59 | } 60 | default: 61 | break; 62 | } 63 | 64 | if (this->enable_auto_learning && this->renew_id_ != 0xFFFF && !this->have_wait_()) { 65 | // 自学习指纹 66 | std::vector update_command = {DEFAULT_PASSWORD, 0x01, 0x16, (uint8_t) (this->renew_id_ >> 8), 67 | (uint8_t) (this->renew_id_ & 0xFF)}; 68 | this->command_(update_command); 69 | this->renew_id_ = 0xFFFF; 70 | } 71 | 72 | // 查询手指在位 73 | if (!this->have_wait_()) { 74 | this->command_(0x01, 0x35); 75 | } 76 | } 77 | 78 | void FPM383cComponent::turn_on_light(Color color) { 79 | std::vector command = {DEFAULT_PASSWORD, 0x02, 0x0F, 0x01, color, 0, 0, 0}; 80 | this->command_(command); 81 | } 82 | 83 | void FPM383cComponent::turn_off_light() { 84 | std::vector command = {DEFAULT_PASSWORD, 0x02, 0x0F, 0x00, 0, 0, 0, 0}; 85 | this->command_(command); 86 | } 87 | 88 | void FPM383cComponent::breathing_light(Color color, uint8_t min_level, uint8_t max_level, uint8_t rate) { 89 | std::vector command = {DEFAULT_PASSWORD, 0x02, 0x0F, 0x03, color, max_level, min_level, rate}; 90 | this->command_(command); 91 | } 92 | 93 | void FPM383cComponent::flashing_light(Color color, uint8_t on_10ms, uint8_t off_10ms, uint8_t count) { 94 | std::vector command = {DEFAULT_PASSWORD, 0x02, 0x0F, 0x04, color, on_10ms, off_10ms, count}; 95 | this->command_(command); 96 | } 97 | 98 | void FPM383cComponent::register_fingerprint() { 99 | if (this->status_ == STATUS_REGISTING) { 100 | // 取消注册 101 | this->command_(0x01, 0x15); 102 | } 103 | // 自动注册 104 | std::vector command = {DEFAULT_PASSWORD, 0x01, 0x18, 0x01, 6, 0xFF, 0xFF}; 105 | this->command_(command); 106 | 107 | this->last_register_progress_time_ = millis(); 108 | this->status_ = STATUS_REGISTING; 109 | } 110 | 111 | void FPM383cComponent::clear_fingerprint() { 112 | // 清除全部指纹 113 | std::vector command = {DEFAULT_PASSWORD, 0x01, 0x31, 0x01, 0x00, 0x00}; 114 | this->command_(command); 115 | } 116 | 117 | void FPM383cComponent::cancel() { this->command_(0x01, 0x15); } 118 | 119 | void FPM383cComponent::reset() { this->command_(0x02, 0x02); } 120 | 121 | int FPM383cComponent::parse_(uint8_t byte) { 122 | size_t at = this->rx_buffer_.size(); 123 | this->rx_buffer_.push_back(byte); 124 | const uint8_t *raw = &this->rx_buffer_[0]; 125 | 126 | static uint16_t frame_length = 0; 127 | 128 | // Start 129 | if (at == 0 && raw[at] == 0x55) { // reset flag 130 | this->on_reset(); 131 | return false; 132 | } 133 | 134 | if (at < sizeof(UART_FRAME_START)) // read frame header:F1,1F,E2,2E,B6,6B,A8,8A 135 | return byte == UART_FRAME_START[at]; 136 | 137 | if (at < sizeof(UART_FRAME_START) + 1) // read data length : 2 bytes 138 | return true; 139 | 140 | if (at == sizeof(UART_FRAME_START) + 1) { // 141 | frame_length = encode_uint16(raw[at - 1], raw[at]) + sizeof(UART_FRAME_START) + 2 + 1; 142 | // ^data length ^header ^data length ^SOF checksum 143 | return frame_length > 17 && frame_length < 256; 144 | } 145 | 146 | if (at == sizeof(UART_FRAME_START) + 2) { // read SOF checksum and check 147 | return raw[at] == checksum_(raw, at); 148 | } 149 | 150 | if (at < frame_length - 1) 151 | return true; 152 | 153 | // at last 154 | 155 | // check data checksum 156 | if (raw[at] != checksum_((raw + sizeof(UART_FRAME_START) + 3), (at - sizeof(UART_FRAME_START) - 3))) { 157 | ESP_LOGE(TAG, "Data checksum failed!"); 158 | return false; 159 | } 160 | 161 | uint16_t ack = encode_uint16(raw[15], raw[16]); 162 | switch (ack) { 163 | // 指纹在位 164 | case 0x0135: { 165 | uint8_t status = raw[21]; 166 | this->on_touch_(status == 1); 167 | break; 168 | } 169 | // 自动注册结果 170 | case 0x0118: { 171 | this->last_register_progress_time_ = millis(); 172 | uint16_t id = encode_uint16(raw[22], raw[23]); 173 | this->on_register_progress_(id, raw[21], raw[24]); 174 | break; 175 | } 176 | // 匹配结果 177 | case 0x0122: { 178 | uint32_t error = encode_uint32(raw[17], raw[18], raw[19], raw[20]); 179 | if (error == 0) { 180 | uint16_t sucessed = encode_uint16(raw[21], raw[22]); 181 | uint16_t score = encode_uint16(raw[23], raw[24]); 182 | uint16_t id = encode_uint16(raw[25], raw[26]); 183 | ESP_LOGV(TAG, "sucessed: %04X, id:%04X, score:%04X", sucessed, id, score); 184 | on_match_((id != 0xFFFF), id, score); 185 | } else if (error == 0x04) { 186 | this->status_ = STATUS_MATCHING; 187 | } else { 188 | on_match_(false, 0, 0); 189 | } 190 | break; 191 | } 192 | // ID 193 | case 0x0301: { 194 | for (uint8_t i = 0; i < 16; i++) { 195 | this->model_id_[i] = raw[21 + i]; 196 | } 197 | this->model_id_[16] = 0x00; 198 | break; 199 | } 200 | // 重置 201 | case 0x0202: { 202 | this->status_ = STATUS_IDLE; 203 | break; 204 | } 205 | default: 206 | break; 207 | } 208 | 209 | // TODO: 检查密码 210 | return false; 211 | } 212 | 213 | void FPM383cComponent::on_touch_(bool touched) { 214 | if (touched == this->flag_touched_) 215 | return; 216 | this->flag_touched_ = touched; 217 | 218 | if (touched) { 219 | if (this->status_ == STATUS_IDLE) { 220 | this->command_(0x01, 0x21); 221 | this->status_ = STATUS_MATCHING; 222 | } 223 | } else if (this->status_ == STATUS_MATCHING) { 224 | this->status_ = STATUS_IDLE; 225 | } 226 | 227 | for (auto *listener : this->touch_listeners_) { 228 | listener->on_touch(touched); 229 | } 230 | } 231 | 232 | void FPM383cComponent::on_reset() { 233 | for (auto *listener : this->reset_listeners_) { 234 | listener->on_reset(); 235 | } 236 | } 237 | 238 | void FPM383cComponent::on_match_(bool sucessed, uint16_t id, uint16_t score) { 239 | this->renew_id_ = sucessed ? id : 0xFFFF; 240 | for (auto *listener : this->fingerprint_match_listeners_) { 241 | listener->on_match(sucessed, id, score); 242 | } 243 | } 244 | 245 | void FPM383cComponent::on_register_progress_(uint16_t id, uint8_t step, uint8_t progress_in_percent) { 246 | if (progress_in_percent >= 100 && this->status_ == STATUS_REGISTING) { 247 | this->status_ = STATUS_IDLE; 248 | } 249 | if (step == 0xFF) 250 | return; 251 | for (auto *listener : this->fingerprint_register_listeners_) { 252 | listener->on_progress(id, step, progress_in_percent); 253 | } 254 | } 255 | 256 | void FPM383cComponent::command_(uint8_t cmd1, uint8_t cmd2) { 257 | std::vector command = {DEFAULT_PASSWORD, cmd1, cmd2}; 258 | this->command_(command); 259 | } 260 | 261 | void FPM383cComponent::command_(std::vector &data) { 262 | std::vector command = {UART_FRAME_FLAG}; 263 | uint16_t data_length = data.size() + 1; 264 | command.push_back((uint8_t) (data_length >> 8)); 265 | command.push_back((uint8_t) (data_length & 0xFF)); 266 | command.push_back(checksum_(&command[0], command.size())); 267 | command.insert(command.end(), data.begin(), data.end()); 268 | command.push_back(checksum_(&data[0], data.size())); 269 | this->write_array(command); 270 | this->flush(); 271 | this->wait_at_ = millis() + UART_WAIT_MS; 272 | } 273 | 274 | bool FPM383cComponent::have_wait_() { 275 | auto now = millis(); 276 | return (now < this->wait_at_) && (now > this->wait_at_ - UART_WAIT_MS); 277 | } 278 | 279 | uint8_t FPM383cComponent::checksum_(const uint8_t *data, const uint32_t length) { 280 | int8_t sum = 0; 281 | for (uint32_t i = 0; i < length; i++) { 282 | sum += data[i]; 283 | } 284 | return (uint8_t) ((~sum) + 1); 285 | } 286 | 287 | } // namespace fpm383c 288 | } // namespace esphome 289 | -------------------------------------------------------------------------------- /components/fpm383c/light.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import light 4 | from esphome.const import CONF_OUTPUT_ID, CONF_GAMMA_CORRECT, CONF_DEFAULT_TRANSITION_LENGTH 5 | from . import FPM383cComponent 6 | 7 | fpm383c_ns = cg.esphome_ns.namespace("fpm383c") 8 | FPM383cLightOutput = fpm383c_ns.class_("FPM383cLightOutput", light.LightOutput) 9 | 10 | CONF_FPM383C_ID = "fpm383c_id" 11 | 12 | CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( 13 | { 14 | cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(FPM383cLightOutput), 15 | cv.GenerateID(CONF_FPM383C_ID): cv.use_id(FPM383cComponent), 16 | cv.Optional(CONF_GAMMA_CORRECT, default=1): cv.positive_float, 17 | cv.Optional( 18 | CONF_DEFAULT_TRANSITION_LENGTH, default="0s" 19 | ): cv.positive_time_period_milliseconds, 20 | } 21 | ) 22 | 23 | 24 | async def to_code(config): 25 | var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) 26 | await light.register_light(var, config) 27 | 28 | fpm383c = await cg.get_variable(config[CONF_FPM383C_ID]) 29 | cg.add(var.set_parent(fpm383c)) 30 | -------------------------------------------------------------------------------- /components/ptx_yk1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanh7/esphome-custom-components/78a9afe3c2164fee312d50f547a7c0cea73e6d86/components/ptx_yk1/__init__.py -------------------------------------------------------------------------------- /components/ptx_yk1/binary_sensor.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import binary_sensor, esp32_ble_tracker 4 | from esphome.const import ( 5 | CONF_MAC_ADDRESS, 6 | CONF_TIMEOUT, 7 | ) 8 | 9 | DEPENDENCIES = ["esp32_ble_tracker"] 10 | 11 | ptx_yk1_ns = cg.esphome_ns.namespace("ptx_yk1") 12 | PTXYK1Device = ptx_yk1_ns.class_( 13 | "PTXYK1Device", 14 | binary_sensor.BinarySensor, 15 | cg.Component, 16 | esp32_ble_tracker.ESPBTDeviceListener, 17 | ) 18 | 19 | 20 | CONFIG_SCHEMA = cv.All( 21 | binary_sensor.binary_sensor_schema(PTXYK1Device) 22 | .extend( 23 | { 24 | cv.Required(CONF_MAC_ADDRESS): cv.mac_address, 25 | cv.Optional( 26 | CONF_TIMEOUT, default="300ms" 27 | ): cv.positive_time_period_milliseconds, 28 | } 29 | ) 30 | .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) 31 | .extend(cv.COMPONENT_SCHEMA), 32 | ) 33 | 34 | 35 | async def to_code(config): 36 | var = await binary_sensor.new_binary_sensor(config) 37 | await cg.register_component(var, config) 38 | await esp32_ble_tracker.register_ble_device(var, config) 39 | cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) 40 | cg.add(var.set_timeout_ms(config[CONF_TIMEOUT])) 41 | -------------------------------------------------------------------------------- /components/ptx_yk1/ptx_yk1_device.cpp: -------------------------------------------------------------------------------- 1 | #include "ptx_yk1_device.h" 2 | #include "esphome/core/log.h" 3 | 4 | #ifdef USE_ESP32 5 | 6 | namespace esphome { 7 | namespace ptx_yk1 { 8 | 9 | static const char *const TAG = "ptx_yk1"; 10 | 11 | void PTXYK1Device::dump_config() { 12 | LOG_BINARY_SENSOR("", "PTX YK1", this); 13 | ESP_LOGCONFIG(TAG, " BLE Timeout: %dms", this->timeout_ms_); 14 | } 15 | 16 | void PTXYK1Device::loop() { 17 | uint32_t now = millis(); 18 | if (this->need_timeout_ && ((now < this->time_) || (now - this->time_) > this->timeout_ms_)) { 19 | this->publish_state(false); 20 | this->need_timeout_ = false; 21 | } 22 | } 23 | 24 | bool PTXYK1Device::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { 25 | if (device.address_uint64() != this->address_) { 26 | return false; 27 | } 28 | for (auto adv : device.get_manufacturer_datas()) { 29 | if (adv.uuid != esp32_ble_tracker::ESPBTUUID::from_uint16(0x5348) || adv.data.size() != 14) { 30 | continue; 31 | } 32 | this->time_ = millis(); 33 | uint32_t id = encode_uint24(adv.data[11], adv.data[12], adv.data[13]); 34 | if (id == this->last_id_) { 35 | return true; 36 | } 37 | if (this->state) { 38 | this->publish_state(false); 39 | this->need_timeout_ = true; 40 | } 41 | this->last_id_ = id; 42 | this->publish_state(true); 43 | this->need_timeout_ = true; 44 | return true; 45 | } 46 | return true; 47 | } 48 | 49 | } // namespace ptx_yk1 50 | } // namespace esphome 51 | 52 | #endif 53 | -------------------------------------------------------------------------------- /components/ptx_yk1/ptx_yk1_device.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" 5 | #include "esphome/components/binary_sensor/binary_sensor.h" 6 | 7 | #ifdef USE_ESP32 8 | 9 | namespace esphome { 10 | namespace ptx_yk1 { 11 | 12 | class PTXYK1Device : public binary_sensor::BinarySensorInitiallyOff, 13 | public esp32_ble_tracker::ESPBTDeviceListener, 14 | public Component { 15 | public: 16 | void set_address(uint64_t address) { this->address_ = address; } 17 | 18 | void set_timeout_ms(uint32_t timeout) { this->timeout_ms_ = timeout; } 19 | 20 | void loop() override; 21 | 22 | bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; 23 | void dump_config() override; 24 | float get_setup_priority() const override { return setup_priority::DATA; } 25 | 26 | protected: 27 | uint64_t address_; 28 | 29 | uint32_t timeout_ms_{300}; 30 | uint32_t time_{0}; 31 | uint32_t last_id_{0}; 32 | bool need_timeout_{false}; 33 | }; 34 | 35 | } // namespace ptx_yk1 36 | } // namespace esphome 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /components/rf_bridge_cc1101/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome import automation, pins 4 | from esphome.components import spi, remote_base 5 | from esphome.core import TimePeriod 6 | from esphome.const import ( 7 | CONF_ID, 8 | CONF_TRIGGER_ID, 9 | CONF_PROTOCOL, 10 | CONF_CODE, 11 | CONF_PIN, 12 | CONF_REPEAT, 13 | CONF_TIMES, 14 | CONF_WAIT_TIME, 15 | CONF_MODE, 16 | CONF_DUMP, 17 | CONF_FREQUENCY, 18 | CONF_TOLERANCE, 19 | CONF_BUFFER_SIZE, 20 | CONF_FILTER, 21 | CONF_IDLE 22 | ) 23 | 24 | AUTO_LOAD = ["remote_base"] 25 | DEPENDENCIES = ["spi"] 26 | CODEOWNERS = ["@ryan"] 27 | 28 | rf_bridge_cc1101_ns = cg.esphome_ns.namespace("rf_bridge_cc1101") 29 | RFBridgeComponent = rf_bridge_cc1101_ns.class_( 30 | "RFBridgeComponent", cg.Component, spi.SPIDevice 31 | ) 32 | 33 | 34 | RFBridgeReceiverModeAction = rf_bridge_cc1101_ns.class_( 35 | "RFBridgeReceiverModeAction", automation.Action 36 | ) 37 | 38 | RFBridgeTransmitterModeAction = rf_bridge_cc1101_ns.class_( 39 | "RFBridgeTransmitterModeAction", automation.Action 40 | ) 41 | 42 | RFBridgeTransmitAction = rf_bridge_cc1101_ns.class_( 43 | "RFBridgeTransmitAction", automation.Action 44 | ) 45 | 46 | 47 | CONF_ON_CODE_RECEIVED = "on_code_received" 48 | 49 | 50 | MODE = {"receiver": 0, "transmitter": 1} 51 | 52 | CONFIG_SCHEMA = cv.All( 53 | cv.Schema( 54 | { 55 | cv.GenerateID(): cv.declare_id(RFBridgeComponent), 56 | cv.Optional(CONF_PIN): pins.gpio_output_pin_schema, 57 | cv.Optional(CONF_MODE, default="receiver"): cv.enum( 58 | MODE, upper=False 59 | ), 60 | cv.Optional(CONF_FREQUENCY, default=433.92): cv.float_range(min=300, max=928), 61 | cv.Optional(CONF_DUMP, default=[]): remote_base.validate_dumpers, 62 | cv.Optional(CONF_TOLERANCE, default=50): cv.All( 63 | cv.percentage_int, cv.Range(min=0) 64 | ), 65 | cv.SplitDefault( 66 | CONF_BUFFER_SIZE, esp32="10kb", esp8266="2kb" 67 | ): cv.validate_bytes, 68 | cv.Optional(CONF_FILTER, default="100us"): cv.All( 69 | cv.positive_time_period_microseconds, 70 | cv.Range(max=TimePeriod(microseconds=255)), 71 | ), 72 | cv.Optional( 73 | CONF_IDLE, default="4ms" 74 | ): cv.positive_time_period_microseconds, 75 | cv.Optional(CONF_ON_CODE_RECEIVED): automation.validate_automation( 76 | { 77 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( 78 | remote_base.RCSwitchTrigger 79 | ), 80 | } 81 | ), 82 | } 83 | ) 84 | .extend(cv.COMPONENT_SCHEMA) 85 | .extend(spi.spi_device_schema(cs_pin_required=True)) 86 | ) 87 | 88 | 89 | async def to_code(config): 90 | 91 | if CONF_PIN in config: 92 | pin = await cg.gpio_pin_expression(config[CONF_PIN]) 93 | var = cg.new_Pvariable(config[CONF_ID], pin) 94 | else: 95 | var = cg.new_Pvariable(config[CONF_ID]) 96 | 97 | await cg.register_component(var, config) 98 | await spi.register_spi_device(var, config) 99 | 100 | cg.add(var.set_mode(config[CONF_MODE])) 101 | cg.add(var.set_frequency_mhz(config[CONF_FREQUENCY])) 102 | 103 | dumpers = await remote_base.build_dumpers(config[CONF_DUMP]) 104 | for dumper in dumpers: 105 | cg.add(var.register_dumper(dumper)) 106 | 107 | for conf in config.get(CONF_ON_CODE_RECEIVED, []): 108 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 109 | cg.add(var.register_listener(trigger)) 110 | await automation.build_automation(trigger, [(remote_base.RCSwitchData, "x")], conf) 111 | 112 | cg.add(var.set_tolerance(config[CONF_TOLERANCE])) 113 | cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) 114 | cg.add(var.set_filter_us(config[CONF_FILTER])) 115 | cg.add(var.set_idle_us(config[CONF_IDLE])) 116 | 117 | 118 | RF_BRIDGE_SCHEMA = cv.Schema( 119 | { 120 | cv.GenerateID(): cv.use_id(RFBridgeComponent), 121 | } 122 | ) 123 | 124 | 125 | @automation.register_action( 126 | "rf_bridge_cc1101.receiver_mode", RFBridgeReceiverModeAction, RF_BRIDGE_SCHEMA 127 | ) 128 | async def receiver_mode_to_code(config, action_id, template_args, args): 129 | paren = await cg.get_variable(config[CONF_ID]) 130 | var = cg.new_Pvariable(action_id, template_args, paren) 131 | return var 132 | 133 | 134 | @automation.register_action( 135 | "rf_bridge_cc1101.transmitter_mode", RFBridgeTransmitterModeAction, RF_BRIDGE_SCHEMA 136 | ) 137 | async def transmitter_mode_to_code(config, action_id, template_args, args): 138 | paren = await cg.get_variable(config[CONF_ID]) 139 | var = cg.new_Pvariable(action_id, template_args, paren) 140 | return var 141 | 142 | 143 | RF_BRIDGE_TRANSMIT_SCHEMA = cv.Schema( 144 | { 145 | cv.GenerateID(): cv.use_id(RFBridgeComponent), 146 | cv.Required(CONF_CODE): cv.templatable(cv.string), 147 | cv.Optional(CONF_PROTOCOL, default=1): cv.templatable(cv.int_range(min=1, max=8)), 148 | cv.Optional(CONF_REPEAT, default={CONF_TIMES: 5}): cv.Schema( 149 | { 150 | cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), 151 | cv.Optional(CONF_WAIT_TIME, default="0us"): cv.templatable( 152 | cv.positive_time_period_microseconds 153 | ), 154 | } 155 | ), 156 | } 157 | ) 158 | 159 | 160 | @automation.register_action( 161 | "rf_bridge_cc1101.transmit", RFBridgeTransmitAction, RF_BRIDGE_TRANSMIT_SCHEMA 162 | ) 163 | async def transmit_to_code(config, action_id, template_args, args): 164 | paren = await cg.get_variable(config[CONF_ID]) 165 | var = cg.new_Pvariable(action_id, template_args, paren) 166 | cg.add(var.set_code((await cg.templatable(config[CONF_CODE], args, cg.std_string)))) 167 | template_ = await cg.templatable(config[CONF_PROTOCOL], args, cg.uint8) 168 | cg.add(var.set_protocol(template_)) 169 | template_ = await cg.templatable(config[CONF_REPEAT][CONF_TIMES], args, cg.uint32) 170 | cg.add(var.set_send_times(template_)) 171 | template_ = await cg.templatable(config[CONF_REPEAT][CONF_WAIT_TIME], args, cg.uint32) 172 | cg.add(var.set_send_wait(template_)) 173 | return var 174 | -------------------------------------------------------------------------------- /components/rf_bridge_cc1101/binary_sensor.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import binary_sensor, remote_base 4 | from esphome.const import CONF_NAME, CONF_ID, CONF_CODE, CONF_PROTOCOL 5 | from . import RFBridgeComponent 6 | 7 | CONFIG_RF_BRIDGE_CC1101_ID = "rf_bridge_cc1101_id" 8 | 9 | DEPENDENCIES = ["rf_bridge_cc1101"] 10 | 11 | CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( 12 | { 13 | cv.GenerateID(): cv.declare_id(remote_base.RCSwitchRawReceiver), 14 | cv.GenerateID(CONFIG_RF_BRIDGE_CC1101_ID): cv.use_id(RFBridgeComponent), 15 | cv.Required(CONF_CODE): cv.templatable(cv.string), 16 | cv.Optional(CONF_PROTOCOL, default=1): cv.int_range(min=1, max=8), 17 | } 18 | ) 19 | 20 | 21 | async def to_code(config): 22 | var = cg.new_Pvariable(config[CONF_ID]) 23 | cg.add(var.set_name(config[CONF_NAME])) 24 | await binary_sensor.register_binary_sensor(var, config) 25 | 26 | cg.add(var.set_protocol(remote_base.build_rc_switch_protocol(config[CONF_PROTOCOL]))) 27 | cg.add(var.set_code(config[CONF_CODE])) 28 | 29 | receicer = await cg.get_variable(config[CONFIG_RF_BRIDGE_CC1101_ID]) 30 | cg.add(receicer.register_listener(var)) 31 | -------------------------------------------------------------------------------- /components/rf_bridge_cc1101/cc1101_def.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace esphome { 4 | namespace rf_bridge_cc1101 { 5 | 6 | #define CC1101_IOCFG2 0x00 // GDO2 output pin configuration 7 | #define CC1101_IOCFG1 0x01 // GDO1 output pin configuration 8 | #define CC1101_IOCFG0 0x02 // GDO0 output pin configuration 9 | #define CC1101_FIFOTHR 0x03 // RX FIFO and TX FIFO thresholds 10 | #define CC1101_SYNC1 0x04 // Sync word, high INT8U 11 | #define CC1101_SYNC0 0x05 // Sync word, low INT8U 12 | #define CC1101_PKTLEN 0x06 // Packet length 13 | #define CC1101_PKTCTRL1 0x07 // Packet automation control 14 | #define CC1101_PKTCTRL0 0x08 // Packet automation control 15 | #define CC1101_ADDR 0x09 // Device address 16 | #define CC1101_CHANNR 0x0A // Channel number 17 | #define CC1101_FSCTRL1 0x0B // Frequency synthesizer control 18 | #define CC1101_FSCTRL0 0x0C // Frequency synthesizer control 19 | #define CC1101_FREQ2 0x0D // Frequency control word, high INT8U 20 | #define CC1101_FREQ1 0x0E // Frequency control word, middle INT8U 21 | #define CC1101_FREQ0 0x0F // Frequency control word, low INT8U 22 | #define CC1101_MDMCFG4 0x10 // Modem configuration 23 | #define CC1101_MDMCFG3 0x11 // Modem configuration 24 | #define CC1101_MDMCFG2 0x12 // Modem configuration 25 | #define CC1101_MDMCFG1 0x13 // Modem configuration 26 | #define CC1101_MDMCFG0 0x14 // Modem configuration 27 | #define CC1101_DEVIATN 0x15 // Modem deviation setting 28 | #define CC1101_MCSM2 0x16 // Main Radio Control State Machine configuration 29 | #define CC1101_MCSM1 0x17 // Main Radio Control State Machine configuration 30 | #define CC1101_MCSM0 0x18 // Main Radio Control State Machine configuration 31 | #define CC1101_FOCCFG 0x19 // Frequency Offset Compensation configuration 32 | #define CC1101_BSCFG 0x1A // Bit Synchronization configuration 33 | #define CC1101_AGCCTRL2 0x1B // AGC control 34 | #define CC1101_AGCCTRL1 0x1C // AGC control 35 | #define CC1101_AGCCTRL0 0x1D // AGC control 36 | #define CC1101_WOREVT1 0x1E // High INT8U Event 0 timeout 37 | #define CC1101_WOREVT0 0x1F // Low INT8U Event 0 timeout 38 | #define CC1101_WORCTRL 0x20 // Wake On Radio control 39 | #define CC1101_FREND1 0x21 // Front end RX configuration 40 | #define CC1101_FREND0 0x22 // Front end TX configuration 41 | #define CC1101_FSCAL3 0x23 // Frequency synthesizer calibration 42 | #define CC1101_FSCAL2 0x24 // Frequency synthesizer calibration 43 | #define CC1101_FSCAL1 0x25 // Frequency synthesizer calibration 44 | #define CC1101_FSCAL0 0x26 // Frequency synthesizer calibration 45 | #define CC1101_RCCTRL1 0x27 // RC oscillator configuration 46 | #define CC1101_RCCTRL0 0x28 // RC oscillator configuration 47 | #define CC1101_FSTEST 0x29 // Frequency synthesizer calibration control 48 | #define CC1101_PTEST 0x2A // Production test 49 | #define CC1101_AGCTEST 0x2B // AGC test 50 | #define CC1101_TEST2 0x2C // Various test settings 51 | #define CC1101_TEST1 0x2D // Various test settings 52 | #define CC1101_TEST0 0x2E // Various test settings 53 | 54 | // CC1101 Strobe commands 55 | #define CC1101_SRES 0x30 // Reset chip. 56 | #define CC1101_SFSTXON \ 57 | 0x31 // Enable and calibrate frequency synthesizer (if MCSM0.FS_AUTOCAL=1). 58 | // If in RX/TX: Go to a wait state where only the synthesizer is 59 | // running (for quick RX / TX turnaround). 60 | #define CC1101_SXOFF 0x32 // Turn off crystal oscillator. 61 | #define CC1101_SCAL \ 62 | 0x33 // Calibrate frequency synthesizer and turn it off 63 | // (enables quick start). 64 | #define CC1101_SRX \ 65 | 0x34 // Enable RX. Perform calibration first if coming from IDLE and 66 | // MCSM0.FS_AUTOCAL=1. 67 | #define CC1101_STX \ 68 | 0x35 // In IDLE state: Enable TX. Perform calibration first if 69 | // MCSM0.FS_AUTOCAL=1. If in RX state and CCA is enabled: 70 | // Only go to TX if channel is clear. 71 | #define CC1101_SIDLE \ 72 | 0x36 // Exit RX / TX, turn off frequency synthesizer and exit 73 | // Wake-On-Radio mode if applicable. 74 | #define CC1101_SAFC 0x37 // Perform AFC adjustment of the frequency synthesizer 75 | #define CC1101_SWOR 0x38 // Start automatic RX polling sequence (Wake-on-Radio) 76 | #define CC1101_SPWD 0x39 // Enter power down mode when CSn goes high. 77 | #define CC1101_SFRX 0x3A // Flush the RX FIFO buffer. 78 | #define CC1101_SFTX 0x3B // Flush the TX FIFO buffer. 79 | #define CC1101_SWORRST 0x3C // Reset real time clock. 80 | #define CC1101_SNOP \ 81 | 0x3D // No operation. May be used to pad strobe commands to two 82 | // INT8Us for simpler software. 83 | // CC1101 STATUS REGSITER 84 | #define CC1101_PARTNUM 0x30 85 | #define CC1101_VERSION 0x31 86 | #define CC1101_FREQEST 0x32 87 | #define CC1101_LQI 0x33 88 | #define CC1101_RSSI 0x34 89 | #define CC1101_MARCSTATE 0x35 90 | #define CC1101_WORTIME1 0x36 91 | #define CC1101_WORTIME0 0x37 92 | #define CC1101_PKTSTATUS 0x38 93 | #define CC1101_VCO_VC_DAC 0x39 94 | #define CC1101_TXBYTES 0x3A 95 | #define CC1101_RXBYTES 0x3B 96 | 97 | // CC1101 PATABLE,TXFIFO,RXFIFO 98 | #define CC1101_PATABLE 0x3E 99 | #define CC1101_TXFIFO 0x3F 100 | #define CC1101_RXFIFO 0x3F 101 | 102 | #define WRITE_BURST 0x40 // write burst 103 | #define READ_SINGLE 0x80 // read single 104 | #define READ_BURST 0xC0 // read burst 105 | 106 | } // namespace rf_bridge_cc1101 107 | } // namespace esphome 108 | -------------------------------------------------------------------------------- /components/rf_bridge_cc1101/remote_receiver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/remote_base/remote_base.h" 5 | 6 | namespace esphome { 7 | namespace rf_bridge_cc1101 { 8 | 9 | #ifdef USE_ESP8266 10 | struct RemoteReceiverStore { 11 | static void gpio_intr(RemoteReceiverStore *arg); 12 | 13 | /// Stores the time (in micros) that the leading/falling edge happened at 14 | /// * An even index means a falling edge appeared at the time stored at the index 15 | /// * An uneven index means a rising edge appeared at the time stored at the index 16 | volatile uint32_t *buffer{nullptr}; 17 | /// The position last written to 18 | volatile uint32_t buffer_write_at; 19 | /// The position last read from 20 | uint32_t buffer_read_at{0}; 21 | bool overflow{false}; 22 | uint32_t buffer_size{1000}; 23 | uint8_t filter_us{10}; 24 | ISRInternalGPIOPin pin; 25 | }; 26 | #endif 27 | 28 | class RemoteReceiver : public remote_base::RemoteReceiverBase 29 | #ifdef USE_ESP32 30 | , 31 | public remote_base::RemoteRMTChannel 32 | #endif 33 | { 34 | public: 35 | #ifdef USE_ESP32 36 | RemoteReceiver(InternalGPIOPin *pin, uint8_t mem_block_num = 1) 37 | : RemoteReceiverBase(pin), remote_base::RemoteRMTChannel(mem_block_num) {} 38 | #else 39 | RemoteReceiver(InternalGPIOPin *pin) : RemoteReceiverBase(pin) {} 40 | #endif 41 | void setup(); 42 | void loop(); 43 | 44 | void enable(); 45 | void diable(); 46 | 47 | void set_buffer_size(uint32_t buffer_size) { this->buffer_size_ = buffer_size; } 48 | void set_filter_us(uint8_t filter_us) { this->filter_us_ = filter_us; } 49 | void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; } 50 | 51 | protected: 52 | #ifdef USE_ESP32 53 | void decode_rmt_(rmt_item32_t *item, size_t len); 54 | RingbufHandle_t ringbuf_; 55 | esp_err_t error_code_{ESP_OK}; 56 | #endif 57 | 58 | #ifdef USE_ESP8266 59 | RemoteReceiverStore store_; 60 | HighFrequencyLoopRequester high_freq_; 61 | #endif 62 | 63 | uint32_t buffer_size_{}; 64 | uint8_t filter_us_{10}; 65 | uint32_t idle_us_{10000}; 66 | }; 67 | 68 | } // namespace rf_bridge_cc1101 69 | } // namespace esphome 70 | -------------------------------------------------------------------------------- /components/rf_bridge_cc1101/remote_receiver_esp32.cpp: -------------------------------------------------------------------------------- 1 | #include "remote_receiver.h" 2 | #include "esphome/core/log.h" 3 | 4 | #ifdef USE_ESP32 5 | #include 6 | 7 | namespace esphome { 8 | namespace rf_bridge_cc1101 { 9 | 10 | static const char *const TAG = "rf_bridge_cc1101.receiver.esp32"; 11 | 12 | void RemoteReceiver::setup() { 13 | ESP_LOGCONFIG(TAG, "Setting up Remote Receiver..."); 14 | this->pin_->setup(); 15 | rmt_config_t rmt{}; 16 | this->config_rmt(rmt); 17 | rmt.gpio_num = gpio_num_t(this->pin_->get_pin()); 18 | rmt.rmt_mode = RMT_MODE_RX; 19 | if (this->filter_us_ == 0) { 20 | rmt.rx_config.filter_en = false; 21 | } else { 22 | rmt.rx_config.filter_en = true; 23 | rmt.rx_config.filter_ticks_thresh = this->from_microseconds_(this->filter_us_); 24 | } 25 | rmt.rx_config.idle_threshold = this->from_microseconds_(this->idle_us_); 26 | 27 | esp_err_t error = rmt_config(&rmt); 28 | if (error != ESP_OK) { 29 | this->error_code_ = error; 30 | this->mark_failed(); 31 | return; 32 | } 33 | 34 | error = rmt_driver_install(this->channel_, this->buffer_size_, 0); 35 | if (error != ESP_OK) { 36 | this->error_code_ = error; 37 | this->mark_failed(); 38 | return; 39 | } 40 | error = rmt_get_ringbuf_handle(this->channel_, &this->ringbuf_); 41 | if (error != ESP_OK) { 42 | this->error_code_ = error; 43 | this->mark_failed(); 44 | return; 45 | } 46 | } 47 | 48 | void RemoteReceiver::enable() { 49 | this->pin_->setup(); 50 | rmt_rx_start(this->channel_, true); 51 | } 52 | 53 | void RemoteReceiver::diable() { rmt_rx_stop(this->channel_); } 54 | 55 | void RemoteReceiver::loop() { 56 | size_t len = 0; 57 | auto *item = (rmt_item32_t *) xRingbufferReceive(this->ringbuf_, &len, 0); 58 | if (item != nullptr) { 59 | this->decode_rmt_(item, len); 60 | vRingbufferReturnItem(this->ringbuf_, item); 61 | 62 | if (this->temp_.empty()) 63 | return; 64 | 65 | this->temp_.push_back(-this->idle_us_); 66 | this->call_listeners_dumpers_(); 67 | } 68 | } 69 | void RemoteReceiver::decode_rmt_(rmt_item32_t *item, size_t len) { 70 | bool prev_level = false; 71 | uint32_t prev_length = 0; 72 | this->temp_.clear(); 73 | int32_t multiplier = this->pin_->is_inverted() ? -1 : 1; 74 | size_t item_count = len / sizeof(rmt_item32_t); 75 | 76 | ESP_LOGVV(TAG, "START:"); 77 | for (size_t i = 0; i < item_count; i++) { 78 | if (item[i].level0) { 79 | ESP_LOGVV(TAG, "%u A: ON %uus (%u ticks)", i, this->to_microseconds_(item[i].duration0), item[i].duration0); 80 | } else { 81 | ESP_LOGVV(TAG, "%u A: OFF %uus (%u ticks)", i, this->to_microseconds_(item[i].duration0), item[i].duration0); 82 | } 83 | if (item[i].level1) { 84 | ESP_LOGVV(TAG, "%u B: ON %uus (%u ticks)", i, this->to_microseconds_(item[i].duration1), item[i].duration1); 85 | } else { 86 | ESP_LOGVV(TAG, "%u B: OFF %uus (%u ticks)", i, this->to_microseconds_(item[i].duration1), item[i].duration1); 87 | } 88 | } 89 | ESP_LOGVV(TAG, "\n"); 90 | 91 | this->temp_.reserve(item_count * 2); // each RMT item has 2 pulses 92 | for (size_t i = 0; i < item_count; i++) { 93 | if (item[i].duration0 == 0u) { 94 | // Do nothing 95 | } else if (bool(item[i].level0) == prev_level) { 96 | prev_length += item[i].duration0; 97 | } else { 98 | if (prev_length > 0) { 99 | if (prev_level) { 100 | this->temp_.push_back(this->to_microseconds_(prev_length) * multiplier); 101 | } else { 102 | this->temp_.push_back(-int32_t(this->to_microseconds_(prev_length)) * multiplier); 103 | } 104 | } 105 | prev_level = bool(item[i].level0); 106 | prev_length = item[i].duration0; 107 | } 108 | 109 | if (item[i].duration1 == 0u) { 110 | // Do nothing 111 | } else if (bool(item[i].level1) == prev_level) { 112 | prev_length += item[i].duration1; 113 | } else { 114 | if (prev_length > 0) { 115 | if (prev_level) { 116 | this->temp_.push_back(this->to_microseconds_(prev_length) * multiplier); 117 | } else { 118 | this->temp_.push_back(-int32_t(this->to_microseconds_(prev_length)) * multiplier); 119 | } 120 | } 121 | prev_level = bool(item[i].level1); 122 | prev_length = item[i].duration1; 123 | } 124 | } 125 | if (prev_length > 0) { 126 | if (prev_level) { 127 | this->temp_.push_back(this->to_microseconds_(prev_length) * multiplier); 128 | } else { 129 | this->temp_.push_back(-int32_t(this->to_microseconds_(prev_length)) * multiplier); 130 | } 131 | } 132 | } 133 | 134 | } // namespace rf_bridge_cc1101 135 | } // namespace esphome 136 | 137 | #endif 138 | -------------------------------------------------------------------------------- /components/rf_bridge_cc1101/remote_receiver_esp8266.cpp: -------------------------------------------------------------------------------- 1 | #include "remote_receiver.h" 2 | #include "esphome/core/hal.h" 3 | #include "esphome/core/log.h" 4 | #include "esphome/core/helpers.h" 5 | 6 | #ifdef USE_ESP8266 7 | 8 | namespace esphome { 9 | namespace rf_bridge_cc1101 { 10 | 11 | static const char *const TAG = "rf_bridge_cc1101.receiver.esp8266"; 12 | 13 | void IRAM_ATTR HOT RemoteReceiverStore::gpio_intr(RemoteReceiverStore *arg) { 14 | const uint32_t now = micros(); 15 | // If the lhs is 1 (rising edge) we should write to an uneven index and vice versa 16 | const uint32_t next = (arg->buffer_write_at + 1) % arg->buffer_size; 17 | const bool level = arg->pin.digital_read(); 18 | if (level != next % 2) 19 | return; 20 | 21 | // If next is buffer_read, we have hit an overflow 22 | if (next == arg->buffer_read_at) 23 | return; 24 | 25 | const uint32_t last_change = arg->buffer[arg->buffer_write_at]; 26 | const uint32_t time_since_change = now - last_change; 27 | if (time_since_change <= arg->filter_us) 28 | return; 29 | 30 | arg->buffer[arg->buffer_write_at = next] = now; 31 | } 32 | 33 | void RemoteReceiver::setup() { 34 | ESP_LOGCONFIG(TAG, "Setting up Remote Receiver..."); 35 | this->pin_->setup(); 36 | auto &s = this->store_; 37 | s.filter_us = this->filter_us_; 38 | s.pin = this->pin_->to_isr(); 39 | s.buffer_size = this->buffer_size_; 40 | 41 | this->high_freq_.start(); 42 | if (s.buffer_size % 2 != 0) { 43 | // Make sure divisible by two. This way, we know that every 0bxxx0 index is a space and every 0bxxx1 index is a mark 44 | s.buffer_size++; 45 | } 46 | 47 | s.buffer = new uint32_t[s.buffer_size]; 48 | void *buf = (void *) s.buffer; 49 | memset(buf, 0, s.buffer_size * sizeof(uint32_t)); 50 | 51 | // First index is a space. 52 | if (this->pin_->digital_read()) { 53 | s.buffer_write_at = s.buffer_read_at = 1; 54 | } else { 55 | s.buffer_write_at = s.buffer_read_at = 0; 56 | } 57 | } 58 | 59 | void RemoteReceiver::enable() { 60 | this->pin_->pin_mode(gpio::FLAG_INPUT); 61 | this->pin_->attach_interrupt(RemoteReceiverStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); 62 | } 63 | 64 | void RemoteReceiver::diable() { this->pin_->detach_interrupt(); } 65 | 66 | void RemoteReceiver::loop() { 67 | auto &s = this->store_; 68 | 69 | // copy write at to local variables, as it's volatile 70 | const uint32_t write_at = s.buffer_write_at; 71 | const uint32_t dist = (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size; 72 | // signals must at least one rising and one leading edge 73 | if (dist <= 1) 74 | return; 75 | const uint32_t now = micros(); 76 | if (now - s.buffer[write_at] < this->idle_us_) 77 | // The last change was fewer than the configured idle time ago. 78 | return; 79 | 80 | ESP_LOGVV(TAG, "read_at=%u write_at=%u dist=%u now=%u end=%u", s.buffer_read_at, write_at, dist, now, 81 | s.buffer[write_at]); 82 | 83 | // Skip first value, it's from the previous idle level 84 | s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; 85 | uint32_t prev = s.buffer_read_at; 86 | s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; 87 | const uint32_t reserve_size = 1 + (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size; 88 | this->temp_.clear(); 89 | this->temp_.reserve(reserve_size); 90 | int32_t multiplier = s.buffer_read_at % 2 == 0 ? 1 : -1; 91 | 92 | for (uint32_t i = 0; prev != write_at; i++) { 93 | int32_t delta = s.buffer[s.buffer_read_at] - s.buffer[prev]; 94 | if (uint32_t(delta) >= this->idle_us_) { 95 | // already found a space longer than idle. There must have been two pulses 96 | break; 97 | } 98 | 99 | ESP_LOGVV(TAG, " i=%u buffer[%u]=%u - buffer[%u]=%u -> %d", i, s.buffer_read_at, s.buffer[s.buffer_read_at], prev, 100 | s.buffer[prev], multiplier * delta); 101 | this->temp_.push_back(multiplier * delta); 102 | prev = s.buffer_read_at; 103 | s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; 104 | multiplier *= -1; 105 | } 106 | s.buffer_read_at = (s.buffer_size + s.buffer_read_at - 1) % s.buffer_size; 107 | this->temp_.push_back(this->idle_us_ * multiplier); 108 | 109 | this->call_listeners_dumpers_(); 110 | } 111 | 112 | } // namespace rf_bridge_cc1101 113 | } // namespace esphome 114 | 115 | #endif 116 | -------------------------------------------------------------------------------- /components/rf_bridge_cc1101/remote_transmitter.cpp: -------------------------------------------------------------------------------- 1 | #include "remote_transmitter.h" 2 | #include "esphome/core/log.h" 3 | #include "esphome/core/application.h" 4 | 5 | namespace esphome { 6 | namespace rf_bridge_cc1101 { 7 | 8 | static const char *const TAG = "rf_bridge_cc1101"; 9 | 10 | } // namespace rf_bridge_cc1101 11 | } // namespace esphome 12 | -------------------------------------------------------------------------------- /components/rf_bridge_cc1101/remote_transmitter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/remote_base/remote_base.h" 5 | 6 | namespace esphome { 7 | namespace rf_bridge_cc1101 { 8 | 9 | class RemoteTransmitter : public remote_base::RemoteTransmitterBase 10 | #ifdef USE_ESP32 11 | , 12 | public remote_base::RemoteRMTChannel 13 | #endif 14 | { 15 | public: 16 | explicit RemoteTransmitter(InternalGPIOPin *pin) : remote_base::RemoteTransmitterBase(pin) {} 17 | 18 | void setup(); 19 | 20 | void enable(); 21 | 22 | void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; } 23 | 24 | protected: 25 | void send_internal(uint32_t send_times, uint32_t send_wait) override; 26 | #ifdef USE_ESP8266 27 | void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period); 28 | 29 | void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); 30 | 31 | void space_(uint32_t usec); 32 | 33 | void await_target_time_(); 34 | uint32_t target_time_; 35 | #endif 36 | 37 | #ifdef USE_ESP32 38 | void configure_rmt_(); 39 | 40 | uint32_t current_carrier_frequency_{UINT32_MAX}; 41 | bool initialized_{false}; 42 | std::vector rmt_temp_; 43 | esp_err_t error_code_{ESP_OK}; 44 | bool inverted_{false}; 45 | #endif 46 | uint8_t carrier_duty_percent_{50}; 47 | }; 48 | 49 | } // namespace rf_bridge_cc1101 50 | } // namespace esphome 51 | -------------------------------------------------------------------------------- /components/rf_bridge_cc1101/remote_transmitter_esp32.cpp: -------------------------------------------------------------------------------- 1 | #include "remote_transmitter.h" 2 | #include "esphome/core/log.h" 3 | #include "esphome/core/application.h" 4 | 5 | #ifdef USE_ESP32 6 | 7 | namespace esphome { 8 | namespace rf_bridge_cc1101 { 9 | 10 | static const char *const TAG = "rf_bridge_cc1101.transmitter.esp32"; 11 | 12 | void RemoteTransmitter::enable() {} 13 | 14 | void RemoteTransmitter::setup() { this->configure_rmt_(); } 15 | 16 | void RemoteTransmitter::configure_rmt_() { 17 | rmt_config_t c{}; 18 | 19 | this->config_rmt(c); 20 | c.rmt_mode = RMT_MODE_TX; 21 | c.gpio_num = gpio_num_t(this->pin_->get_pin()); 22 | c.tx_config.loop_en = false; 23 | 24 | if (this->current_carrier_frequency_ == 0 || this->carrier_duty_percent_ == 100) { 25 | c.tx_config.carrier_en = false; 26 | } else { 27 | c.tx_config.carrier_en = true; 28 | c.tx_config.carrier_freq_hz = this->current_carrier_frequency_; 29 | c.tx_config.carrier_duty_percent = this->carrier_duty_percent_; 30 | } 31 | 32 | c.tx_config.idle_output_en = true; 33 | if (!this->pin_->is_inverted()) { 34 | c.tx_config.carrier_level = RMT_CARRIER_LEVEL_HIGH; 35 | c.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; 36 | } else { 37 | c.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; 38 | c.tx_config.idle_level = RMT_IDLE_LEVEL_HIGH; 39 | this->inverted_ = true; 40 | } 41 | 42 | esp_err_t error = rmt_config(&c); 43 | if (error != ESP_OK) { 44 | this->error_code_ = error; 45 | this->mark_failed(); 46 | return; 47 | } 48 | 49 | if (!this->initialized_) { 50 | error = rmt_driver_install(this->channel_, 0, 0); 51 | if (error != ESP_OK) { 52 | this->error_code_ = error; 53 | this->mark_failed(); 54 | return; 55 | } 56 | this->initialized_ = true; 57 | } 58 | } 59 | 60 | void RemoteTransmitter::send_internal(uint32_t send_times, uint32_t send_wait) { 61 | if (this->is_failed()) 62 | return; 63 | 64 | if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) { 65 | this->current_carrier_frequency_ = this->temp_.get_carrier_frequency(); 66 | this->configure_rmt_(); 67 | } 68 | 69 | this->rmt_temp_.clear(); 70 | this->rmt_temp_.reserve((this->temp_.get_data().size() + 1) / 2); 71 | uint32_t rmt_i = 0; 72 | rmt_item32_t rmt_item; 73 | 74 | for (int32_t val : this->temp_.get_data()) { 75 | bool level = val >= 0; 76 | if (!level) 77 | val = -val; 78 | val = this->from_microseconds_(static_cast(val)); 79 | 80 | do { 81 | int32_t item = std::min(val, int32_t(32767)); 82 | val -= item; 83 | 84 | if (rmt_i % 2 == 0) { 85 | rmt_item.level0 = static_cast(level ^ this->inverted_); 86 | rmt_item.duration0 = static_cast(item); 87 | } else { 88 | rmt_item.level1 = static_cast(level ^ this->inverted_); 89 | rmt_item.duration1 = static_cast(item); 90 | this->rmt_temp_.push_back(rmt_item); 91 | } 92 | rmt_i++; 93 | } while (val != 0); 94 | } 95 | 96 | if (rmt_i % 2 == 1) { 97 | rmt_item.level1 = 0; 98 | rmt_item.duration1 = 0; 99 | this->rmt_temp_.push_back(rmt_item); 100 | } 101 | 102 | for (uint32_t i = 0; i < send_times; i++) { 103 | esp_err_t error = rmt_write_items(this->channel_, this->rmt_temp_.data(), this->rmt_temp_.size(), true); 104 | if (error != ESP_OK) { 105 | ESP_LOGW(TAG, "rmt_write_items failed: %s", esp_err_to_name(error)); 106 | this->status_set_warning(); 107 | } else { 108 | this->status_clear_warning(); 109 | } 110 | if (i + 1 < send_times) 111 | delayMicroseconds(send_wait); 112 | } 113 | } 114 | 115 | } // namespace rf_bridge_cc1101 116 | } // namespace esphome 117 | 118 | #endif 119 | -------------------------------------------------------------------------------- /components/rf_bridge_cc1101/remote_transmitter_esp8266.cpp: -------------------------------------------------------------------------------- 1 | #include "remote_transmitter.h" 2 | #include "esphome/core/log.h" 3 | #include "esphome/core/application.h" 4 | 5 | #ifdef USE_ESP8266 6 | 7 | namespace esphome { 8 | namespace rf_bridge_cc1101 { 9 | 10 | static const char *const TAG = "rf_bridge_cc1101.transmitter.esp8266"; 11 | 12 | void RemoteTransmitter::setup() { 13 | this->pin_->setup(); 14 | this->pin_->digital_write(false); 15 | } 16 | 17 | void RemoteTransmitter::enable() { 18 | this->pin_->pin_mode(gpio::FLAG_OUTPUT); 19 | this->pin_->digital_write(false); 20 | } 21 | 22 | void RemoteTransmitter::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, 23 | uint32_t *off_time_period) { 24 | if (carrier_frequency == 0) { 25 | *on_time_period = 0; 26 | *off_time_period = 0; 27 | return; 28 | } 29 | uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency; // round(1000000/freq) 30 | period = std::max(uint32_t(1), period); 31 | *on_time_period = (period * this->carrier_duty_percent_) / 100; 32 | *off_time_period = period - *on_time_period; 33 | } 34 | 35 | void RemoteTransmitter::await_target_time_() { 36 | const uint32_t current_time = micros(); 37 | if (this->target_time_ == 0) 38 | this->target_time_ = current_time; 39 | else if (this->target_time_ > current_time) 40 | delayMicroseconds(this->target_time_ - current_time); 41 | } 42 | 43 | void RemoteTransmitter::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { 44 | this->await_target_time_(); 45 | this->pin_->digital_write(true); 46 | 47 | const uint32_t target = this->target_time_ + usec; 48 | if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { 49 | while (true) { // Modulate with carrier frequency 50 | this->target_time_ += on_time; 51 | if (this->target_time_ >= target) 52 | break; 53 | this->await_target_time_(); 54 | this->pin_->digital_write(false); 55 | 56 | this->target_time_ += off_time; 57 | if (this->target_time_ >= target) 58 | break; 59 | this->await_target_time_(); 60 | this->pin_->digital_write(true); 61 | } 62 | } 63 | this->target_time_ = target; 64 | } 65 | 66 | void RemoteTransmitter::space_(uint32_t usec) { 67 | this->await_target_time_(); 68 | this->pin_->digital_write(false); 69 | this->target_time_ += usec; 70 | } 71 | 72 | void RemoteTransmitter::send_internal(uint32_t send_times, uint32_t send_wait) { 73 | ESP_LOGD(TAG, "Sending remote code..."); 74 | uint32_t on_time, off_time; 75 | this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); 76 | this->target_time_ = 0; 77 | for (uint32_t i = 0; i < send_times; i++) { 78 | for (int32_t item : this->temp_.get_data()) { 79 | if (item > 0) { 80 | const auto length = uint32_t(item); 81 | this->mark_(on_time, off_time, length); 82 | } else { 83 | const auto length = uint32_t(-item); 84 | this->space_(length); 85 | } 86 | App.feed_wdt(); 87 | } 88 | this->await_target_time_(); // wait for duration of last pulse 89 | this->pin_->digital_write(false); 90 | 91 | if (i + 1 < send_times) 92 | this->target_time_ += send_wait; 93 | } 94 | } 95 | 96 | } // namespace rf_bridge_cc1101 97 | } // namespace esphome 98 | 99 | #endif 100 | -------------------------------------------------------------------------------- /components/rf_bridge_cc1101/rf_bridge_cc1101.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "esphome/core/component.h" 6 | #include "esphome/core/hal.h" 7 | #include "esphome/components/spi/spi.h" 8 | #include "esphome/core/automation.h" 9 | 10 | #include "remote_transmitter.h" 11 | #include "remote_receiver.h" 12 | 13 | namespace esphome { 14 | namespace rf_bridge_cc1101 { 15 | 16 | enum rf_bridge_mode_type { MODE_RECEIVER, MODE_TRANSMITTER }; 17 | 18 | class RFBridgeComponent : public Component, 19 | public spi::SPIDevice { 21 | public: 22 | RFBridgeComponent(InternalGPIOPin *pin); 23 | RFBridgeComponent() {} 24 | void setup() override; 25 | void loop() override; 26 | void dump_config() override; 27 | float get_setup_priority() const override { return setup_priority::DATA; } 28 | 29 | void receiver_mode(); 30 | void transmitter_mode(); 31 | void set_mode(uint8_t mode) { mode_ = mode; } 32 | void set_frequency_mhz(float mhz) { mhz_ = mhz; } 33 | 34 | void transmit(const std::string &code, uint8_t protocol, uint32_t send_times, uint32_t send_wait); 35 | 36 | void register_listener(remote_base::RemoteReceiverListener *listener); 37 | void register_dumper(remote_base::RemoteReceiverDumperBase *dumper); 38 | 39 | void set_buffer_size(uint32_t buffer_size) { this->receiver_->set_buffer_size(buffer_size); } 40 | void set_filter_us(uint8_t filter_us) { this->receiver_->set_filter_us(filter_us); } 41 | void set_idle_us(uint32_t idle_us) { this->receiver_->set_idle_us(idle_us); } 42 | void set_tolerance(uint8_t tolerance) { this->receiver_->set_tolerance(tolerance); } 43 | 44 | protected: 45 | void cc1101_setup(); 46 | void write_reg(uint8_t addr, uint8_t value); 47 | void write_burst_reg(uint8_t addr, uint8_t *value, size_t lenght); 48 | void strobe(uint8_t strobe); 49 | uint8_t read_reg(uint8_t addr); 50 | void set_mhz(float mhz); 51 | void set_pa(int pa); 52 | void calibrate(); 53 | void set_io_mode(); 54 | void set_modulation(uint8_t modulation); 55 | void split_MDMCFG2(); 56 | void reset(); 57 | 58 | InternalGPIOPin *pin_; 59 | uint8_t mode_{MODE_RECEIVER}; 60 | 61 | float mhz_{433.92}; 62 | int last_pa_, pa_{12}; 63 | uint8_t modulation_{2}; 64 | uint8_t m2DCOFF, m2MANCH, m2MODFM, m2SYNCM, frend0, m4RxBw; 65 | uint8_t clb1[2]{24, 28}; 66 | uint8_t clb2[2]{31, 38}; 67 | uint8_t clb3[2]{65, 76}; 68 | uint8_t clb4[2]{77, 79}; 69 | uint8_t PA_TABLE[8]{0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 70 | 71 | RemoteTransmitter *transmitter_{NULL}; 72 | RemoteReceiver *receiver_{NULL}; 73 | }; 74 | 75 | template class RFBridgeReceiverModeAction : public Action { 76 | public: 77 | RFBridgeReceiverModeAction(RFBridgeComponent *parent) : parent_(parent) {} 78 | 79 | void play(Ts... x) { this->parent_->receiver_mode(); } 80 | 81 | protected: 82 | RFBridgeComponent *parent_; 83 | }; 84 | 85 | template class RFBridgeTransmitterModeAction : public Action { 86 | public: 87 | RFBridgeTransmitterModeAction(RFBridgeComponent *parent) : parent_(parent) {} 88 | 89 | void play(Ts... x) { this->parent_->transmitter_mode(); } 90 | 91 | protected: 92 | RFBridgeComponent *parent_; 93 | }; 94 | 95 | template class RFBridgeTransmitAction : public Action { 96 | public: 97 | RFBridgeTransmitAction(RFBridgeComponent *parent) : parent_(parent) {} 98 | TEMPLATABLE_VALUE(std::string, code) 99 | TEMPLATABLE_VALUE(uint8_t, protocol) 100 | TEMPLATABLE_VALUE(uint32_t, send_times) 101 | TEMPLATABLE_VALUE(uint32_t, send_wait) 102 | 103 | void play(Ts... x) { 104 | this->parent_->transmit(code_.value(x...), protocol_.value(x...), send_times_.value_or(x..., 1), 105 | send_wait_.value_or(x..., 0)); 106 | } 107 | 108 | protected: 109 | RFBridgeComponent *parent_; 110 | }; 111 | 112 | } // namespace rf_bridge_cc1101 113 | } // namespace esphome 114 | -------------------------------------------------------------------------------- /components/ssw_tds/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanh7/esphome-custom-components/78a9afe3c2164fee312d50f547a7c0cea73e6d86/components/ssw_tds/__init__.py -------------------------------------------------------------------------------- /components/ssw_tds/sensor.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import uart, sensor 4 | from esphome.const import ( 5 | CONF_ID, 6 | CONF_TEMPERATURE, 7 | UNIT_PARTS_PER_MILLION, 8 | ICON_WATER_PERCENT, 9 | ICON_THERMOMETER, 10 | DEVICE_CLASS_EMPTY, 11 | DEVICE_CLASS_TEMPERATURE, 12 | STATE_CLASS_MEASUREMENT, 13 | UNIT_CELSIUS, 14 | ) 15 | 16 | CONF_SOURCE_TDS = "source_tds" 17 | CONF_CLEAN_TDS = "clean_tds" 18 | 19 | DEPENDENCIES = ["uart"] 20 | 21 | ssw_tds_ns = cg.esphome_ns.namespace("ssw_tds") 22 | SSWTDSComponent = ssw_tds_ns.class_( 23 | "SSWTDSComponent", cg.PollingComponent, uart.UARTDevice 24 | ) 25 | 26 | 27 | CONFIG_SCHEMA = cv.All( 28 | cv.Schema( 29 | { 30 | cv.GenerateID(): cv.declare_id(SSWTDSComponent), 31 | cv.Optional(CONF_SOURCE_TDS): sensor.sensor_schema( 32 | unit_of_measurement=UNIT_PARTS_PER_MILLION, 33 | icon=ICON_WATER_PERCENT, 34 | accuracy_decimals=0, 35 | device_class=DEVICE_CLASS_EMPTY, 36 | state_class=STATE_CLASS_MEASUREMENT 37 | ), 38 | cv.Optional(CONF_CLEAN_TDS): sensor.sensor_schema( 39 | unit_of_measurement=UNIT_PARTS_PER_MILLION, 40 | icon=ICON_WATER_PERCENT, 41 | accuracy_decimals=0, 42 | device_class=DEVICE_CLASS_EMPTY, 43 | state_class=STATE_CLASS_MEASUREMENT 44 | ), 45 | cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( 46 | unit_of_measurement=UNIT_CELSIUS, 47 | icon=ICON_THERMOMETER, 48 | accuracy_decimals=0, 49 | device_class=DEVICE_CLASS_TEMPERATURE, 50 | state_class=STATE_CLASS_MEASUREMENT 51 | ), 52 | } 53 | ) 54 | .extend(cv.polling_component_schema("5s")) 55 | .extend(uart.UART_DEVICE_SCHEMA) 56 | ) 57 | 58 | 59 | async def to_code(config): 60 | var = cg.new_Pvariable(config[CONF_ID]) 61 | await cg.register_component(var, config) 62 | await uart.register_uart_device(var, config) 63 | 64 | if CONF_SOURCE_TDS in config: 65 | sens = await sensor.new_sensor(config[CONF_SOURCE_TDS]) 66 | cg.add(var.set_source_tds_sensor(sens)) 67 | if CONF_CLEAN_TDS in config: 68 | sens = await sensor.new_sensor(config[CONF_CLEAN_TDS]) 69 | cg.add(var.set_clean_tds_sensor(sens)) 70 | if CONF_TEMPERATURE in config: 71 | sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) 72 | cg.add(var.set_temperature_sensor(sens)) -------------------------------------------------------------------------------- /components/ssw_tds/ssw_tds.cpp: -------------------------------------------------------------------------------- 1 | #include "ssw_tds.h" 2 | #include "esphome/core/log.h" 3 | #include 4 | 5 | namespace esphome { 6 | namespace ssw_tds { 7 | 8 | static const char *const TAG = "ssw_tds"; 9 | 10 | void SSWTDSComponent::dump_config() { 11 | ESP_LOGCONFIG(TAG, "SSW_TDS:"); 12 | this->check_uart_settings(9600); 13 | LOG_SENSOR(" ", "Source TDS", this->source_tds_sensor_); 14 | LOG_SENSOR(" ", "Clean TDS", this->clean_tds_sensor_); 15 | LOG_SENSOR(" ", "Temperature TDS", this->temperature_sensor_); 16 | } 17 | 18 | void SSWTDSComponent::update() { 19 | this->write_array(URAT_QUERY_COMMAND, sizeof(URAT_QUERY_COMMAND)); 20 | this->flush(); 21 | } 22 | 23 | void SSWTDSComponent::loop() { 24 | while (this->available()) { 25 | uint8_t byte; 26 | this->read_byte(&byte); 27 | if (this->parse_(byte)) { 28 | ESP_LOGVV(TAG, "Parsed: 0x%02X", byte); 29 | } else { 30 | this->rx_buffer_.clear(); 31 | } 32 | } 33 | } 34 | 35 | int SSWTDSComponent::parse_(uint8_t byte) { 36 | size_t at = this->rx_buffer_.size(); 37 | this->rx_buffer_.push_back(byte); 38 | const uint8_t *raw = &this->rx_buffer_[0]; 39 | 40 | ESP_LOGVV(TAG, "Processing byte: 0x%02X", byte); 41 | 42 | // Byte 0: Start 43 | if (at == 0) 44 | return byte == URAT_FEEDBACK_START; 45 | 46 | // Byte 1: Action 47 | if (at == 1) 48 | return byte == URAT_QUERY_COMMAND[1]; 49 | 50 | if (at < 12) 51 | return true; 52 | 53 | if (!this->checksum_()) { 54 | ESP_LOGE(TAG, "checksum failed!"); 55 | return false; 56 | } 57 | 58 | int source_tds = raw[2] << 24 | raw[3] << 16 | raw[4] << 8 | raw[5]; 59 | int clean_tds = raw[6] << 24 | raw[7] << 16 | raw[8] << 8 | raw[9]; 60 | int temperature = raw[10]; 61 | 62 | ESP_LOGD(TAG, "got source tds=%d, clean tds=%d, temperature=%d", source_tds, clean_tds, temperature); 63 | 64 | if (this->source_tds_sensor_ != nullptr) { 65 | this->source_tds_sensor_->publish_state(source_tds); 66 | } 67 | 68 | if (this->clean_tds_sensor_ != nullptr) { 69 | this->clean_tds_sensor_->publish_state(clean_tds); 70 | } 71 | 72 | if (this->temperature_sensor_ != nullptr) { 73 | this->temperature_sensor_->publish_state(temperature); 74 | } 75 | 76 | return false; 77 | } 78 | 79 | int SSWTDSComponent::checksum_() { 80 | if (this->rx_buffer_.size() < 13) 81 | return false; 82 | const uint8_t *raw = &this->rx_buffer_[0]; 83 | uint16_t calc = 0; 84 | for (int i = 0; i < 11; i++) { 85 | calc += raw[i]; 86 | } 87 | return calc == (raw[11] << 8 | raw[12]); 88 | } 89 | 90 | } // namespace ssw_tds 91 | } // namespace esphome 92 | -------------------------------------------------------------------------------- /components/ssw_tds/ssw_tds.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "esphome/core/component.h" 6 | #include "esphome/components/uart/uart.h" 7 | #include "esphome/components/sensor/sensor.h" 8 | 9 | namespace esphome { 10 | namespace ssw_tds { 11 | 12 | static const uint8_t URAT_QUERY_COMMAND[] ={0xff, 0x06, 0x01, 0x05}; 13 | static const uint8_t URAT_FEEDBACK_START = 0xfe; 14 | 15 | 16 | class SSWTDSComponent : public uart::UARTDevice, public PollingComponent { 17 | public: 18 | void dump_config() override; 19 | void update() override; 20 | void loop() override; 21 | float get_setup_priority() const override { return setup_priority::DATA; } 22 | 23 | void set_source_tds_sensor(sensor::Sensor *source_tds_sensor) { source_tds_sensor_ = source_tds_sensor; } 24 | void set_clean_tds_sensor(sensor::Sensor *clean_tds_sensor) { clean_tds_sensor_ = clean_tds_sensor; } 25 | void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } 26 | 27 | protected: 28 | int parse_(uint8_t byte); 29 | int checksum_(); 30 | 31 | std::vector rx_buffer_; 32 | uint32_t last_byte_{0}; 33 | 34 | sensor::Sensor *source_tds_sensor_{nullptr}; 35 | sensor::Sensor *clean_tds_sensor_{nullptr}; 36 | sensor::Sensor *temperature_sensor_{nullptr}; 37 | }; 38 | 39 | 40 | } // namespace ssw_tds 41 | } // namespace esphome 42 | -------------------------------------------------------------------------------- /components/telnet/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import uart 4 | from esphome.const import ( 5 | CONF_ID, 6 | CONF_PORT 7 | ) 8 | 9 | DEPENDENCIES = ["uart", "network"] 10 | 11 | telnet_ns = cg.esphome_ns.namespace("telnet") 12 | TelnetComponent = telnet_ns.class_( 13 | "TelnetComponent", cg.Component, uart.UARTDevice 14 | ) 15 | 16 | CONFIG_SCHEMA = cv.All( 17 | cv.Schema( 18 | { 19 | cv.GenerateID(): cv.declare_id(TelnetComponent), 20 | cv.Optional(CONF_PORT, default=23): cv.port, 21 | } 22 | ) 23 | .extend(cv.COMPONENT_SCHEMA) 24 | .extend(uart.UART_DEVICE_SCHEMA) 25 | ) 26 | 27 | 28 | async def to_code(config): 29 | var = cg.new_Pvariable(config[CONF_ID]) 30 | await cg.register_component(var, config) 31 | await uart.register_uart_device(var, config) 32 | 33 | cg.add(var.set_port(config[CONF_PORT])) 34 | -------------------------------------------------------------------------------- /components/telnet/telnet.cpp: -------------------------------------------------------------------------------- 1 | #include "telnet.h" 2 | #include "esphome/core/log.h" 3 | #include 4 | 5 | namespace esphome { 6 | namespace telnet { 7 | 8 | static const char *const TAG = "telnet"; 9 | 10 | void TelnetComponent::setup() { 11 | server_ = socket::socket_ip(SOCK_STREAM, 0); 12 | if (server_ == nullptr) { 13 | ESP_LOGW(TAG, "Could not create socket."); 14 | this->mark_failed(); 15 | return; 16 | } 17 | int enable = 1; 18 | int err = server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); 19 | if (err != 0) { 20 | ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); 21 | // we can still continue 22 | } 23 | err = server_->setblocking(false); 24 | if (err != 0) { 25 | ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); 26 | this->mark_failed(); 27 | return; 28 | } 29 | 30 | struct sockaddr_storage server; 31 | 32 | socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), htons(this->port_)); 33 | if (sl == 0) { 34 | ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); 35 | this->mark_failed(); 36 | return; 37 | } 38 | 39 | err = server_->bind((struct sockaddr *) &server, sizeof(server)); 40 | if (err != 0) { 41 | ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); 42 | this->mark_failed(); 43 | return; 44 | } 45 | 46 | err = server_->listen(4); 47 | if (err != 0) { 48 | ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); 49 | this->mark_failed(); 50 | return; 51 | } 52 | } 53 | 54 | void TelnetComponent::dump_config() { 55 | ESP_LOGCONFIG(TAG, "Telnet:"); 56 | ESP_LOGCONFIG(TAG, " Port: %d", this->port_); 57 | if (client_ != nullptr) { 58 | ESP_LOGCONFIG(TAG, " Remote: %s", this->client_->getpeername().c_str()); 59 | } 60 | } 61 | 62 | void TelnetComponent::loop() { 63 | if (client_ == nullptr) { 64 | struct sockaddr_storage source_addr; 65 | socklen_t addr_len = sizeof(source_addr); 66 | client_ = server_->accept((struct sockaddr *) &source_addr, &addr_len); 67 | if (client_ == nullptr) 68 | return; 69 | ESP_LOGI(TAG, "Accept connection from %s...", this->client_->getpeername().c_str()); 70 | 71 | int err = client_->setblocking(false); 72 | if (err != 0) { 73 | ESP_LOGW(TAG, "Setting nonblocking failed with errno %d", errno); 74 | } 75 | 76 | int enable = 1; 77 | err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); 78 | if (err != 0) { 79 | ESP_LOGW(TAG, "Socket could not enable tcp nodelay, errno: %d", errno); 80 | } 81 | } 82 | 83 | while (true) { 84 | int write = this->available(); 85 | if (write > 0) { 86 | write = std::min(write, (int) sizeof(buf_)); 87 | this->read_array(buf_, write); 88 | client_->write(buf_, write); 89 | } 90 | 91 | ssize_t read = this->client_->read(buf_, sizeof(buf_)); 92 | if (read == -1) { 93 | if (errno != EAGAIN && errno != EWOULDBLOCK) { 94 | ESP_LOGW(TAG, "Error receiving data, errno: %d", errno); 95 | this->client_->close(); 96 | this->client_ = nullptr; 97 | return; 98 | } 99 | } else if (read == 0) { 100 | ESP_LOGI(TAG, "Remote end closed connection"); 101 | this->client_->close(); 102 | this->client_ = nullptr; 103 | return; 104 | } else { 105 | this->write_array(buf_, read); 106 | this->flush(); 107 | } 108 | 109 | if (write < 1 && read < 1) { 110 | return; 111 | } 112 | } 113 | } 114 | 115 | } // namespace telnet 116 | } // namespace esphome 117 | -------------------------------------------------------------------------------- /components/telnet/telnet.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "esphome/core/component.h" 6 | #include "esphome/components/uart/uart.h" 7 | #include "esphome/components/socket/socket.h" 8 | 9 | namespace esphome { 10 | namespace telnet { 11 | 12 | class TelnetComponent : public uart::UARTDevice, public Component { 13 | public: 14 | void setup() override; 15 | void dump_config() override; 16 | void loop() override; 17 | float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } 18 | void set_port(uint16_t port) { this->port_ = port; } 19 | 20 | protected: 21 | uint16_t port_{23}; 22 | std::unique_ptr server_; 23 | std::unique_ptr client_; 24 | 25 | uint8_t buf_[1024]; 26 | }; 27 | 28 | } // namespace telnet 29 | } // namespace esphome 30 | -------------------------------------------------------------------------------- /components/xiaomi_m1st500/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanh7/esphome-custom-components/78a9afe3c2164fee312d50f547a7c0cea73e6d86/components/xiaomi_m1st500/__init__.py -------------------------------------------------------------------------------- /components/xiaomi_m1st500/sensor.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import sensor, esp32_ble_tracker 4 | from esphome.const import ( 5 | CONF_BATTERY_LEVEL, 6 | CONF_MAC_ADDRESS, 7 | STATE_CLASS_MEASUREMENT, 8 | UNIT_EMPTY, 9 | ICON_EMPTY, 10 | ICON_ACCOUNT_CHECK, 11 | UNIT_PERCENT, 12 | DEVICE_CLASS_EMPTY, 13 | DEVICE_CLASS_BATTERY, 14 | CONF_ID, 15 | ) 16 | 17 | CONF_SCORE="score" 18 | 19 | DEPENDENCIES = ["esp32_ble_tracker"] 20 | 21 | xiaomi_m1st500_ns = cg.esphome_ns.namespace("xiaomi_m1st500") 22 | XiaomiM1ST500 = xiaomi_m1st500_ns.class_( 23 | "XiaomiM1ST500", esp32_ble_tracker.ESPBTDeviceListener, cg.Component 24 | ) 25 | 26 | CONFIG_SCHEMA = ( 27 | cv.Schema( 28 | { 29 | cv.GenerateID(): cv.declare_id(XiaomiM1ST500), 30 | cv.Required(CONF_MAC_ADDRESS): cv.mac_address, 31 | cv.Optional(CONF_SCORE): sensor.sensor_schema( 32 | unit_of_measurement=UNIT_EMPTY, 33 | icon=ICON_ACCOUNT_CHECK, 34 | accuracy_decimals=0, 35 | device_class=DEVICE_CLASS_EMPTY, 36 | state_class=STATE_CLASS_MEASUREMENT, 37 | ), 38 | cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( 39 | unit_of_measurement=UNIT_PERCENT, 40 | icon=ICON_EMPTY, 41 | accuracy_decimals=0, 42 | device_class=DEVICE_CLASS_BATTERY, 43 | state_class=STATE_CLASS_MEASUREMENT, 44 | ), 45 | } 46 | ) 47 | .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) 48 | .extend(cv.COMPONENT_SCHEMA) 49 | ) 50 | 51 | 52 | async def to_code(config): 53 | var = cg.new_Pvariable(config[CONF_ID]) 54 | await cg.register_component(var, config) 55 | await esp32_ble_tracker.register_ble_device(var, config) 56 | 57 | cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) 58 | 59 | if CONF_SCORE in config: 60 | sens = await sensor.new_sensor(config[CONF_SCORE]) 61 | cg.add(var.set_score(sens)) 62 | if CONF_BATTERY_LEVEL in config: 63 | sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) 64 | cg.add(var.set_battery_level(sens)) 65 | -------------------------------------------------------------------------------- /components/xiaomi_m1st500/xiaomi_m1st500.cpp: -------------------------------------------------------------------------------- 1 | #include "xiaomi_m1st500.h" 2 | #include "esphome/core/log.h" 3 | 4 | #ifdef USE_ESP32 5 | 6 | namespace esphome { 7 | namespace xiaomi_m1st500 { 8 | 9 | static const char *const TAG = "xiaomi_m1st500"; 10 | 11 | void XiaomiM1ST500::dump_config() { 12 | ESP_LOGCONFIG(TAG, "Xiaomi M1ST500"); 13 | LOG_SENSOR(" ", "Score", this->score_); 14 | LOG_SENSOR(" ", "Battery Level", this->battery_level_); 15 | } 16 | 17 | bool XiaomiM1ST500::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { 18 | if (device.address_uint64() != this->address_) { 19 | ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); 20 | return false; 21 | } 22 | ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); 23 | 24 | bool success = false; 25 | for (auto &service_data : device.get_service_datas()) { 26 | auto res = xiaomi_toothbrush_ble::parse_xiaomi_header(service_data); 27 | if (!res.has_value()) { 28 | continue; 29 | } 30 | if (res->is_duplicate) { 31 | continue; 32 | } 33 | if (res->has_encryption) { 34 | ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); 35 | continue; 36 | } 37 | if (!(xiaomi_toothbrush_ble::parse_xiaomi_message(service_data.data, *res))) { 38 | continue; 39 | } 40 | if (!(xiaomi_toothbrush_ble::report_xiaomi_results(res, device.address_str()))) { 41 | continue; 42 | } 43 | if (res->score.has_value() && this->score_ != nullptr) 44 | this->score_->publish_state(*res->score); 45 | if (res->battery_level.has_value() && this->battery_level_ != nullptr) 46 | this->battery_level_->publish_state(*res->battery_level); 47 | success = true; 48 | } 49 | 50 | if (!success) { 51 | return false; 52 | } 53 | 54 | return true; 55 | } 56 | 57 | } // namespace xiaomi_m1st500 58 | } // namespace esphome 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /components/xiaomi_m1st500/xiaomi_m1st500.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/sensor/sensor.h" 5 | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" 6 | #include "xiaomi_toothbrush_ble.h" 7 | 8 | #ifdef USE_ESP32 9 | 10 | namespace esphome { 11 | namespace xiaomi_m1st500 { 12 | 13 | class XiaomiM1ST500 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { 14 | public: 15 | void set_address(uint64_t address) { address_ = address; } 16 | 17 | bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; 18 | 19 | void dump_config() override; 20 | float get_setup_priority() const override { return setup_priority::DATA; } 21 | void set_score(sensor::Sensor *score) { score_ = score; } 22 | void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } 23 | 24 | protected: 25 | uint64_t address_; 26 | sensor::Sensor *score_{nullptr}; 27 | sensor::Sensor *battery_level_{nullptr}; 28 | }; 29 | 30 | } // namespace xiaomi_m1st500 31 | } // namespace esphome 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /components/xiaomi_m1st500/xiaomi_toothbrush_ble.cpp: -------------------------------------------------------------------------------- 1 | #include "xiaomi_toothbrush_ble.h" 2 | #include "esphome/core/log.h" 3 | #include "esphome/core/helpers.h" 4 | 5 | #ifdef USE_ESP32 6 | 7 | #include 8 | 9 | namespace esphome { 10 | namespace xiaomi_toothbrush_ble { 11 | 12 | static const char *const TAG = "xiaomi_toothbrush_ble"; 13 | 14 | bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) { 15 | // battery, 1 byte, 8-bit unsigned integer, 1 % 16 | if ((value_type == 0x0A10) && (value_length == 1)) { 17 | result.battery_level = data[0]; 18 | } else if ((value_type == 0x1000) && (value_length == 2) && (data[0] == 0x01)) { 19 | result.score = data[1]; 20 | } else { 21 | return false; 22 | } 23 | 24 | return true; 25 | } 26 | 27 | bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result) { 28 | result.has_encryption = (message[0] & 0x08) ? true : false; // update encryption status 29 | if (result.has_encryption) { 30 | ESP_LOGVV(TAG, "parse_xiaomi_message(): payload is encrypted, stop reading message."); 31 | return false; 32 | } 33 | 34 | // Data point specs 35 | // Byte 0: type 36 | // Byte 1: fixed 0x10 or 0x00 37 | // Byte 2: length 38 | // Byte 3..3+len-1: data point value 39 | 40 | const uint8_t *payload = message.data() + result.raw_offset; 41 | uint8_t payload_length = message.size() - result.raw_offset; 42 | uint8_t payload_offset = 0; 43 | bool success = false; 44 | 45 | if (payload_length < 4) { 46 | ESP_LOGVV(TAG, "parse_xiaomi_message(): payload has wrong size (%d)!", payload_length); 47 | return false; 48 | } 49 | 50 | while (payload_length > 3) { 51 | const uint8_t value_length = payload[payload_offset + 2]; 52 | if ((value_length < 1) || (value_length > 4) || (payload_length < (3 + value_length))) { 53 | ESP_LOGVV(TAG, "parse_xiaomi_message(): value has wrong size (%d)!", value_length); 54 | break; 55 | } 56 | 57 | const uint16_t value_type = payload[payload_offset + 0] << 8 | payload[payload_offset + 1]; 58 | const uint8_t *data = &payload[payload_offset + 3]; 59 | 60 | if (parse_xiaomi_value(value_type, data, value_length, result)) 61 | success = true; 62 | 63 | payload_length -= 3 + value_length; 64 | payload_offset += 3 + value_length; 65 | } 66 | 67 | return success; 68 | } 69 | 70 | optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data) { 71 | XiaomiParseResult result; 72 | if (!service_data.uuid.contains(0x95, 0xFE)) { 73 | ESP_LOGVV(TAG, "parse_xiaomi_header(): no service data UUID magic bytes."); 74 | return {}; 75 | } 76 | 77 | auto raw = service_data.data; 78 | result.has_data = (raw[0] & 0x40) ? true : false; 79 | result.has_capability = (raw[0] & 0x20) ? true : false; 80 | result.has_encryption = (raw[0] & 0x08) ? true : false; 81 | 82 | if (!result.has_data) { 83 | ESP_LOGVV(TAG, "parse_xiaomi_header(): service data has no DATA flag."); 84 | return {}; 85 | } 86 | 87 | static uint8_t last_frame_count = 0; 88 | if (last_frame_count == raw[4]) { 89 | ESP_LOGVV(TAG, "parse_xiaomi_header(): duplicate data packet received (%d).", static_cast(last_frame_count)); 90 | result.is_duplicate = true; 91 | return {}; 92 | } 93 | last_frame_count = raw[4]; 94 | result.is_duplicate = false; 95 | result.raw_offset = result.has_capability ? 12 : 11; 96 | 97 | if ((raw[2] == 0x89) && (raw[3] == 0x04)) { // MiFlora 98 | result.type = XiaomiParseResult::TYPE_M1ST500; 99 | result.name = "M1S-T500"; 100 | } else { 101 | ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes."); 102 | return {}; 103 | } 104 | 105 | return result; 106 | } 107 | 108 | bool report_xiaomi_results(const optional &result, const std::string &address) { 109 | if (!result.has_value()) { 110 | ESP_LOGVV(TAG, "report_xiaomi_results(): no results available."); 111 | return false; 112 | } 113 | 114 | ESP_LOGD(TAG, "Got Xiaomi %s (%s):", result->name.c_str(), address.c_str()); 115 | 116 | if (result->score.has_value()) { 117 | ESP_LOGD(TAG, " Score: %.0f", *result->score); 118 | } 119 | if (result->battery_level.has_value()) { 120 | ESP_LOGD(TAG, " Battery Level: %.0f%%", *result->battery_level); 121 | } 122 | 123 | return true; 124 | } 125 | 126 | } // namespace xiaomi_toothbrush_ble 127 | } // namespace esphome 128 | 129 | #endif 130 | -------------------------------------------------------------------------------- /components/xiaomi_m1st500/xiaomi_toothbrush_ble.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" 5 | 6 | #ifdef USE_ESP32 7 | 8 | namespace esphome { 9 | namespace xiaomi_toothbrush_ble { 10 | 11 | struct XiaomiParseResult { 12 | enum { 13 | TYPE_M1ST500 14 | } type; 15 | std::string name; 16 | optional score; 17 | optional battery_level; 18 | bool has_data; // 0x40 19 | bool has_capability; // 0x20 20 | bool has_encryption; // 0x08 21 | bool is_duplicate; 22 | int raw_offset; 23 | }; 24 | 25 | 26 | bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result); 27 | bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result); 28 | optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data); 29 | bool report_xiaomi_results(const optional &result, const std::string &address); 30 | 31 | 32 | } // namespace xiaomi_ble 33 | } // namespace esphome 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /components/xiaomi_remote/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome import automation 4 | from esphome.components import sensor, binary_sensor, text_sensor, esp32_ble_tracker 5 | from esphome.const import ( 6 | CONF_BATTERY_LEVEL, 7 | CONF_MAC_ADDRESS, 8 | STATE_CLASS_MEASUREMENT, 9 | ICON_EMPTY, 10 | UNIT_PERCENT, 11 | DEVICE_CLASS_BATTERY, 12 | DEVICE_CLASS_SMOKE, 13 | DEVICE_CLASS_EMPTY, 14 | CONF_ID, 15 | CONF_BINDKEY, 16 | CONF_TRIGGER_ID 17 | ) 18 | 19 | CONF_BUTTON = "button" 20 | CONF_PRESS = "press" 21 | CONF_ACTION = "action" 22 | CONF_ACTION_TEXT = "action_text" 23 | CONF_ON_CLICK = "on_click" 24 | CONF_ON_DOUBLE_CLICK = "on_double_click" 25 | CONF_ON_TRIPLE_CLICK = "on_triple_click" 26 | CONF_ON_LONG_PRESS = "on_long_press" 27 | 28 | DEPENDENCIES = ["esp32_ble_tracker"] 29 | AUTO_LOAD = ["binary_sensor", "sensor", "text_sensor"] 30 | MULTI_CONF = True 31 | 32 | xiaomi_remote_ns = cg.esphome_ns.namespace("xiaomi_remote") 33 | XiaomiRemote = xiaomi_remote_ns.class_( 34 | "XiaomiRemote", esp32_ble_tracker.ESPBTDeviceListener, cg.Component 35 | ) 36 | 37 | ButtonListener = xiaomi_remote_ns.class_( 38 | "ButtonListener" 39 | ) 40 | 41 | RemoteBinarySensor = xiaomi_remote_ns.class_( 42 | "RemoteBinarySensor", binary_sensor.BinarySensor, ButtonListener 43 | ) 44 | 45 | RemoteSensor = xiaomi_remote_ns.class_( 46 | "RemoteSensor", sensor.Sensor, ButtonListener 47 | ) 48 | 49 | RemoteTextSensor = xiaomi_remote_ns.class_( 50 | "RemoteTextSensor", text_sensor.TextSensor, ButtonListener 51 | ) 52 | 53 | ClickTrigger = xiaomi_remote_ns.class_( 54 | "ClickTrigger", automation.Trigger.template() 55 | ) 56 | 57 | DoubleClickTrigger = xiaomi_remote_ns.class_( 58 | "DoubleClickTrigger", automation.Trigger.template() 59 | ) 60 | 61 | TripleClickTrigger = xiaomi_remote_ns.class_( 62 | "TripleClickTrigger", automation.Trigger.template() 63 | ) 64 | 65 | LongPressTrigger = xiaomi_remote_ns.class_( 66 | "LongPressTrigger", automation.Trigger.template() 67 | ) 68 | 69 | def trigger_schema(trigger): 70 | return automation.validate_automation( 71 | { 72 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( 73 | trigger 74 | ), 75 | cv.Optional(CONF_BUTTON): cv.int_range(min=0, max=9), 76 | } 77 | ) 78 | 79 | CONFIG_SCHEMA = ( 80 | cv.Schema( 81 | { 82 | cv.GenerateID(): cv.declare_id(XiaomiRemote), 83 | cv.Required(CONF_MAC_ADDRESS): cv.mac_address, 84 | cv.Optional(CONF_BINDKEY): cv.bind_key, 85 | cv.Optional(CONF_PRESS): cv.ensure_list( 86 | binary_sensor.binary_sensor_schema( 87 | RemoteBinarySensor, 88 | device_class=DEVICE_CLASS_SMOKE, 89 | ).extend( 90 | { 91 | cv.Optional(CONF_BUTTON): cv.int_range(min=0, max=9), 92 | } 93 | ) 94 | ), 95 | cv.Optional(CONF_ACTION): cv.ensure_list( 96 | sensor.sensor_schema( 97 | RemoteSensor, 98 | icon="mdi:counter", 99 | accuracy_decimals=1, 100 | device_class=DEVICE_CLASS_EMPTY, 101 | state_class=STATE_CLASS_MEASUREMENT, 102 | ).extend( 103 | { 104 | cv.Optional(CONF_BUTTON): cv.int_range(min=0, max=9), 105 | } 106 | ) 107 | ), 108 | cv.Optional(CONF_ACTION_TEXT): cv.ensure_list( 109 | text_sensor.text_sensor_schema( 110 | RemoteTextSensor, 111 | ).extend( 112 | { 113 | cv.Optional(CONF_BUTTON): cv.int_range(min=0, max=9), 114 | } 115 | ) 116 | ), 117 | cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( 118 | unit_of_measurement=UNIT_PERCENT, 119 | icon=ICON_EMPTY, 120 | accuracy_decimals=0, 121 | device_class=DEVICE_CLASS_BATTERY, 122 | state_class=STATE_CLASS_MEASUREMENT, 123 | ), 124 | cv.Optional(CONF_ON_CLICK): trigger_schema(ClickTrigger), 125 | cv.Optional(CONF_ON_DOUBLE_CLICK): trigger_schema(DoubleClickTrigger), 126 | cv.Optional(CONF_ON_TRIPLE_CLICK): trigger_schema(TripleClickTrigger), 127 | cv.Optional(CONF_ON_LONG_PRESS): trigger_schema(LongPressTrigger), 128 | } 129 | ) 130 | .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) 131 | .extend(cv.COMPONENT_SCHEMA) 132 | ) 133 | 134 | 135 | async def to_code(config): 136 | var = cg.new_Pvariable(config[CONF_ID]) 137 | await cg.register_component(var, config) 138 | await esp32_ble_tracker.register_ble_device(var, config) 139 | 140 | cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) 141 | 142 | if CONF_BINDKEY in config: 143 | cg.add(var.set_bindkey(config[CONF_BINDKEY])) 144 | 145 | for conf in config.get(CONF_PRESS, []): 146 | sens = await binary_sensor.new_binary_sensor(conf) 147 | cg.add(var.register_listener(sens)) 148 | if CONF_BUTTON in conf: 149 | cg.add(sens.set_button_index(conf[CONF_BUTTON])) 150 | for conf in config.get(CONF_ACTION, []): 151 | sens = await sensor.new_sensor(conf) 152 | cg.add(var.register_listener(sens)) 153 | if CONF_BUTTON in conf: 154 | cg.add(sens.set_button_index(conf[CONF_BUTTON])) 155 | for conf in config.get(CONF_ACTION_TEXT, []): 156 | sens = await text_sensor.new_text_sensor(conf) 157 | cg.add(var.register_listener(sens)) 158 | if CONF_BUTTON in conf: 159 | cg.add(sens.set_button_index(conf[CONF_BUTTON])) 160 | 161 | if CONF_BATTERY_LEVEL in config: 162 | sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) 163 | cg.add(var.set_battery_level(sens)) 164 | 165 | 166 | for trigger_key in [CONF_ON_CLICK, CONF_ON_DOUBLE_CLICK, CONF_ON_TRIPLE_CLICK, CONF_ON_LONG_PRESS]: 167 | for conf in config.get(trigger_key, []): 168 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) 169 | cg.add(var.register_listener(trigger)) 170 | if CONF_BUTTON in conf: 171 | cg.add(trigger.set_button_index(conf[CONF_BUTTON])) 172 | await automation.build_automation(trigger, [], conf) -------------------------------------------------------------------------------- /components/xiaomi_remote/xiaomi_remote.cpp: -------------------------------------------------------------------------------- 1 | #include "xiaomi_remote.h" 2 | #include "esphome/core/log.h" 3 | 4 | #ifdef USE_ESP32 5 | 6 | namespace esphome { 7 | namespace xiaomi_remote { 8 | 9 | static const char *const TAG = "xiaomi_remote"; 10 | 11 | void XiaomiRemote::dump_config() { 12 | ESP_LOGCONFIG(TAG, "Xiaomi Remote"); 13 | LOG_SENSOR(" ", "Battery Level", this->battery_level_); 14 | } 15 | 16 | bool XiaomiRemote::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { 17 | if (device.address_uint64() != this->address_) { 18 | ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); 19 | return false; 20 | } 21 | ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); 22 | 23 | for (auto &service_data : device.get_service_datas()) { 24 | auto res = parse_xiaomi_header(service_data); 25 | if (!res.has_value()) { 26 | continue; 27 | } 28 | if (res->is_duplicate) { 29 | continue; 30 | } 31 | if (res->has_encryption) { 32 | if (!this->bindkey_.has_value()) { 33 | ESP_LOGE(TAG, "need bindkey!"); 34 | continue; 35 | } 36 | if (!(decrypt_xiaomi_payload(const_cast &>(service_data.data), (*this->bindkey_).data(), this->address_))) { 37 | continue; 38 | } 39 | } 40 | if (!(parse_xiaomi_message(service_data.data, *res))) { 41 | continue; 42 | } 43 | if (!(report_xiaomi_results(res, device.address_str()))) { 44 | continue; 45 | } 46 | if (res->button.has_value()) 47 | for (auto *listener : listeners_) { 48 | listener->on_change(*res->button, *res->action); 49 | } 50 | if (res->battery_level.has_value() && this->battery_level_ != nullptr) 51 | this->battery_level_->publish_state(*res->battery_level); 52 | } 53 | 54 | return true; 55 | } 56 | 57 | void XiaomiRemote::set_bindkey(const std::string &bindkey) { 58 | if (bindkey.size() != 32) { 59 | return; 60 | } 61 | std::array keys; 62 | char temp[3] = {0}; 63 | for (int i = 0; i < 16; i++) { 64 | strncpy(temp, &(bindkey.c_str()[i * 2]), 2); 65 | keys[i] = std::strtoul(temp, nullptr, 16); 66 | } 67 | this->bindkey_ = keys; 68 | } 69 | 70 | void RemoteSensor::on_change(uint8_t action) { 71 | switch (action) { 72 | case BUTTON_TYPE_CLICK: 73 | this->publish_state(1); 74 | break; 75 | case BUTTON_TYPE_DOUBLE_CLICK: 76 | this->publish_state(2); 77 | break; 78 | case BUTTON_TYPE_TRIPLE_CLICK: 79 | this->publish_state(3); 80 | break; 81 | case BUTTON_TYPE_LONG_PRESS: 82 | this->publish_state(99); 83 | break; 84 | default: 85 | break; 86 | } 87 | this->publish_state(0); 88 | } 89 | 90 | void RemoteTextSensor::on_change(uint8_t action) { 91 | switch (action) { 92 | case BUTTON_TYPE_CLICK: 93 | this->publish_state("Click"); 94 | break; 95 | case BUTTON_TYPE_DOUBLE_CLICK: 96 | this->publish_state("Double-click"); 97 | break; 98 | case BUTTON_TYPE_TRIPLE_CLICK: 99 | this->publish_state("Triple-click"); 100 | break; 101 | case BUTTON_TYPE_LONG_PRESS: 102 | this->publish_state("Long press"); 103 | break; 104 | default: 105 | this->publish_state("Unkown"); 106 | break; 107 | } 108 | this->publish_state("Idle"); 109 | } 110 | 111 | } // namespace xiaomi_remote 112 | } // namespace esphome 113 | 114 | #endif 115 | -------------------------------------------------------------------------------- /components/xiaomi_remote/xiaomi_remote.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/sensor/sensor.h" 5 | #include "esphome/components/binary_sensor/binary_sensor.h" 6 | #include "esphome/components/text_sensor/text_sensor.h" 7 | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" 8 | #include "xiaomi_remote_ble.h" 9 | 10 | #ifdef USE_ESP32 11 | 12 | namespace esphome { 13 | namespace xiaomi_remote { 14 | 15 | class ButtonListener { 16 | public: 17 | virtual void on_change(uint8_t action) = 0; 18 | void on_change(uint16_t button, uint8_t action) { 19 | if (this->button_.has_value() && button != this->button_) { 20 | return; 21 | } 22 | this->on_change(action); 23 | } 24 | void set_button_index(uint16_t index) {this->button_ = index;} 25 | protected: 26 | optional button_; 27 | }; 28 | 29 | 30 | class XiaomiRemote : public Component, public esp32_ble_tracker::ESPBTDeviceListener { 31 | public: 32 | void set_address(uint64_t address) { address_ = address; } 33 | void set_bindkey(const std::string &bindkey); 34 | 35 | bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; 36 | 37 | void dump_config() override; 38 | float get_setup_priority() const override { return setup_priority::DATA; } 39 | void register_listener(ButtonListener *listener) { this->listeners_.push_back(listener); } 40 | void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } 41 | 42 | protected: 43 | uint64_t address_; 44 | std::vector listeners_; 45 | sensor::Sensor *battery_level_{nullptr}; 46 | optional> bindkey_; 47 | }; 48 | 49 | 50 | class RemoteBinarySensor : public binary_sensor::BinarySensor, public ButtonListener { 51 | public: 52 | virtual void on_change(uint8_t action) override { 53 | this->publish_state(true); 54 | this->publish_state(false); 55 | } 56 | }; 57 | 58 | class RemoteSensor : public sensor::Sensor, public ButtonListener { 59 | public: 60 | virtual void on_change(uint8_t action) override; 61 | }; 62 | 63 | class RemoteTextSensor : public text_sensor::TextSensor, public ButtonListener { 64 | public: 65 | virtual void on_change(uint8_t action) override; 66 | }; 67 | 68 | class ClickTrigger : public Trigger<>, public ButtonListener { 69 | public: 70 | void on_change(uint8_t action) override { 71 | if (action == BUTTON_TYPE_CLICK) { 72 | this->trigger(); 73 | } 74 | } 75 | }; 76 | 77 | class DoubleClickTrigger : public Trigger<>, public ButtonListener { 78 | public: 79 | void on_change(uint8_t action) override { 80 | if (action == BUTTON_TYPE_DOUBLE_CLICK) { 81 | this->trigger(); 82 | } 83 | } 84 | }; 85 | 86 | class TripleClickTrigger : public Trigger<>, public ButtonListener { 87 | public: 88 | void on_change(uint8_t action) override { 89 | if (action == BUTTON_TYPE_TRIPLE_CLICK) { 90 | this->trigger(); 91 | } 92 | } 93 | }; 94 | 95 | class LongPressTrigger : public Trigger<>, public ButtonListener { 96 | public: 97 | void on_change(uint8_t action) override { 98 | if (action == BUTTON_TYPE_LONG_PRESS) { 99 | this->trigger(); 100 | } 101 | } 102 | }; 103 | 104 | } // namespace xiaomi_remote 105 | } // namespace esphome 106 | 107 | #endif 108 | -------------------------------------------------------------------------------- /components/xiaomi_remote/xiaomi_remote_ble.cpp: -------------------------------------------------------------------------------- 1 | #include "xiaomi_remote.h" 2 | #include "esphome/core/helpers.h" 3 | #include "esphome/core/log.h" 4 | 5 | #ifdef USE_ESP32 6 | 7 | #include 8 | #include "mbedtls/ccm.h" 9 | 10 | namespace esphome { 11 | namespace xiaomi_remote { 12 | 13 | static const char *const TAG = "xiaomi_remote"; 14 | 15 | bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) { 16 | if ((value_type == 0x100A) && (value_length == 1)) { 17 | result.battery_level = data[0]; 18 | } else if ((value_type == 0x1001) && (value_length == 3)) { 19 | result.button = encode_uint16(data[0], data[1]); 20 | result.action = data[2]; 21 | } else { 22 | return false; 23 | } 24 | 25 | return true; 26 | } 27 | 28 | bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result) { 29 | result.has_encryption = message[0] & 0x08; // update encryption status 30 | if (result.has_encryption) { 31 | ESP_LOGVV(TAG, "parse_xiaomi_message(): payload is encrypted, stop reading message."); 32 | return false; 33 | } 34 | 35 | // Data point specs 36 | // Byte 0: type 37 | // Byte 1: fixed 0x10 38 | // Byte 2: length 39 | // Byte 3..3+len-1: data point value 40 | 41 | const uint8_t *payload = message.data() + result.raw_offset; 42 | uint8_t payload_length = message.size() - result.raw_offset; 43 | uint8_t payload_offset = 0; 44 | bool success = false; 45 | 46 | if (payload_length < 4) { 47 | ESP_LOGVV(TAG, "parse_xiaomi_message(): payload has wrong size (%d)!", payload_length); 48 | return false; 49 | } 50 | 51 | while (payload_length > 3) { 52 | if (payload[payload_offset + 1] != 0x10 && payload[payload_offset + 1] != 0x00) { 53 | ESP_LOGVV(TAG, "parse_xiaomi_message(): fixed byte not found, stop parsing residual data."); 54 | break; 55 | } 56 | 57 | const uint8_t value_length = payload[payload_offset + 2]; 58 | if ((value_length < 1) || (value_length > 4) || (payload_length < (3 + value_length))) { 59 | ESP_LOGVV(TAG, "parse_xiaomi_message(): value has wrong size (%d)!", value_length); 60 | break; 61 | } 62 | 63 | const uint16_t value_type = encode_uint16(payload[payload_offset + 1], payload[payload_offset + 0]); 64 | const uint8_t *data = &payload[payload_offset + 3]; 65 | 66 | if (parse_xiaomi_value(value_type, data, value_length, result)) 67 | success = true; 68 | 69 | payload_length -= 3 + value_length; 70 | payload_offset += 3 + value_length; 71 | } 72 | 73 | return success; 74 | } 75 | 76 | optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data) { 77 | XiaomiParseResult result; 78 | if (!service_data.uuid.contains(0x95, 0xFE)) { 79 | ESP_LOGVV(TAG, "parse_xiaomi_header(): no service data UUID magic bytes."); 80 | return {}; 81 | } 82 | 83 | auto raw = service_data.data; 84 | result.has_data = raw[0] & 0x40; 85 | result.has_capability = raw[0] & 0x20; 86 | result.has_encryption = raw[0] & 0x08; 87 | result.has_mac = raw[0] & 0x10; 88 | 89 | if (!result.has_data) { 90 | ESP_LOGVV(TAG, "parse_xiaomi_header(): service data has no DATA flag."); 91 | return {}; 92 | } 93 | 94 | static uint8_t last_frame_count = 0; 95 | if (last_frame_count == raw[4]) { 96 | ESP_LOGVV(TAG, "parse_xiaomi_header(): duplicate data packet received (%d).", static_cast(last_frame_count)); 97 | result.is_duplicate = true; 98 | return {}; 99 | } 100 | last_frame_count = raw[4]; 101 | result.is_duplicate = false; 102 | result.raw_offset = 5 + (result.has_capability ? 1: 0) + (result.has_mac? 6 : 0); 103 | 104 | const uint16_t device_uuid = encode_uint16(raw[3], raw[2]); 105 | 106 | result.type = XiaomiParseResult::TYPE_REMOTE; 107 | result.name = "Remote"; 108 | 109 | return result; 110 | } 111 | 112 | bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address) { 113 | if (!((raw.size() == 19) || ((raw.size() >= 22) && (raw.size() <= 24)))) { 114 | ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): data packet has wrong size (%d)!", raw.size()); 115 | ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str()); 116 | return false; 117 | } 118 | 119 | uint8_t mac_reverse[6] = {0}; 120 | mac_reverse[5] = (uint8_t) (address >> 40); 121 | mac_reverse[4] = (uint8_t) (address >> 32); 122 | mac_reverse[3] = (uint8_t) (address >> 24); 123 | mac_reverse[2] = (uint8_t) (address >> 16); 124 | mac_reverse[1] = (uint8_t) (address >> 8); 125 | mac_reverse[0] = (uint8_t) (address >> 0); 126 | 127 | XiaomiAESVector vector{.key = {0}, 128 | .plaintext = {0}, 129 | .ciphertext = {0}, 130 | .authdata = {0x11}, 131 | .iv = {0}, 132 | .tag = {0}, 133 | .keysize = 16, 134 | .authsize = 1, 135 | .datasize = 0, 136 | .tagsize = 4, 137 | .ivsize = 12}; 138 | 139 | vector.datasize = (raw.size() == 19) ? raw.size() - 12 : raw.size() - 18; 140 | int cipher_pos = (raw.size() == 19) ? 5 : 11; 141 | 142 | const uint8_t *v = raw.data(); 143 | 144 | memcpy(vector.key, bindkey, vector.keysize); 145 | memcpy(vector.ciphertext, v + cipher_pos, vector.datasize); 146 | memcpy(vector.tag, v + raw.size() - vector.tagsize, vector.tagsize); 147 | memcpy(vector.iv, mac_reverse, 6); // MAC address reverse 148 | memcpy(vector.iv + 6, v + 2, 3); // sensor type (2) + packet id (1) 149 | memcpy(vector.iv + 9, v + raw.size() - 7, 3); // payload counter 150 | 151 | mbedtls_ccm_context ctx; 152 | mbedtls_ccm_init(&ctx); 153 | 154 | int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, vector.key, vector.keysize * 8); 155 | if (ret) { 156 | ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): mbedtls_ccm_setkey() failed."); 157 | mbedtls_ccm_free(&ctx); 158 | return false; 159 | } 160 | 161 | ret = mbedtls_ccm_auth_decrypt(&ctx, vector.datasize, vector.iv, vector.ivsize, vector.authdata, vector.authsize, 162 | vector.ciphertext, vector.plaintext, vector.tag, vector.tagsize); 163 | if (ret) { 164 | uint8_t mac_address[6] = {0}; 165 | memcpy(mac_address, mac_reverse + 5, 1); 166 | memcpy(mac_address + 1, mac_reverse + 4, 1); 167 | memcpy(mac_address + 2, mac_reverse + 3, 1); 168 | memcpy(mac_address + 3, mac_reverse + 2, 1); 169 | memcpy(mac_address + 4, mac_reverse + 1, 1); 170 | memcpy(mac_address + 5, mac_reverse, 1); 171 | ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed."); 172 | ESP_LOGVV(TAG, " MAC address : %s", format_hex_pretty(mac_address, 6).c_str()); 173 | ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str()); 174 | ESP_LOGVV(TAG, " Key : %s", format_hex_pretty(vector.key, vector.keysize).c_str()); 175 | ESP_LOGVV(TAG, " Iv : %s", format_hex_pretty(vector.iv, vector.ivsize).c_str()); 176 | ESP_LOGVV(TAG, " Cipher : %s", format_hex_pretty(vector.ciphertext, vector.datasize).c_str()); 177 | ESP_LOGVV(TAG, " Tag : %s", format_hex_pretty(vector.tag, vector.tagsize).c_str()); 178 | mbedtls_ccm_free(&ctx); 179 | return false; 180 | } 181 | 182 | // replace encrypted payload with plaintext 183 | uint8_t *p = vector.plaintext; 184 | for (std::vector::iterator it = raw.begin() + cipher_pos; it != raw.begin() + cipher_pos + vector.datasize; 185 | ++it) { 186 | *it = *(p++); 187 | } 188 | 189 | // clear encrypted flag 190 | raw[0] &= ~0x08; 191 | 192 | ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption passed."); 193 | ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d", format_hex_pretty(raw.data() + cipher_pos, vector.datasize).c_str(), 194 | static_cast(raw[4])); 195 | 196 | mbedtls_ccm_free(&ctx); 197 | return true; 198 | } 199 | 200 | bool report_xiaomi_results(const optional &result, const std::string &address) { 201 | if (!result.has_value()) { 202 | ESP_LOGVV(TAG, "report_xiaomi_results(): no results available."); 203 | return false; 204 | } 205 | 206 | ESP_LOGD(TAG, "Got Xiaomi %s (%s):", result->name.c_str(), address.c_str()); 207 | 208 | if (result->button.has_value()) { 209 | ESP_LOGD(TAG, " Button Index: %d", *result->button); 210 | } 211 | 212 | if (result->action.has_value()) { 213 | switch (*result->action) { 214 | case BUTTON_TYPE_CLICK: 215 | ESP_LOGD(TAG, " Action: Click"); 216 | break; 217 | case BUTTON_TYPE_DOUBLE_CLICK: 218 | ESP_LOGD(TAG, " Action: Double-click"); 219 | break; 220 | case BUTTON_TYPE_LONG_PRESS: 221 | ESP_LOGD(TAG, " Action: Long press"); 222 | break; 223 | case BUTTON_TYPE_TRIPLE_CLICK: 224 | ESP_LOGD(TAG, " Action: Triple-click"); 225 | break; 226 | default: 227 | ESP_LOGD(TAG, " Action: Unkown(%d)", *result->action); 228 | break; 229 | } 230 | } 231 | if (result->battery_level.has_value()) { 232 | ESP_LOGD(TAG, " Battery Level: %.0f%%", *result->battery_level); 233 | } 234 | 235 | return true; 236 | } 237 | 238 | } // namespace xiaomi_remote 239 | } // namespace esphome 240 | 241 | #endif 242 | -------------------------------------------------------------------------------- /components/xiaomi_remote/xiaomi_remote_ble.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" 5 | 6 | #ifdef USE_ESP32 7 | 8 | namespace esphome { 9 | namespace xiaomi_remote { 10 | 11 | enum { 12 | BUTTON_TYPE_CLICK = 0, 13 | BUTTON_TYPE_DOUBLE_CLICK, 14 | BUTTON_TYPE_LONG_PRESS, 15 | BUTTON_TYPE_TRIPLE_CLICK, 16 | }; 17 | 18 | struct XiaomiParseResult { 19 | enum { 20 | TYPE_REMOTE 21 | } type; 22 | std::string name; 23 | optional button; 24 | optional action; 25 | optional battery_level; 26 | bool has_data; // 0x40 27 | bool has_capability; // 0x20 28 | bool has_encryption; // 0x08 29 | bool has_mac; // 0x10 30 | bool is_duplicate; 31 | int raw_offset; 32 | }; 33 | 34 | 35 | struct XiaomiAESVector { 36 | uint8_t key[16]; 37 | uint8_t plaintext[16]; 38 | uint8_t ciphertext[16]; 39 | uint8_t authdata[16]; 40 | uint8_t iv[16]; 41 | uint8_t tag[16]; 42 | size_t keysize; 43 | size_t authsize; 44 | size_t datasize; 45 | size_t tagsize; 46 | size_t ivsize; 47 | }; 48 | 49 | bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result); 50 | bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result); 51 | optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data); 52 | bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address); 53 | bool report_xiaomi_results(const optional &result, const std::string &address); 54 | 55 | 56 | } // namespace xiaomi_ble 57 | } // namespace esphome 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /components/xiaomi_smoke_detector/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanh7/esphome-custom-components/78a9afe3c2164fee312d50f547a7c0cea73e6d86/components/xiaomi_smoke_detector/__init__.py -------------------------------------------------------------------------------- /components/xiaomi_smoke_detector/sensor.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import sensor, binary_sensor, text_sensor, esp32_ble_tracker 4 | from esphome.const import ( 5 | CONF_BATTERY_LEVEL, 6 | CONF_MAC_ADDRESS, 7 | STATE_CLASS_MEASUREMENT, 8 | ICON_EMPTY, 9 | UNIT_PERCENT, 10 | DEVICE_CLASS_BATTERY, 11 | DEVICE_CLASS_SMOKE, 12 | DEVICE_CLASS_EMPTY, 13 | CONF_ID, 14 | CONF_BINDKEY, 15 | ) 16 | 17 | CONF_STATUS = "status" 18 | CONF_STATUS_TEXT = "status_text" 19 | CONF_ALERT = "alert" 20 | 21 | DEPENDENCIES = ["esp32_ble_tracker"] 22 | AUTO_LOAD = ["binary_sensor", "text_sensor"] 23 | 24 | xiaomi_smoke_detector_ns = cg.esphome_ns.namespace("xiaomi_smoke_detector") 25 | XiaomiSmokeDetector = xiaomi_smoke_detector_ns.class_( 26 | "XiaomiSmokeDetector", esp32_ble_tracker.ESPBTDeviceListener, cg.Component 27 | ) 28 | 29 | StatusListener = xiaomi_smoke_detector_ns.class_( 30 | "StatusListener" 31 | ) 32 | 33 | SmokerDetectorStatusSensor = xiaomi_smoke_detector_ns.class_( 34 | "SmokerDetectorStatusSensor", sensor.Sensor, StatusListener 35 | ) 36 | 37 | SmokerDetectorStatusTextSensor = xiaomi_smoke_detector_ns.class_( 38 | "SmokerDetectorStatusTextSensor", text_sensor.TextSensor, StatusListener 39 | ) 40 | 41 | SmokerDetectorAlarmSensor = xiaomi_smoke_detector_ns.class_( 42 | "SmokerDetectorAlarmSensor", binary_sensor.BinarySensor, StatusListener 43 | ) 44 | 45 | CONFIG_SCHEMA = ( 46 | cv.Schema( 47 | { 48 | cv.GenerateID(): cv.declare_id(XiaomiSmokeDetector), 49 | cv.Required(CONF_MAC_ADDRESS): cv.mac_address, 50 | cv.Required(CONF_BINDKEY): cv.bind_key, 51 | cv.Optional(CONF_ALERT): binary_sensor.binary_sensor_schema( 52 | SmokerDetectorAlarmSensor, 53 | device_class=DEVICE_CLASS_SMOKE, 54 | ).extend(cv.COMPONENT_SCHEMA), 55 | cv.Optional(CONF_STATUS): sensor.sensor_schema( 56 | SmokerDetectorStatusSensor, 57 | icon="mdi:smoke-detector", 58 | accuracy_decimals=1, 59 | device_class=DEVICE_CLASS_EMPTY, 60 | state_class=STATE_CLASS_MEASUREMENT, 61 | ), 62 | cv.Optional(CONF_STATUS_TEXT): text_sensor.text_sensor_schema( 63 | SmokerDetectorStatusTextSensor, 64 | icon="mdi:smoke-detector", 65 | ).extend(cv.COMPONENT_SCHEMA), 66 | cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( 67 | unit_of_measurement=UNIT_PERCENT, 68 | icon=ICON_EMPTY, 69 | accuracy_decimals=0, 70 | device_class=DEVICE_CLASS_BATTERY, 71 | state_class=STATE_CLASS_MEASUREMENT, 72 | ), 73 | } 74 | ) 75 | .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) 76 | .extend(cv.COMPONENT_SCHEMA) 77 | ) 78 | 79 | 80 | async def to_code(config): 81 | var = cg.new_Pvariable(config[CONF_ID]) 82 | await cg.register_component(var, config) 83 | await esp32_ble_tracker.register_ble_device(var, config) 84 | 85 | cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) 86 | cg.add(var.set_bindkey(config[CONF_BINDKEY])) 87 | 88 | if CONF_ALERT in config: 89 | sens = await binary_sensor.new_binary_sensor(config[CONF_ALERT]) 90 | cg.add(var.register_listener(sens)) 91 | if CONF_STATUS in config: 92 | sens = await sensor.new_sensor(config[CONF_STATUS]) 93 | cg.add(var.register_listener(sens)) 94 | if CONF_STATUS_TEXT in config: 95 | sens = await text_sensor.new_text_sensor(config[CONF_STATUS_TEXT]) 96 | cg.add(var.register_listener(sens)) 97 | if CONF_BATTERY_LEVEL in config: 98 | sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) 99 | cg.add(var.set_battery_level(sens)) 100 | -------------------------------------------------------------------------------- /components/xiaomi_smoke_detector/xiaomi_smoke_ble.cpp: -------------------------------------------------------------------------------- 1 | #include "xiaomi_smoke_ble.h" 2 | #include "esphome/core/helpers.h" 3 | #include "esphome/core/log.h" 4 | 5 | #ifdef USE_ESP32 6 | 7 | #include 8 | #include "mbedtls/ccm.h" 9 | 10 | namespace esphome { 11 | namespace xiaomi_smoke_ble { 12 | 13 | static const char *const TAG = "xiaomi_smoke_ble"; 14 | 15 | bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) { 16 | if ((value_type == 0x100A) && (value_length == 1)) { 17 | result.battery_level = data[0]; 18 | } else if ((value_type == 0x000D || value_type == 0x1015) && (value_length == 1)) { 19 | result.event = data[0]; 20 | } else { 21 | return false; 22 | } 23 | 24 | return true; 25 | } 26 | 27 | bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result) { 28 | result.has_encryption = message[0] & 0x08; // update encryption status 29 | if (result.has_encryption) { 30 | ESP_LOGVV(TAG, "parse_xiaomi_message(): payload is encrypted, stop reading message."); 31 | return false; 32 | } 33 | 34 | // Data point specs 35 | // Byte 0: type 36 | // Byte 1: fixed 0x10 37 | // Byte 2: length 38 | // Byte 3..3+len-1: data point value 39 | 40 | const uint8_t *payload = message.data() + result.raw_offset; 41 | uint8_t payload_length = message.size() - result.raw_offset; 42 | uint8_t payload_offset = 0; 43 | bool success = false; 44 | 45 | if (payload_length < 4) { 46 | ESP_LOGVV(TAG, "parse_xiaomi_message(): payload has wrong size (%d)!", payload_length); 47 | return false; 48 | } 49 | 50 | while (payload_length > 3) { 51 | if (payload[payload_offset + 1] != 0x10 && payload[payload_offset + 1] != 0x00) { 52 | ESP_LOGVV(TAG, "parse_xiaomi_message(): fixed byte not found, stop parsing residual data."); 53 | break; 54 | } 55 | 56 | const uint8_t value_length = payload[payload_offset + 2]; 57 | if ((value_length < 1) || (value_length > 4) || (payload_length < (3 + value_length))) { 58 | ESP_LOGVV(TAG, "parse_xiaomi_message(): value has wrong size (%d)!", value_length); 59 | break; 60 | } 61 | 62 | const uint16_t value_type = encode_uint16(payload[payload_offset + 1], payload[payload_offset + 0]); 63 | const uint8_t *data = &payload[payload_offset + 3]; 64 | 65 | if (parse_xiaomi_value(value_type, data, value_length, result)) 66 | success = true; 67 | 68 | payload_length -= 3 + value_length; 69 | payload_offset += 3 + value_length; 70 | } 71 | 72 | return success; 73 | } 74 | 75 | optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data) { 76 | XiaomiParseResult result; 77 | if (!service_data.uuid.contains(0x95, 0xFE)) { 78 | ESP_LOGVV(TAG, "parse_xiaomi_header(): no service data UUID magic bytes."); 79 | return {}; 80 | } 81 | 82 | auto raw = service_data.data; 83 | result.has_data = raw[0] & 0x40; 84 | result.has_capability = raw[0] & 0x20; 85 | result.has_encryption = raw[0] & 0x08; 86 | 87 | if (!result.has_data) { 88 | ESP_LOGVV(TAG, "parse_xiaomi_header(): service data has no DATA flag."); 89 | return {}; 90 | } 91 | 92 | static uint8_t last_frame_count = 0; 93 | if (last_frame_count == raw[4]) { 94 | ESP_LOGVV(TAG, "parse_xiaomi_header(): duplicate data packet received (%d).", static_cast(last_frame_count)); 95 | result.is_duplicate = true; 96 | return {}; 97 | } 98 | last_frame_count = raw[4]; 99 | result.is_duplicate = false; 100 | result.raw_offset = result.has_capability ? 12 : 11; 101 | 102 | const uint16_t device_uuid = encode_uint16(raw[3], raw[2]); 103 | 104 | if (device_uuid == 0x0997) { // Mi Smoke Detector 105 | result.type = XiaomiParseResult::TYPE_SMOKE_MCN02; 106 | result.name = "Smoke Detector"; 107 | } else { 108 | ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes."); 109 | return {}; 110 | } 111 | 112 | return result; 113 | } 114 | 115 | bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address) { 116 | if (!((raw.size() == 19) || ((raw.size() >= 22) && (raw.size() <= 24)))) { 117 | ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): data packet has wrong size (%d)!", raw.size()); 118 | ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str()); 119 | return false; 120 | } 121 | 122 | uint8_t mac_reverse[6] = {0}; 123 | mac_reverse[5] = (uint8_t) (address >> 40); 124 | mac_reverse[4] = (uint8_t) (address >> 32); 125 | mac_reverse[3] = (uint8_t) (address >> 24); 126 | mac_reverse[2] = (uint8_t) (address >> 16); 127 | mac_reverse[1] = (uint8_t) (address >> 8); 128 | mac_reverse[0] = (uint8_t) (address >> 0); 129 | 130 | XiaomiAESVector vector{.key = {0}, 131 | .plaintext = {0}, 132 | .ciphertext = {0}, 133 | .authdata = {0x11}, 134 | .iv = {0}, 135 | .tag = {0}, 136 | .keysize = 16, 137 | .authsize = 1, 138 | .datasize = 0, 139 | .tagsize = 4, 140 | .ivsize = 12}; 141 | 142 | vector.datasize = (raw.size() == 19) ? raw.size() - 12 : raw.size() - 18; 143 | int cipher_pos = (raw.size() == 19) ? 5 : 11; 144 | 145 | const uint8_t *v = raw.data(); 146 | 147 | memcpy(vector.key, bindkey, vector.keysize); 148 | memcpy(vector.ciphertext, v + cipher_pos, vector.datasize); 149 | memcpy(vector.tag, v + raw.size() - vector.tagsize, vector.tagsize); 150 | memcpy(vector.iv, mac_reverse, 6); // MAC address reverse 151 | memcpy(vector.iv + 6, v + 2, 3); // sensor type (2) + packet id (1) 152 | memcpy(vector.iv + 9, v + raw.size() - 7, 3); // payload counter 153 | 154 | mbedtls_ccm_context ctx; 155 | mbedtls_ccm_init(&ctx); 156 | 157 | int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, vector.key, vector.keysize * 8); 158 | if (ret) { 159 | ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): mbedtls_ccm_setkey() failed."); 160 | mbedtls_ccm_free(&ctx); 161 | return false; 162 | } 163 | 164 | ret = mbedtls_ccm_auth_decrypt(&ctx, vector.datasize, vector.iv, vector.ivsize, vector.authdata, vector.authsize, 165 | vector.ciphertext, vector.plaintext, vector.tag, vector.tagsize); 166 | if (ret) { 167 | uint8_t mac_address[6] = {0}; 168 | memcpy(mac_address, mac_reverse + 5, 1); 169 | memcpy(mac_address + 1, mac_reverse + 4, 1); 170 | memcpy(mac_address + 2, mac_reverse + 3, 1); 171 | memcpy(mac_address + 3, mac_reverse + 2, 1); 172 | memcpy(mac_address + 4, mac_reverse + 1, 1); 173 | memcpy(mac_address + 5, mac_reverse, 1); 174 | ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed."); 175 | ESP_LOGVV(TAG, " MAC address : %s", format_hex_pretty(mac_address, 6).c_str()); 176 | ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str()); 177 | ESP_LOGVV(TAG, " Key : %s", format_hex_pretty(vector.key, vector.keysize).c_str()); 178 | ESP_LOGVV(TAG, " Iv : %s", format_hex_pretty(vector.iv, vector.ivsize).c_str()); 179 | ESP_LOGVV(TAG, " Cipher : %s", format_hex_pretty(vector.ciphertext, vector.datasize).c_str()); 180 | ESP_LOGVV(TAG, " Tag : %s", format_hex_pretty(vector.tag, vector.tagsize).c_str()); 181 | mbedtls_ccm_free(&ctx); 182 | return false; 183 | } 184 | 185 | // replace encrypted payload with plaintext 186 | uint8_t *p = vector.plaintext; 187 | for (std::vector::iterator it = raw.begin() + cipher_pos; it != raw.begin() + cipher_pos + vector.datasize; 188 | ++it) { 189 | *it = *(p++); 190 | } 191 | 192 | // clear encrypted flag 193 | raw[0] &= ~0x08; 194 | 195 | ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption passed."); 196 | ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d", format_hex_pretty(raw.data() + cipher_pos, vector.datasize).c_str(), 197 | static_cast(raw[4])); 198 | 199 | mbedtls_ccm_free(&ctx); 200 | return true; 201 | } 202 | 203 | bool report_xiaomi_results(const optional &result, const std::string &address) { 204 | if (!result.has_value()) { 205 | ESP_LOGVV(TAG, "report_xiaomi_results(): no results available."); 206 | return false; 207 | } 208 | 209 | ESP_LOGD(TAG, "Got Xiaomi %s (%s):", result->name.c_str(), address.c_str()); 210 | 211 | if (result->event.has_value()) { 212 | ESP_LOGD(TAG, " Status(Event): %d", *result->event); 213 | switch (*result->event) { 214 | case 0x00: 215 | ESP_LOGD(TAG, " Status(Event): Normal"); 216 | break; 217 | case 0x01: 218 | ESP_LOGD(TAG, " Status(Event): Alert"); 219 | break; 220 | case 0x02: 221 | ESP_LOGD(TAG, " Status(Event): Fault"); 222 | break; 223 | case 0x03: 224 | ESP_LOGD(TAG, " Status(Event): Self-check"); 225 | break; 226 | case 0x04: 227 | ESP_LOGD(TAG, " Status(Event): Analog Alert"); 228 | break; 229 | default: 230 | ESP_LOGD(TAG, " Status(Event): Unkown(%d)", *result->event); 231 | break; 232 | } 233 | } 234 | if (result->battery_level.has_value()) { 235 | ESP_LOGD(TAG, " Battery Level: %.0f%%", *result->battery_level); 236 | } 237 | 238 | return true; 239 | } 240 | 241 | } // namespace xiaomi_smoke_ble 242 | } // namespace esphome 243 | 244 | #endif 245 | -------------------------------------------------------------------------------- /components/xiaomi_smoke_detector/xiaomi_smoke_ble.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" 5 | 6 | #ifdef USE_ESP32 7 | 8 | namespace esphome { 9 | namespace xiaomi_smoke_ble { 10 | 11 | struct XiaomiParseResult { 12 | enum { 13 | TYPE_SMOKE_MCN02 14 | } type; 15 | std::string name; 16 | optional event; 17 | optional battery_level; 18 | bool has_data; // 0x40 19 | bool has_capability; // 0x20 20 | bool has_encryption; // 0x08 21 | bool is_duplicate; 22 | int raw_offset; 23 | }; 24 | 25 | 26 | struct XiaomiAESVector { 27 | uint8_t key[16]; 28 | uint8_t plaintext[16]; 29 | uint8_t ciphertext[16]; 30 | uint8_t authdata[16]; 31 | uint8_t iv[16]; 32 | uint8_t tag[16]; 33 | size_t keysize; 34 | size_t authsize; 35 | size_t datasize; 36 | size_t tagsize; 37 | size_t ivsize; 38 | }; 39 | 40 | bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result); 41 | bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result); 42 | optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data); 43 | bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address); 44 | bool report_xiaomi_results(const optional &result, const std::string &address); 45 | 46 | 47 | } // namespace xiaomi_ble 48 | } // namespace esphome 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /components/xiaomi_smoke_detector/xiaomi_smoke_detector.cpp: -------------------------------------------------------------------------------- 1 | #include "xiaomi_smoke_detector.h" 2 | #include "esphome/core/log.h" 3 | 4 | #ifdef USE_ESP32 5 | 6 | namespace esphome { 7 | namespace xiaomi_smoke_detector { 8 | 9 | static const char *const TAG = "xiaomi_smoke_detector"; 10 | 11 | void XiaomiSmokeDetector::dump_config() { 12 | ESP_LOGCONFIG(TAG, "Xiaomi Smoke Detector"); 13 | LOG_SENSOR(" ", "Battery Level", this->battery_level_); 14 | } 15 | 16 | bool XiaomiSmokeDetector::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { 17 | if (device.address_uint64() != this->address_) { 18 | ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); 19 | return false; 20 | } 21 | ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); 22 | 23 | for (auto &service_data : device.get_service_datas()) { 24 | auto res = xiaomi_smoke_ble::parse_xiaomi_header(service_data); 25 | if (!res.has_value()) { 26 | continue; 27 | } 28 | if (res->is_duplicate) { 29 | continue; 30 | } 31 | if (res->has_encryption && 32 | (!(xiaomi_smoke_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, 33 | this->address_)))) { 34 | continue; 35 | } 36 | if (!(xiaomi_smoke_ble::parse_xiaomi_message(service_data.data, *res))) { 37 | continue; 38 | } 39 | if (!(xiaomi_smoke_ble::report_xiaomi_results(res, device.address_str()))) { 40 | continue; 41 | } 42 | if (res->event.has_value()) 43 | for (auto *listener : listeners_) { 44 | listener->on_change(*res->event); 45 | } 46 | if (res->battery_level.has_value() && this->battery_level_ != nullptr) 47 | this->battery_level_->publish_state(*res->battery_level); 48 | } 49 | 50 | return true; 51 | } 52 | 53 | void XiaomiSmokeDetector::set_bindkey(const std::string &bindkey) { 54 | memset(bindkey_, 0, 16); 55 | if (bindkey.size() != 32) { 56 | return; 57 | } 58 | char temp[3] = {0}; 59 | for (int i = 0; i < 16; i++) { 60 | strncpy(temp, &(bindkey.c_str()[i * 2]), 2); 61 | bindkey_[i] = std::strtoul(temp, nullptr, 16); 62 | } 63 | } 64 | 65 | void SmokerDetectorStatusTextSensor::on_change(uint8_t status) { 66 | switch (status) { 67 | case 0x00: 68 | this->publish_state("normal"); 69 | break; 70 | case 0x01: 71 | this->publish_state("alert"); 72 | break; 73 | case 0x02: 74 | this->publish_state("fault"); 75 | break; 76 | case 0x03: 77 | this->publish_state("self-check"); 78 | break; 79 | case 0x04: 80 | this->publish_state("analog alert"); 81 | break; 82 | default: 83 | this->publish_state("unkown"); 84 | break; 85 | } 86 | } 87 | 88 | } // namespace xiaomi_smoke_detector 89 | } // namespace esphome 90 | 91 | #endif 92 | -------------------------------------------------------------------------------- /components/xiaomi_smoke_detector/xiaomi_smoke_detector.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/sensor/sensor.h" 5 | #include "esphome/components/binary_sensor/binary_sensor.h" 6 | #include "esphome/components/text_sensor/text_sensor.h" 7 | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" 8 | #include "xiaomi_smoke_ble.h" 9 | 10 | #ifdef USE_ESP32 11 | 12 | namespace esphome { 13 | namespace xiaomi_smoke_detector { 14 | 15 | class StatusListener { 16 | public: 17 | virtual void on_change(uint8_t status) = 0; 18 | }; 19 | 20 | 21 | class XiaomiSmokeDetector : public Component, public esp32_ble_tracker::ESPBTDeviceListener { 22 | public: 23 | void set_address(uint64_t address) { address_ = address; } 24 | void set_bindkey(const std::string &bindkey); 25 | 26 | bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; 27 | 28 | void dump_config() override; 29 | float get_setup_priority() const override { return setup_priority::DATA; } 30 | void register_listener(StatusListener *listener) { this->listeners_.push_back(listener); } 31 | void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } 32 | 33 | protected: 34 | uint64_t address_; 35 | std::vector listeners_; 36 | sensor::Sensor *battery_level_{nullptr}; 37 | uint8_t bindkey_[16]; 38 | }; 39 | 40 | class SmokerDetectorAlarmSensor : public binary_sensor::BinarySensor, public StatusListener { 41 | public: 42 | virtual void on_change(uint8_t status) override { 43 | this->publish_state(status == 0x01); 44 | } 45 | }; 46 | 47 | class SmokerDetectorStatusSensor : public sensor::Sensor, public StatusListener { 48 | public: 49 | virtual void on_change(uint8_t status) override { 50 | this->publish_state(status); 51 | } 52 | }; 53 | 54 | class SmokerDetectorStatusTextSensor : public text_sensor::TextSensor, public StatusListener { 55 | public: 56 | virtual void on_change(uint8_t status) override; 57 | }; 58 | 59 | } // namespace xiaomi_smoke_detector 60 | } // namespace esphome 61 | 62 | #endif 63 | --------------------------------------------------------------------------------