From 42e6758aad235ec5cff344d45586b43ef619b336 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 16 Jan 2023 11:09:46 +0100 Subject: [PATCH] Initial commit --- .clang-format | 137 +++ .github/workflows/ci.yaml | 202 ++++ .github/workflows/matchers/ci-custom.json | 16 + .github/workflows/matchers/clang-tidy.json | 17 + .github/workflows/matchers/gcc.json | 18 + .github/workflows/matchers/lint-python.json | 28 + .github/workflows/matchers/python.json | 18 + .gitignore | 14 + .pre-commit-config.yaml | 28 + .yamllint | 77 ++ LICENSE | 201 ++++ README.md | 105 ++ components/ble_client/__init__.py | 153 +++ components/ble_client/automation.cpp | 75 ++ components/ble_client/automation.h | 100 ++ components/ble_client/ble_client.cpp | 95 ++ components/ble_client/ble_client.h | 81 ++ components/ble_client/output/__init__.py | 68 ++ .../ble_client/output/ble_binary_output.cpp | 75 ++ .../ble_client/output/ble_binary_output.h | 41 + components/ble_client/sensor/__init__.py | 174 ++++ components/ble_client/sensor/automation.h | 39 + .../ble_client/sensor/ble_rssi_sensor.cpp | 78 ++ .../ble_client/sensor/ble_rssi_sensor.h | 31 + components/ble_client/sensor/ble_sensor.cpp | 136 +++ components/ble_client/sensor/ble_sensor.h | 52 + components/ble_client/switch/__init__.py | 21 + components/ble_client/switch/ble_switch.cpp | 39 + components/ble_client/switch/ble_switch.h | 30 + components/ble_client/text_sensor/__init__.py | 121 +++ .../ble_client/text_sensor/automation.h | 39 + .../text_sensor/ble_text_sensor.cpp | 135 +++ .../ble_client/text_sensor/ble_text_sensor.h | 46 + components/esp32_ble_client/__init__.py | 12 + .../esp32_ble_client/ble_characteristic.cpp | 99 ++ .../esp32_ble_client/ble_characteristic.h | 39 + .../esp32_ble_client/ble_client_base.cpp | 439 ++++++++ components/esp32_ble_client/ble_client_base.h | 99 ++ components/esp32_ble_client/ble_descriptor.h | 25 + components/esp32_ble_client/ble_service.cpp | 77 ++ components/esp32_ble_client/ble_service.h | 36 + components/esp32_ble_tracker/__init__.py | 320 ++++++ components/esp32_ble_tracker/automation.h | 108 ++ .../esp32_ble_tracker/esp32_ble_tracker.cpp | 941 ++++++++++++++++++ .../esp32_ble_tracker/esp32_ble_tracker.h | 288 ++++++ components/esp32_ble_tracker/queue.h | 109 ++ components/votronic_ble/__init__.py | 39 + components/votronic_ble/sensor.py | 71 ++ components/votronic_ble/text_sensor.py | 49 + components/votronic_ble/votronic_ble.cpp | 217 ++++ components/votronic_ble/votronic_ble.h | 80 ++ docs/pdus/battery.txt | 170 ++++ docs/pdus/mppt.txt | 79 ++ esp32-ble-example-debug.yaml | 13 + esp32-ble-example-faker.yaml | 7 + esp32-ble-example.yaml | 69 ++ test-esp32.sh | 3 + 57 files changed, 5879 insertions(+) create mode 100644 .clang-format create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/matchers/ci-custom.json create mode 100644 .github/workflows/matchers/clang-tidy.json create mode 100644 .github/workflows/matchers/gcc.json create mode 100644 .github/workflows/matchers/lint-python.json create mode 100644 .github/workflows/matchers/python.json create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .yamllint create mode 100644 LICENSE create mode 100644 README.md create mode 100644 components/ble_client/__init__.py create mode 100644 components/ble_client/automation.cpp create mode 100644 components/ble_client/automation.h create mode 100644 components/ble_client/ble_client.cpp create mode 100644 components/ble_client/ble_client.h create mode 100644 components/ble_client/output/__init__.py create mode 100644 components/ble_client/output/ble_binary_output.cpp create mode 100644 components/ble_client/output/ble_binary_output.h create mode 100644 components/ble_client/sensor/__init__.py create mode 100644 components/ble_client/sensor/automation.h create mode 100644 components/ble_client/sensor/ble_rssi_sensor.cpp create mode 100644 components/ble_client/sensor/ble_rssi_sensor.h create mode 100644 components/ble_client/sensor/ble_sensor.cpp create mode 100644 components/ble_client/sensor/ble_sensor.h create mode 100644 components/ble_client/switch/__init__.py create mode 100644 components/ble_client/switch/ble_switch.cpp create mode 100644 components/ble_client/switch/ble_switch.h create mode 100644 components/ble_client/text_sensor/__init__.py create mode 100644 components/ble_client/text_sensor/automation.h create mode 100644 components/ble_client/text_sensor/ble_text_sensor.cpp create mode 100644 components/ble_client/text_sensor/ble_text_sensor.h create mode 100644 components/esp32_ble_client/__init__.py create mode 100644 components/esp32_ble_client/ble_characteristic.cpp create mode 100644 components/esp32_ble_client/ble_characteristic.h create mode 100644 components/esp32_ble_client/ble_client_base.cpp create mode 100644 components/esp32_ble_client/ble_client_base.h create mode 100644 components/esp32_ble_client/ble_descriptor.h create mode 100644 components/esp32_ble_client/ble_service.cpp create mode 100644 components/esp32_ble_client/ble_service.h create mode 100644 components/esp32_ble_tracker/__init__.py create mode 100644 components/esp32_ble_tracker/automation.h create mode 100644 components/esp32_ble_tracker/esp32_ble_tracker.cpp create mode 100644 components/esp32_ble_tracker/esp32_ble_tracker.h create mode 100644 components/esp32_ble_tracker/queue.h create mode 100644 components/votronic_ble/__init__.py create mode 100644 components/votronic_ble/sensor.py create mode 100644 components/votronic_ble/text_sensor.py create mode 100644 components/votronic_ble/votronic_ble.cpp create mode 100644 components/votronic_ble/votronic_ble.h create mode 100644 docs/pdus/battery.txt create mode 100644 docs/pdus/mppt.txt create mode 100644 esp32-ble-example-debug.yaml create mode 100644 esp32-ble-example-faker.yaml create mode 100644 esp32-ble-example.yaml create mode 100755 test-esp32.sh diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..f2d86c5 --- /dev/null +++ b/.clang-format @@ -0,0 +1,137 @@ +Language: Cpp +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^' + Priority: 2 + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 2000 +PointerAlignment: Right +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + CanonicalDelimiter: '' + BasedOnStyle: google +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: true +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 2 +UseTab: Never diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..98c3a5d --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,202 @@ +name: CI + +on: # yamllint disable-line rule:truthy + push: + branches: + - main + pull_request: + schedule: + - cron: 0 12 * * * + +jobs: + yamllint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: yaml-lint + uses: ibiqlik/action-yamllint@v3 + with: + config_file: .yamllint + + lint-clang-format: + env: + esphome_directory: esphome + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:latest + steps: + - uses: actions/checkout@v2 + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + + - name: 💣 Clone esphome project + run: git clone https://github.com/esphome/esphome.git + - name: 💣 Copy component into the esphome project + run: | + cp -r ../components/* esphome/components/ + git config user.name "ci" + git config user.email "ci@github.com" + git add . + git commit -a -m "Add external component" + working-directory: ${{ env.esphome_directory }} + + - name: Set up platformio environment + run: pio init --ide atom + working-directory: ${{ env.esphome_directory }} + + - name: Run clang-format + run: script/clang-format -i + working-directory: ${{ env.esphome_directory }} + + - name: Suggest changes + run: script/ci-suggest-changes + working-directory: ${{ env.esphome_directory }} + + lint-clang-tidy: + env: + esphome_directory: esphome + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:latest + steps: + - uses: actions/checkout@v2 + + - name: 💣 Clone esphome project + run: git clone https://github.com/esphome/esphome.git + - name: 💣 Copy component into the esphome project + run: | + cp -r ../components/* esphome/components/ + git config user.name "ci" + git config user.email "ci@github.com" + git add . + git commit -a -m "Add external component" + working-directory: ${{ env.esphome_directory }} + + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + working-directory: ${{ env.esphome_directory }} + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + # Can be removed as soon as esphome-lint container is fixed + - name: Add missing pexpect + run: pip install pexpect + - name: Run lint-cpp + run: script/lint-cpp -c + working-directory: ${{ env.esphome_directory }} + - name: Suggest changes + run: script/ci-suggest-changes + working-directory: ${{ env.esphome_directory }} + + lint-python: + env: + esphome_directory: esphome + # Don't use the esphome-lint docker image because it may contain outdated requirements. + # This way, all dependencies are cached via the cache action. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.9-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.9- + + - name: 💣Clone esphome project + run: git clone https://github.com/esphome/esphome.git + - name: 💣Copy component into the esphome project + run: | + cp -r ../components/* esphome/components/ + git config user.name "ci" + git config user.email "ci@github.com" + git add . + git commit -a -m "Add modbus_solax and solax_x1" + working-directory: ${{ env.esphome_directory }} + + - name: Set up python environment + run: script/setup + working-directory: ${{ env.esphome_directory }} + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/ci-custom.json" + echo "::add-matcher::.github/workflows/matchers/lint-python.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + + - name: Lint Custom + run: script/ci-custom.py -c + working-directory: ${{ env.esphome_directory }} + - name: Lint Python + run: script/lint-python -c + working-directory: ${{ env.esphome_directory }} + + esphome-config: + runs-on: ubuntu-latest + steps: + - name: ⤵️ Check out configuration from GitHub + uses: actions/checkout@v2 + - name: Setup Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + pip install esphome + pip list + esphome version + - name: Write secrets.yaml + shell: bash + run: 'echo -e "wifi_ssid: ssid\nwifi_password: password\nmqtt_host: host\nmqtt_username: username\nmqtt_password: password" > secrets.yaml' + - run: | + esphome -s external_components_source components config esp32-ble-example.yaml + + esphome-compile: + runs-on: ubuntu-latest + needs: [esphome-config] + steps: + - name: ⤵️ Check out configuration from GitHub + uses: actions/checkout@v2 + - name: Cache .esphome + uses: actions/cache@v2 + with: + path: .esphome + key: esphome-compile-esphome-${{ hashFiles('*.yaml') }} + restore-keys: esphome-compile-esphome- + - name: Cache .pioenvs + uses: actions/cache@v2 + with: + path: .pioenvs + key: esphome-compile-pioenvs-${{ hashFiles('*.yaml') }} + restore-keys: esphome-compile-pioenvs- + - name: Set up Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + pip install esphome + pip list + esphome version + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Write secrets.yaml + shell: bash + run: 'echo -e "wifi_ssid: ssid\nwifi_password: password\nmqtt_host: host\nmqtt_username: username\nmqtt_password: password" > secrets.yaml' + - run: | + esphome -s external_components_source components compile esp32-ble-example.yaml diff --git a/.github/workflows/matchers/ci-custom.json b/.github/workflows/matchers/ci-custom.json new file mode 100644 index 0000000..4e1eaff --- /dev/null +++ b/.github/workflows/matchers/ci-custom.json @@ -0,0 +1,16 @@ +{ + "problemMatcher": [ + { + "owner": "ci-custom", + "pattern": [ + { + "regexp": "^ERROR (.*):(\\d+):(\\d+) - (.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/clang-tidy.json b/.github/workflows/matchers/clang-tidy.json new file mode 100644 index 0000000..03e7734 --- /dev/null +++ b/.github/workflows/matchers/clang-tidy.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "clang-tidy", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(error):\\s+(.*) \\[([a-z0-9,\\-]+)\\]\\s*$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/gcc.json b/.github/workflows/matchers/gcc.json new file mode 100644 index 0000000..899239f --- /dev/null +++ b/.github/workflows/matchers/gcc.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "gcc", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/lint-python.json b/.github/workflows/matchers/lint-python.json new file mode 100644 index 0000000..decbe36 --- /dev/null +++ b/.github/workflows/matchers/lint-python.json @@ -0,0 +1,28 @@ +{ + "problemMatcher": [ + { + "owner": "flake8", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+) - ([EFCDNW]\\d{3}.*)$", + "file": 1, + "line": 2, + "message": 3 + } + ] + }, + { + "owner": "pylint", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+) - (\\[[EFCRW]\\d{4}\\(.*\\),.*\\].*)$", + "file": 1, + "line": 2, + "message": 3 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/python.json b/.github/workflows/matchers/python.json new file mode 100644 index 0000000..9c3095c --- /dev/null +++ b/.github/workflows/matchers/python.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "python", + "pattern": [ + { + "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", + "file": 1, + "line": 2 + }, + { + "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", + "message": 2 + } + ] + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0784bce --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.idea/ +secrets.yaml +.esphome/ +**/.pioenvs/ +**/.piolibdeps/ +**/lib/ +**/src/ +**/partitions.csv + +__pycache__/ +*.py[cod] +*$py.class + +local/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..72d46dd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/ambv/black + rev: 22.1.0 + hooks: + - id: black + args: + - --safe + - --quiet + files: ^((components|esphome|script|tests)/.+)?[^/]+\.py$ + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + additional_dependencies: + - flake8-docstrings==1.5.0 + - pydocstyle==5.1.1 + files: ^(components|esphome|tests)/.+\.py$ + - repo: https://github.com/asottile/pyupgrade + rev: v2.31.0 + hooks: + - id: pyupgrade + args: [--py38-plus] + - repo: https://github.com/pocc/pre-commit-hooks + rev: v1.3.5 + hooks: + - id: clang-format diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..b72570f --- /dev/null +++ b/.yamllint @@ -0,0 +1,77 @@ +extends: default + +yaml-files: + - '*.yaml' + - '*.yml' + - '.yamllint' + +ignore: | + /.cache/ + esphome/**/*.pio* + config/automations.yaml + config/known_devices.yaml + config/scenes.yaml + config/google_calendars.yaml + config/custom_components/scheduler + config/custom_components/xiaomi_cloud_map_extractor + config/custom_components/zha_map + config/custom_components/hacs + +rules: + braces: + level: error + min-spaces-inside: 0 + max-spaces-inside: 1 + min-spaces-inside-empty: -1 + max-spaces-inside-empty: -1 + brackets: + level: error + min-spaces-inside: 0 + max-spaces-inside: 0 + min-spaces-inside-empty: -1 + max-spaces-inside-empty: -1 + colons: + level: error + max-spaces-before: 0 + max-spaces-after: 1 + commas: + level: error + max-spaces-before: 0 + min-spaces-after: 1 + max-spaces-after: 1 + comments: + level: error + require-starting-space: true + min-spaces-from-content: 2 + comments-indentation: disable + document-end: + level: error + present: false + document-start: + level: error + present: false + empty-lines: + level: error + max: 2 + max-start: 0 + max-end: 1 + hyphens: + level: error + max-spaces-after: 1 + indentation: + level: error + spaces: 2 + indent-sequences: true + check-multi-line-strings: false + key-duplicates: + level: error + line-length: disable + new-line-at-end-of-file: + level: error + new-lines: + level: error + type: unix + trailing-spaces: + level: error + truthy: + level: error diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..26b112e --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# esphome-votronic + +![GitHub actions](https://github.com/syssi/esphome-votronic/actions/workflows/ci.yaml/badge.svg) +![GitHub stars](https://img.shields.io/github/stars/syssi/esphome-votronic) +![GitHub forks](https://img.shields.io/github/forks/syssi/esphome-votronic) +![GitHub watchers](https://img.shields.io/github/watchers/syssi/esphome-votronic) +[!["Buy Me A Coffee"](https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg)](https://www.buymeacoffee.com/syssi) + +ESPHome component to monitor votronic devices via BLE + +## Supported devices + +* Bluetooth Connector S-BC + * Solar Charger SR/MPP since 2014 (S/N 14Vxx.xxxxx) + * Battery Computer S + Smart Shunt + +## Untested devices + +* Battery Charger VBCS-Triple +* VPC Jupiter + Smart Shunt + +## Requirements + +* [ESPHome 2022.12.0 or higher](https://github.com/esphome/esphome/releases). +* Generic ESP32 board + +## Installation + +You can install this component with [ESPHome external components feature](https://esphome.io/components/external_components.html) like this: +```yaml +external_components: + - source: github://syssi/esphome-votronic@main +``` + +or just use the `esp32-ble-example.yaml` as proof of concept: + +```bash +# Install esphome +pip3 install esphome + +# Clone this external component +git clone https://github.com/syssi/esphome-votronic.git +cd esphome-votronic + +# Create a secrets.yaml containing some setup specific secrets +cat > secrets.yaml < +#include +#include + +#include "esphome/core/log.h" + +namespace esphome { +namespace ble_client { +static const char *const TAG = "ble_client.automation"; + +void BLEWriterClientNode::write(const std::vector &value) { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "Cannot write to BLE characteristic - not connected"); + return; + } else if (this->ble_char_handle_ == 0) { + ESP_LOGW(TAG, "Cannot write to BLE characteristic - characteristic not found"); + return; + } + esp_gatt_write_type_t write_type; + if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) { + write_type = ESP_GATT_WRITE_TYPE_RSP; + ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP"); + } else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) { + write_type = ESP_GATT_WRITE_TYPE_NO_RSP; + ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP"); + } else { + ESP_LOGE(TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str()); + return; + } + ESP_LOGVV(TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str()); + esp_err_t err = + esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->ble_char_handle_, + value.size(), const_cast(value.data()), write_type, ESP_GATT_AUTH_REQ_NONE); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); + } +} + +void BLEWriterClientNode::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_REG_EVT: + break; + case ESP_GATTC_OPEN_EVT: + this->node_state = espbt::ClientState::ESTABLISHED; + ESP_LOGD(TAG, "Connection established with %s", ble_client_->address_str().c_str()); + break; + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + ESP_LOGW("ble_write_action", "Characteristic %s was not found in service %s", + this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str()); + break; + } + this->ble_char_handle_ = chr->handle; + this->char_props_ = chr->properties; + this->node_state = espbt::ClientState::ESTABLISHED; + ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), + ble_client_->address_str().c_str()); + break; + } + case ESP_GATTC_DISCONNECT_EVT: + this->node_state = espbt::ClientState::IDLE; + this->ble_char_handle_ = 0; + ESP_LOGD(TAG, "Disconnected from %s", ble_client_->address_str().c_str()); + break; + default: + break; + } +} + +} // namespace ble_client +} // namespace esphome diff --git a/components/ble_client/automation.h b/components/ble_client/automation.h new file mode 100644 index 0000000..ef38333 --- /dev/null +++ b/components/ble_client/automation.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include + +#include "esphome/core/automation.h" +#include "esphome/components/ble_client/ble_client.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { +class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode { + public: + explicit BLEClientConnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override { + if (event == ESP_GATTC_SEARCH_CMPL_EVT) { + this->node_state = espbt::ClientState::ESTABLISHED; + this->trigger(); + } + } +}; + +class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { + public: + explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override { + if (event == ESP_GATTC_DISCONNECT_EVT && + memcmp(param->disconnect.remote_bda, this->parent_->get_remote_bda(), 6) == 0) + this->trigger(); + if (event == ESP_GATTC_SEARCH_CMPL_EVT) + this->node_state = espbt::ClientState::ESTABLISHED; + } +}; + +class BLEWriterClientNode : public BLEClientNode { + public: + BLEWriterClientNode(BLEClient *ble_client) { + ble_client->register_ble_node(this); + ble_client_ = ble_client; + } + + // Attempts to write the contents of value to char_uuid_. + void write(const std::vector &value); + + void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + + void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + + private: + BLEClient *ble_client_; + int ble_char_handle_ = 0; + esp_gatt_char_prop_t char_props_; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID char_uuid_; +}; + +template class BLEClientWriteAction : public Action, public BLEWriterClientNode { + public: + BLEClientWriteAction(BLEClient *ble_client) : BLEWriterClientNode(ble_client) {} + + void play(Ts... x) override { + if (has_simple_value_) { + return write(this->value_simple_); + } else { + return write(this->value_template_(x...)); + } + } + + void set_value_template(std::function(Ts...)> func) { + this->value_template_ = std::move(func); + has_simple_value_ = false; + } + + void set_value_simple(const std::vector &value) { + this->value_simple_ = value; + has_simple_value_ = true; + } + + private: + bool has_simple_value_ = true; + std::vector value_simple_; + std::function(Ts...)> value_template_{}; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/components/ble_client/ble_client.cpp b/components/ble_client/ble_client.cpp new file mode 100644 index 0000000..f3a9f01 --- /dev/null +++ b/components/ble_client/ble_client.cpp @@ -0,0 +1,95 @@ +#include "ble_client.h" +#include "esphome/components/esp32_ble_client/ble_client_base.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *const TAG = "ble_client"; + +void BLEClient::setup() { + BLEClientBase::setup(); + this->enabled = true; +} + +void BLEClient::loop() { + BLEClientBase::loop(); + for (auto *node : this->nodes_) + node->loop(); +} + +void BLEClient::dump_config() { + ESP_LOGCONFIG(TAG, "BLE Client:"); + ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str()); +} + +bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { + if (!this->enabled) + return false; + return BLEClientBase::parse_device(device); +} + +void BLEClient::set_enabled(bool enabled) { + if (enabled == this->enabled) + return; + if (!enabled && this->state() != espbt::ClientState::IDLE) { + ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); + auto ret = esp_ble_gattc_close(this->gattc_if_, this->conn_id_); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s status=%d", this->address_str().c_str(), ret); + } + } + this->enabled = enabled; +} + +bool BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, + esp_ble_gattc_cb_param_t *param) { + bool all_established = this->all_nodes_established_(); + + if (!BLEClientBase::gattc_event_handler(event, esp_gattc_if, param)) + return false; + + for (auto *node : this->nodes_) + node->gattc_event_handler(event, esp_gattc_if, param); + + // Delete characteristics after clients have used them to save RAM. + if (!all_established && this->all_nodes_established_()) { + for (auto &svc : this->services_) + delete svc; // NOLINT(cppcoreguidelines-owning-memory) + this->services_.clear(); + } + return true; +} + +void BLEClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + BLEClientBase::gap_event_handler(event, param); + + for (auto *node : this->nodes_) + node->gap_event_handler(event, param); +} + +void BLEClient::set_state(espbt::ClientState state) { + BLEClientBase::set_state(state); + for (auto &node : nodes_) + node->node_state = state; +} + +bool BLEClient::all_nodes_established_() { + if (this->state() != espbt::ClientState::ESTABLISHED) + return false; + for (auto &node : nodes_) { + if (node->node_state != espbt::ClientState::ESTABLISHED) + return false; + } + return true; +} + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/components/ble_client/ble_client.h b/components/ble_client/ble_client.h new file mode 100644 index 0000000..ceca94c --- /dev/null +++ b/components/ble_client/ble_client.h @@ -0,0 +1,81 @@ +#pragma once + +#include "esphome/components/esp32_ble_client/ble_client_base.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#ifdef USE_ESP32 + +#include +#include +#include +#include +#include +#include +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +using namespace esp32_ble_client; + +class BLEClient; + +class BLEClientNode { + public: + virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) = 0; + virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {} + virtual void loop() {} + void set_address(uint64_t address) { address_ = address; } + espbt::ESPBTClient *client; + // This should be transitioned to Established once the node no longer needs + // the services/descriptors/characteristics of the parent client. This will + // allow some memory to be freed. + espbt::ClientState node_state; + + BLEClient *parent() { return this->parent_; } + void set_ble_client_parent(BLEClient *parent) { this->parent_ = parent; } + + protected: + BLEClient *parent_; + uint64_t address_; +}; + +class BLEClient : public BLEClientBase { + public: + void setup() override; + void dump_config() override; + void loop() override; + + bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + bool parse_device(const espbt::ESPBTDevice &device) override; + + void set_enabled(bool enabled); + + void register_ble_node(BLEClientNode *node) { + node->client = this; + node->set_ble_client_parent(this); + this->nodes_.push_back(node); + } + + bool enabled; + + void set_state(espbt::ClientState state) override; + + protected: + bool all_nodes_established_(); + + std::vector nodes_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/components/ble_client/output/__init__.py b/components/ble_client/output/__init__.py new file mode 100644 index 0000000..fd847d8 --- /dev/null +++ b/components/ble_client/output/__init__.py @@ -0,0 +1,68 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ble_client, esp32_ble_tracker, output +from esphome.const import CONF_CHARACTERISTIC_UUID, CONF_ID, CONF_SERVICE_UUID + +from .. import ble_client_ns + +DEPENDENCIES = ["ble_client"] + +CONF_REQUIRE_RESPONSE = "require_response" + +BLEBinaryOutput = ble_client_ns.class_( + "BLEBinaryOutput", output.BinaryOutput, ble_client.BLEClientNode, cg.Component +) + +CONFIG_SCHEMA = cv.All( + output.BINARY_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(BLEBinaryOutput), + cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_REQUIRE_RESPONSE, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(ble_client.BLE_CLIENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add( + var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) + + if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_char_uuid16( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_char_uuid32( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_CHARACTERISTIC_UUID] + ) + cg.add(var.set_char_uuid128(uuid128)) + cg.add(var.set_require_response(config[CONF_REQUIRE_RESPONSE])) + yield output.register_output(var, config) + yield ble_client.register_ble_node(var, config) + yield cg.register_component(var, config) diff --git a/components/ble_client/output/ble_binary_output.cpp b/components/ble_client/output/ble_binary_output.cpp new file mode 100644 index 0000000..6709803 --- /dev/null +++ b/components/ble_client/output/ble_binary_output.cpp @@ -0,0 +1,75 @@ +#include "ble_binary_output.h" +#include "esphome/core/log.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 +namespace esphome { +namespace ble_client { + +static const char *const TAG = "ble_binary_output"; + +void BLEBinaryOutput::dump_config() { + ESP_LOGCONFIG(TAG, "BLE Binary Output:"); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent_->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Characteristic UUID: %s", this->char_uuid_.to_string().c_str()); + LOG_BINARY_OUTPUT(this); +} + +void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: + this->client_state_ = espbt::ClientState::ESTABLISHED; + ESP_LOGW(TAG, "[%s] Connected successfully!", this->char_uuid_.to_string().c_str()); + break; + case ESP_GATTC_DISCONNECT_EVT: + ESP_LOGW(TAG, "[%s] Disconnected", this->char_uuid_.to_string().c_str()); + this->client_state_ = espbt::ClientState::IDLE; + break; + case ESP_GATTC_WRITE_CHAR_EVT: { + if (param->write.status == 0) { + break; + } + + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] Characteristic not found.", this->char_uuid_.to_string().c_str()); + break; + } + if (param->write.handle == chr->handle) { + ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_string().c_str(), param->write.status); + } + break; + } + default: + break; + } +} + +void BLEBinaryOutput::write_state(bool state) { + if (this->client_state_ != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", + this->char_uuid_.to_string().c_str()); + return; + } + + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] Characteristic not found. State update can not be written.", + this->char_uuid_.to_string().c_str()); + return; + } + + uint8_t state_as_uint = (uint8_t) state; + ESP_LOGV(TAG, "[%s] Write State: %d", this->char_uuid_.to_string().c_str(), state_as_uint); + if (this->require_response_) { + chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_RSP); + } else { + chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_NO_RSP); + } +} + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/components/ble_client/output/ble_binary_output.h b/components/ble_client/output/ble_binary_output.h new file mode 100644 index 0000000..83eabcf --- /dev/null +++ b/components/ble_client/output/ble_binary_output.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/output/binary_output.h" + +#ifdef USE_ESP32 +#include +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, public Component { + public: + void dump_config() override; + void loop() override {} + float get_setup_priority() const override { return setup_priority::DATA; } + void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void set_require_response(bool response) { this->require_response_ = response; } + + protected: + void write_state(bool state) override; + bool require_response_; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID char_uuid_; + espbt::ClientState client_state_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/components/ble_client/sensor/__init__.py b/components/ble_client/sensor/__init__.py new file mode 100644 index 0000000..c9bf299 --- /dev/null +++ b/components/ble_client/sensor/__init__.py @@ -0,0 +1,174 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client, esp32_ble_tracker +from esphome.const import ( + CONF_CHARACTERISTIC_UUID, + CONF_LAMBDA, + CONF_TRIGGER_ID, + CONF_TYPE, + CONF_SERVICE_UUID, + DEVICE_CLASS_SIGNAL_STRENGTH, + STATE_CLASS_MEASUREMENT, + UNIT_DECIBEL_MILLIWATT, +) +from esphome import automation +from .. import ble_client_ns + +DEPENDENCIES = ["ble_client"] + +CONF_DESCRIPTOR_UUID = "descriptor_uuid" + +CONF_NOTIFY = "notify" +CONF_ON_NOTIFY = "on_notify" +TYPE_CHARACTERISTIC = "characteristic" +TYPE_RSSI = "rssi" + +adv_data_t = cg.std_vector.template(cg.uint8) +adv_data_t_const_ref = adv_data_t.operator("ref").operator("const") + +BLESensor = ble_client_ns.class_( + "BLESensor", sensor.Sensor, cg.PollingComponent, ble_client.BLEClientNode +) +BLESensorNotifyTrigger = ble_client_ns.class_( + "BLESensorNotifyTrigger", automation.Trigger.template(cg.float_) +) + +BLEClientRssiSensor = ble_client_ns.class_( + "BLEClientRSSISensor", sensor.Sensor, cg.PollingComponent, ble_client.BLEClientNode +) + + +def checkType(value): + if CONF_TYPE not in value and CONF_SERVICE_UUID in value: + raise cv.Invalid( + "Looks like you're trying to create a ble characteristic sensor. Please add `type: characteristic` to your sensor config." + ) + return value + + +CONFIG_SCHEMA = cv.All( + checkType, + cv.typed_schema( + { + TYPE_CHARACTERISTIC: sensor.sensor_schema( + BLESensor, + accuracy_decimals=0, + ) + .extend(cv.polling_component_schema("60s")) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend( + { + cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_NOTIFY, default=False): cv.boolean, + cv.Optional(CONF_ON_NOTIFY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLESensorNotifyTrigger + ), + } + ), + } + ), + TYPE_RSSI: sensor.sensor_schema( + BLEClientRssiSensor, + accuracy_decimals=0, + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(ble_client.BLE_CLIENT_SCHEMA), + }, + lower=True, + ), +) + + +async def rssi_sensor_to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await ble_client.register_ble_node(var, config) + + +async def characteristic_sensor_to_code(config): + var = await sensor.new_sensor(config) + if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add( + var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) + + if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_char_uuid16( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_char_uuid32( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_CHARACTERISTIC_UUID] + ) + cg.add(var.set_char_uuid128(uuid128)) + + if CONF_DESCRIPTOR_UUID in config: + if len(config[CONF_DESCRIPTOR_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_descr_uuid16( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_descr_uuid32( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_DESCRIPTOR_UUID] + ) + cg.add(var.set_descr_uuid128(uuid128)) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(adv_data_t_const_ref, "x")], return_type=cg.float_ + ) + cg.add(var.set_data_to_value(lambda_)) + + await cg.register_component(var, config) + await ble_client.register_ble_node(var, config) + cg.add(var.set_enable_notify(config[CONF_NOTIFY])) + for conf in config.get(CONF_ON_NOTIFY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await ble_client.register_ble_node(trigger, config) + await automation.build_automation(trigger, [(float, "x")], conf) + + +async def to_code(config): + if config[CONF_TYPE] == TYPE_RSSI: + await rssi_sensor_to_code(config) + elif config[CONF_TYPE] == TYPE_CHARACTERISTIC: + await characteristic_sensor_to_code(config) diff --git a/components/ble_client/sensor/automation.h b/components/ble_client/sensor/automation.h new file mode 100644 index 0000000..d830165 --- /dev/null +++ b/components/ble_client/sensor/automation.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/ble_client/sensor/ble_sensor.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { + +class BLESensorNotifyTrigger : public Trigger, public BLESensor { + public: + explicit BLESensorNotifyTrigger(BLESensor *sensor) { sensor_ = sensor; } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override { + switch (event) { + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->sensor_->node_state = espbt::ClientState::ESTABLISHED; + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->sensor_->parent()->get_conn_id() || + param->notify.handle != this->sensor_->handle) + break; + this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len)); + } + default: + break; + } + } + + protected: + BLESensor *sensor_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/components/ble_client/sensor/ble_rssi_sensor.cpp b/components/ble_client/sensor/ble_rssi_sensor.cpp new file mode 100644 index 0000000..13e51ed --- /dev/null +++ b/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -0,0 +1,78 @@ +#include "ble_rssi_sensor.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *const TAG = "ble_rssi_sensor"; + +void BLEClientRSSISensor::loop() {} + +void BLEClientRSSISensor::dump_config() { + LOG_SENSOR("", "BLE Client RSSI Sensor", this); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); + LOG_UPDATE_INTERVAL(this); +} + +void BLEClientRSSISensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + if (param->open.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str()); + break; + } + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); + this->status_set_warning(); + this->publish_state(NAN); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: + this->node_state = espbt::ClientState::ESTABLISHED; + break; + default: + break; + } +} + +void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + // server response on RSSI request: + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) { + int8_t rssi = param->read_rssi_cmpl.rssi; + ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi); + this->publish_state(rssi); + } + break; + default: + break; + } +} + +void BLEClientRSSISensor::update() { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str()); + return; + } + + ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str().c_str()); + auto status = esp_ble_gap_read_rssi(this->parent()->get_remote_bda()); + if (status != ESP_OK) { + ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str().c_str(), status); + this->status_set_warning(); + this->publish_state(NAN); + } +} + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/components/ble_client/sensor/ble_rssi_sensor.h b/components/ble_client/sensor/ble_rssi_sensor.h new file mode 100644 index 0000000..028df83 --- /dev/null +++ b/components/ble_client/sensor/ble_rssi_sensor.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" + +#ifdef USE_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, public BLEClientNode { + public: + void loop() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/components/ble_client/sensor/ble_sensor.cpp b/components/ble_client/sensor/ble_sensor.cpp new file mode 100644 index 0000000..a05efad --- /dev/null +++ b/components/ble_client/sensor/ble_sensor.cpp @@ -0,0 +1,136 @@ +#include "ble_sensor.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *const TAG = "ble_sensor"; + +void BLESensor::loop() {} + +void BLESensor::dump_config() { + LOG_SENSOR("", "BLE Sensor", this); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Characteristic UUID: %s", this->char_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Descriptor UUID : %s", this->descr_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Notifications : %s", YESNO(this->notify_)); + LOG_UPDATE_INTERVAL(this); +} + +void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + if (param->open.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str()); + break; + } + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); + this->status_set_warning(); + this->publish_state(NAN); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->handle = 0; + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), + this->char_uuid_.to_string().c_str()); + break; + } + this->handle = chr->handle; + if (this->descr_uuid_.get_uuid().len > 0) { + auto *descr = chr->get_descriptor(this->descr_uuid_); + if (descr == nullptr) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "No sensor descriptor found at service %s char %s descr %s", + this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), + this->descr_uuid_.to_string().c_str()); + break; + } + this->handle = descr->handle; + } + if (this->notify_) { + auto status = esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), + this->parent()->get_remote_bda(), chr->handle); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); + } + } else { + this->node_state = espbt::ClientState::ESTABLISHED; + } + break; + } + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->parent()->get_conn_id()) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } + if (param->read.handle == this->handle) { + this->status_clear_warning(); + this->publish_state(this->parse_data_(param->read.value, param->read.value_len)); + } + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle) + break; + ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), + param->notify.handle, param->notify.value[0]); + this->publish_state(this->parse_data_(param->notify.value, param->notify.value_len)); + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + break; + } + default: + break; + } +} + +float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) { + if (this->data_to_value_func_.has_value()) { + std::vector data(value, value + value_len); + return (*this->data_to_value_func_)(data); + } else { + return value[0]; + } +} + +void BLESensor::update() { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str()); + return; + } + if (this->handle == 0) { + ESP_LOGW(TAG, "[%s] Cannot poll, no service or characteristic found", this->get_name().c_str()); + return; + } + + auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle, + ESP_GATT_AUTH_REQ_NONE); + if (status) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "[%s] Error sending read request for sensor, status=%d", this->get_name().c_str(), status); + } +} + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/components/ble_client/sensor/ble_sensor.h b/components/ble_client/sensor/ble_sensor.h new file mode 100644 index 0000000..b11a010 --- /dev/null +++ b/components/ble_client/sensor/ble_sensor.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" + +#include + +#ifdef USE_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +using data_to_value_t = std::function)>; + +class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode { + public: + void loop() override; + void update() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_data_to_value(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; } + void set_enable_notify(bool notify) { this->notify_ = notify; } + uint16_t handle; + + protected: + float parse_data_(uint8_t *value, uint16_t value_len); + optional data_to_value_func_{}; + bool notify_; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID char_uuid_; + espbt::ESPBTUUID descr_uuid_; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/components/ble_client/switch/__init__.py b/components/ble_client/switch/__init__.py new file mode 100644 index 0000000..2304d65 --- /dev/null +++ b/components/ble_client/switch/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch, ble_client +from esphome.const import ICON_BLUETOOTH +from .. import ble_client_ns + +BLEClientSwitch = ble_client_ns.class_( + "BLEClientSwitch", switch.Switch, cg.Component, ble_client.BLEClientNode +) + +CONFIG_SCHEMA = ( + switch.switch_schema(BLEClientSwitch, icon=ICON_BLUETOOTH, block_inverted=True) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await switch.new_switch(config) + await cg.register_component(var, config) + await ble_client.register_ble_node(var, config) diff --git a/components/ble_client/switch/ble_switch.cpp b/components/ble_client/switch/ble_switch.cpp new file mode 100644 index 0000000..6de5252 --- /dev/null +++ b/components/ble_client/switch/ble_switch.cpp @@ -0,0 +1,39 @@ +#include "ble_switch.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *const TAG = "ble_switch"; + +void BLEClientSwitch::write_state(bool state) { + this->parent_->set_enabled(state); + this->publish_state(state); +} + +void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_REG_EVT: + this->publish_state(this->parent_->enabled); + break; + case ESP_GATTC_OPEN_EVT: + this->node_state = espbt::ClientState::ESTABLISHED; + break; + case ESP_GATTC_DISCONNECT_EVT: + this->node_state = espbt::ClientState::IDLE; + this->publish_state(this->parent_->enabled); + break; + default: + break; + } +} + +void BLEClientSwitch::dump_config() { LOG_SWITCH("", "BLE Client Switch", this); } + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/components/ble_client/switch/ble_switch.h b/components/ble_client/switch/ble_switch.h new file mode 100644 index 0000000..2e19c8a --- /dev/null +++ b/components/ble_client/switch/ble_switch.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/switch/switch.h" + +#ifdef USE_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClientSwitch : public switch_::Switch, public Component, public BLEClientNode { + public: + void dump_config() override; + void loop() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void write_state(bool state) override; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/components/ble_client/text_sensor/__init__.py b/components/ble_client/text_sensor/__init__.py new file mode 100644 index 0000000..66f00c5 --- /dev/null +++ b/components/ble_client/text_sensor/__init__.py @@ -0,0 +1,121 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor, ble_client, esp32_ble_tracker +from esphome.const import ( + CONF_CHARACTERISTIC_UUID, + CONF_ID, + CONF_TRIGGER_ID, + CONF_SERVICE_UUID, +) +from esphome import automation +from .. import ble_client_ns + +DEPENDENCIES = ["ble_client"] + +CONF_DESCRIPTOR_UUID = "descriptor_uuid" + +CONF_NOTIFY = "notify" +CONF_ON_NOTIFY = "on_notify" + +adv_data_t = cg.std_vector.template(cg.uint8) +adv_data_t_const_ref = adv_data_t.operator("ref").operator("const") + +BLETextSensor = ble_client_ns.class_( + "BLETextSensor", + text_sensor.TextSensor, + cg.PollingComponent, + ble_client.BLEClientNode, +) +BLETextSensorNotifyTrigger = ble_client_ns.class_( + "BLETextSensorNotifyTrigger", automation.Trigger.template(cg.std_string) +) + +CONFIG_SCHEMA = cv.All( + text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BLETextSensor), + cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_NOTIFY, default=False): cv.boolean, + cv.Optional(CONF_ON_NOTIFY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLETextSensorNotifyTrigger + ), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(ble_client.BLE_CLIENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add( + var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) + + if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_char_uuid16( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_char_uuid32( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_CHARACTERISTIC_UUID] + ) + cg.add(var.set_char_uuid128(uuid128)) + + if CONF_DESCRIPTOR_UUID in config: + if len(config[CONF_DESCRIPTOR_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_descr_uuid16( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_descr_uuid32( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_DESCRIPTOR_UUID] + ) + cg.add(var.set_descr_uuid128(uuid128)) + + await cg.register_component(var, config) + await ble_client.register_ble_node(var, config) + cg.add(var.set_enable_notify(config[CONF_NOTIFY])) + await text_sensor.register_text_sensor(var, config) + for conf in config.get(CONF_ON_NOTIFY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await ble_client.register_ble_node(trigger, config) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) diff --git a/components/ble_client/text_sensor/automation.h b/components/ble_client/text_sensor/automation.h new file mode 100644 index 0000000..c504c35 --- /dev/null +++ b/components/ble_client/text_sensor/automation.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/ble_client/text_sensor/ble_text_sensor.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { + +class BLETextSensorNotifyTrigger : public Trigger, public BLETextSensor { + public: + explicit BLETextSensorNotifyTrigger(BLETextSensor *sensor) { sensor_ = sensor; } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override { + switch (event) { + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->sensor_->node_state = espbt::ClientState::ESTABLISHED; + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->sensor_->parent()->get_conn_id() || + param->notify.handle != this->sensor_->handle) + break; + this->trigger(this->sensor_->parse_data(param->notify.value, param->notify.value_len)); + } + default: + break; + } + } + + protected: + BLETextSensor *sensor_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/components/ble_client/text_sensor/ble_text_sensor.cpp b/components/ble_client/text_sensor/ble_text_sensor.cpp new file mode 100644 index 0000000..1a30459 --- /dev/null +++ b/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -0,0 +1,135 @@ +#include "ble_text_sensor.h" + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *const TAG = "ble_text_sensor"; + +static const std::string EMPTY = ""; + +void BLETextSensor::loop() {} + +void BLETextSensor::dump_config() { + LOG_TEXT_SENSOR("", "BLE Text Sensor", this); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Characteristic UUID: %s", this->char_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Descriptor UUID : %s", this->descr_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Notifications : %s", YESNO(this->notify_)); + LOG_UPDATE_INTERVAL(this); +} + +void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + if (param->open.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str()); + break; + } + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); + this->status_set_warning(); + this->publish_state(EMPTY); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->handle = 0; + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + this->status_set_warning(); + this->publish_state(EMPTY); + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), + this->char_uuid_.to_string().c_str()); + break; + } + this->handle = chr->handle; + if (this->descr_uuid_.get_uuid().len > 0) { + auto *descr = chr->get_descriptor(this->descr_uuid_); + if (descr == nullptr) { + this->status_set_warning(); + this->publish_state(EMPTY); + ESP_LOGW(TAG, "No sensor descriptor found at service %s char %s descr %s", + this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), + this->descr_uuid_.to_string().c_str()); + break; + } + this->handle = descr->handle; + } + if (this->notify_) { + auto status = esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), + this->parent()->get_remote_bda(), chr->handle); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); + } + } else { + this->node_state = espbt::ClientState::ESTABLISHED; + } + break; + } + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->parent()->get_conn_id()) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } + if (param->read.handle == this->handle) { + this->status_clear_warning(); + this->publish_state(this->parse_data(param->read.value, param->read.value_len)); + } + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle) + break; + ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), + param->notify.handle, param->notify.value[0]); + this->publish_state(this->parse_data(param->notify.value, param->notify.value_len)); + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + break; + } + default: + break; + } +} + +std::string BLETextSensor::parse_data(uint8_t *value, uint16_t value_len) { + std::string text(value, value + value_len); + return text; +} + +void BLETextSensor::update() { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str()); + return; + } + if (this->handle == 0) { + ESP_LOGW(TAG, "[%s] Cannot poll, no service or characteristic found", this->get_name().c_str()); + return; + } + + auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle, + ESP_GATT_AUTH_REQ_NONE); + if (status) { + this->status_set_warning(); + this->publish_state(EMPTY); + ESP_LOGW(TAG, "[%s] Error sending read request for sensor, status=%d", this->get_name().c_str(), status); + } +} + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/components/ble_client/text_sensor/ble_text_sensor.h b/components/ble_client/text_sensor/ble_text_sensor.h new file mode 100644 index 0000000..cb34043 --- /dev/null +++ b/components/ble_client/text_sensor/ble_text_sensor.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/text_sensor/text_sensor.h" + +#ifdef USE_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, public BLEClientNode { + public: + void loop() override; + void update() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_enable_notify(bool notify) { this->notify_ = notify; } + std::string parse_data(uint8_t *value, uint16_t value_len); + uint16_t handle; + + protected: + bool notify_; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID char_uuid_; + espbt::ESPBTUUID descr_uuid_; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/components/esp32_ble_client/__init__.py b/components/esp32_ble_client/__init__.py new file mode 100644 index 0000000..94a5576 --- /dev/null +++ b/components/esp32_ble_client/__init__.py @@ -0,0 +1,12 @@ +import esphome.codegen as cg + +from esphome.components import esp32_ble_tracker + +AUTO_LOAD = ["esp32_ble_tracker"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["esp32"] + +esp32_ble_client_ns = cg.esphome_ns.namespace("esp32_ble_client") +BLEClientBase = esp32_ble_client_ns.class_( + "BLEClientBase", esp32_ble_tracker.ESPBTClient, cg.Component +) diff --git a/components/esp32_ble_client/ble_characteristic.cpp b/components/esp32_ble_client/ble_characteristic.cpp new file mode 100644 index 0000000..2fd7fe9 --- /dev/null +++ b/components/esp32_ble_client/ble_characteristic.cpp @@ -0,0 +1,99 @@ +#include "ble_characteristic.h" +#include "ble_client_base.h" +#include "ble_service.h" + +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_client { + +static const char *const TAG = "esp32_ble_client"; + +BLECharacteristic::~BLECharacteristic() { + for (auto &desc : this->descriptors) + delete desc; // NOLINT(cppcoreguidelines-owning-memory) +} + +void BLECharacteristic::release_descriptors() { + this->parsed = false; + for (auto &desc : this->descriptors) + delete desc; // NOLINT(cppcoreguidelines-owning-memory) + this->descriptors.clear(); +} + +void BLECharacteristic::parse_descriptors() { + this->parsed = true; + uint16_t offset = 0; + esp_gattc_descr_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = + esp_ble_gattc_get_all_descr(this->service->client->get_gattc_if(), this->service->client->get_conn_id(), + this->handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", + this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status); + break; + } + if (count == 0) { + break; + } + + BLEDescriptor *desc = new BLEDescriptor(); // NOLINT(cppcoreguidelines-owning-memory) + desc->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); + desc->handle = result.handle; + desc->characteristic = this; + this->descriptors.push_back(desc); + ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(), + this->service->client->address_str().c_str(), desc->uuid.to_string().c_str(), desc->handle); + offset++; + } +} + +BLEDescriptor *BLECharacteristic::get_descriptor(espbt::ESPBTUUID uuid) { + if (!this->parsed) + this->parse_descriptors(); + for (auto &desc : this->descriptors) { + if (desc->uuid == uuid) + return desc; + } + return nullptr; +} +BLEDescriptor *BLECharacteristic::get_descriptor(uint16_t uuid) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(uuid)); +} +BLEDescriptor *BLECharacteristic::get_descriptor_by_handle(uint16_t handle) { + if (!this->parsed) + this->parse_descriptors(); + for (auto &desc : this->descriptors) { + if (desc->handle == handle) + return desc; + } + return nullptr; +} + +esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type) { + auto *client = this->service->client; + auto status = esp_ble_gattc_write_char(client->get_gattc_if(), client->get_conn_id(), this->handle, new_val_size, + new_val, write_type, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "[%d] [%s] Error sending write value to BLE gattc server, status=%d", + this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status); + } + return status; +} + +esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size) { + return write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP); +} + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/components/esp32_ble_client/ble_characteristic.h b/components/esp32_ble_client/ble_characteristic.h new file mode 100644 index 0000000..a014788 --- /dev/null +++ b/components/esp32_ble_client/ble_characteristic.h @@ -0,0 +1,39 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#include "ble_descriptor.h" + +#include + +namespace esphome { +namespace esp32_ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEService; + +class BLECharacteristic { + public: + ~BLECharacteristic(); + bool parsed = false; + espbt::ESPBTUUID uuid; + uint16_t handle; + esp_gatt_char_prop_t properties; + std::vector descriptors; + void parse_descriptors(); + void release_descriptors(); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid); + BLEDescriptor *get_descriptor(uint16_t uuid); + BLEDescriptor *get_descriptor_by_handle(uint16_t handle); + esp_err_t write_value(uint8_t *new_val, int16_t new_val_size); + esp_err_t write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type); + BLEService *service; +}; + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/components/esp32_ble_client/ble_client_base.cpp b/components/esp32_ble_client/ble_client_base.cpp new file mode 100644 index 0000000..4c689b3 --- /dev/null +++ b/components/esp32_ble_client/ble_client_base.cpp @@ -0,0 +1,439 @@ +#include "ble_client_base.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_client { + +static const char *const TAG = "esp32_ble_client"; +static const esp_bt_uuid_t NOTIFY_DESC_UUID = { + .len = ESP_UUID_LEN_16, + .uuid = + { + .uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG, + }, +}; + +void BLEClientBase::setup() { + static uint8_t connection_index = 0; + this->connection_index_ = connection_index++; + + auto ret = esp_ble_gattc_app_register(this->app_id); + if (ret) { + ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); + this->mark_failed(); + } + this->set_state(espbt::ClientState::IDLE); +} + +void BLEClientBase::loop() { + // READY_TO_CONNECT means we have discovered the device + // and the scanner has been stopped by the tracker. + if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { + this->connect(); + } +} + +float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } + +bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) + return false; + if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING) + return false; + + ESP_LOGD(TAG, "[%d] [%s] Found device", this->connection_index_, this->address_str_.c_str()); + this->set_state(espbt::ClientState::DISCOVERED); + + auto addr = device.address_uint64(); + this->remote_bda_[0] = (addr >> 40) & 0xFF; + this->remote_bda_[1] = (addr >> 32) & 0xFF; + this->remote_bda_[2] = (addr >> 24) & 0xFF; + this->remote_bda_[3] = (addr >> 16) & 0xFF; + this->remote_bda_[4] = (addr >> 8) & 0xFF; + this->remote_bda_[5] = (addr >> 0) & 0xFF; + this->remote_addr_type_ = device.get_address_type(); + return true; +} + +void BLEClientBase::connect() { + ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(), + this->remote_addr_type_); + auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true); + if (ret) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(), + ret); + this->set_state(espbt::ClientState::IDLE); + } else { + this->set_state(espbt::ClientState::CONNECTING); + } +} + +void BLEClientBase::disconnect() { + if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) + return; + ESP_LOGI(TAG, "[%d] [%s] Disconnecting.", this->connection_index_, this->address_str_.c_str()); + auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_close error, err=%d", this->connection_index_, this->address_str_.c_str(), + err); + } + + if (this->state_ == espbt::ClientState::SEARCHING || this->state_ == espbt::ClientState::READY_TO_CONNECT || + this->state_ == espbt::ClientState::DISCOVERED) { + this->set_address(0); + this->set_state(espbt::ClientState::IDLE); + } else { + this->set_state(espbt::ClientState::DISCONNECTING); + } +} + +void BLEClientBase::release_services() { + for (auto &svc : this->services_) + delete svc; // NOLINT(cppcoreguidelines-owning-memory) + this->services_.clear(); +#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH + esp_ble_gattc_cache_clean(this->remote_bda_); +#endif +} + +bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, + esp_ble_gattc_cb_param_t *param) { + if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) + return false; + if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_) + return false; + + ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_, + this->address_str_.c_str(), event, esp_gattc_if); + + switch (event) { + case ESP_GATTC_REG_EVT: { + if (param->reg.status == ESP_GATT_OK) { + ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_.c_str(), + this->app_id); + this->gattc_if_ = esp_gattc_if; + } else { + ESP_LOGE(TAG, "[%d] [%s] gattc app registration failed id=%d code=%d", this->connection_index_, + this->address_str_.c_str(), param->reg.app_id, param->reg.status); + } + break; + } + case ESP_GATTC_OPEN_EVT: { + ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT", this->connection_index_, this->address_str_.c_str()); + this->conn_id_ = param->open.conn_id; + this->service_count_ = 0; + if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { + ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(), + param->open.status); + this->set_state(espbt::ClientState::IDLE); + break; + } + auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id); + if (ret) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_, + this->address_str_.c_str(), ret); + } + if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { + this->set_state(espbt::ClientState::CONNECTED); + this->state_ = espbt::ClientState::ESTABLISHED; + break; + } + esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); + break; + } + case ESP_GATTC_CFG_MTU_EVT: { + if (param->cfg_mtu.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, + this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status); + this->set_state(espbt::ClientState::IDLE); + break; + } + ESP_LOGV(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(), + param->cfg_mtu.status, param->cfg_mtu.mtu); + this->mtu_ = param->cfg_mtu.mtu; + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + if (memcmp(param->disconnect.remote_bda, this->remote_bda_, 6) != 0) + return false; + ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_, + this->address_str_.c_str(), param->disconnect.reason); + this->release_services(); + this->set_state(espbt::ClientState::IDLE); + break; + } + case ESP_GATTC_SEARCH_RES_EVT: { + this->service_count_++; + if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + // V3 clients don't need services initialized since + // they only request by handle after receiving the services. + break; + } + BLEService *ble_service = new BLEService(); // NOLINT(cppcoreguidelines-owning-memory) + ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid); + ble_service->start_handle = param->search_res.start_handle; + ble_service->end_handle = param->search_res.end_handle; + ble_service->client = this; + this->services_.push_back(ble_service); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_SEARCH_CMPL_EVT", this->connection_index_, this->address_str_.c_str()); + for (auto &svc : this->services_) { + ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(), + svc->uuid.to_string().c_str()); + ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, + this->address_str_.c_str(), svc->start_handle, svc->end_handle); + } + this->set_state(espbt::ClientState::CONNECTED); + this->state_ = espbt::ClientState::ESTABLISHED; + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || + this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + // Client is responsible for flipping the descriptor value + // when using the cache + break; + } + esp_gattc_descr_elem_t desc_result; + uint16_t count = 1; + esp_gatt_status_t descr_status = + esp_ble_gattc_get_descr_by_char_handle(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle, + NOTIFY_DESC_UUID, &desc_result, &count); + if (descr_status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_, + this->address_str_.c_str(), descr_status); + break; + } + esp_gattc_char_elem_t char_result; + esp_gatt_status_t char_status = + esp_ble_gattc_get_all_char(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle, + param->reg_for_notify.handle, &char_result, &count, 0); + if (char_status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, + this->address_str_.c_str(), char_status); + break; + } + + /* + 1 = notify + 2 = indicate + */ + uint16_t notify_en = char_result.properties & ESP_GATT_CHAR_PROP_BIT_NOTIFY ? 1 : 2; + esp_err_t status = + esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en), + (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_, + this->address_str_.c_str(), status); + } + break; + } + + default: + break; + } + return true; +} + +void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + esp_bd_addr_t bd_addr; + memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); + if (memcmp(bd_addr, this->remote_bda_, sizeof(esp_bd_addr_t)) != 0) + return; + switch (event) { + // This event is sent by the server when it requests security + case ESP_GAP_BLE_SEC_REQ_EVT: + ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event); + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + break; + // This event is sent once authentication has completed + case ESP_GAP_BLE_AUTH_CMPL_EVT: + ESP_LOGV(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(), + format_hex(bd_addr, 6).c_str()); + if (!param->ble_security.auth_cmpl.success) { + ESP_LOGE(TAG, "[%d] [%s] auth fail reason = 0x%x", this->connection_index_, this->address_str_.c_str(), + param->ble_security.auth_cmpl.fail_reason); + } else { + ESP_LOGI(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_, + this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type, + param->ble_security.auth_cmpl.auth_mode); + } + break; + case ESP_GAP_BLE_PASSKEY_REQ_EVT: + // Call the following function to input the passkey which is displayed on the remote device + ESP_LOGD(TAG, "[%d] [%s] ESP_GAP_BLE_PASSKEY_REQ_EVT: Authenticating with passkey", this->connection_index_, + this->address_str_.c_str()); + esp_ble_passkey_reply(this->remote_bda_, true, this->pin_code_); + break; + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: + // The app will receive this event when the IO has Output capability and the peer device IO has Input capability. + // Show the passkey number to the user to input it in the peer device. + ESP_LOGD(TAG, "[%d] [%s] ESP_GAP_BLE_PASSKEY_NOTIF_EVT: Passkey: %06d (0x%x)", this->connection_index_, + this->address_str_.c_str(), param->ble_security.key_notif.passkey, + param->ble_security.key_notif.passkey); + break; + case ESP_GAP_BLE_NC_REQ_EVT: + // The app will receive this evt when the IO has DisplayYesNO capability and the peer device IO also has + // DisplayYesNo capability. Show the passkey number to the user to confirm it with the number displayed by peer + // device. + ESP_LOGW(TAG, "[%d] [%s] ESP_GAP_BLE_NC_REQ_EVT: Passkey: %06d (0x%x) (Not implemented: esp_ble_confirm_reply)", + this->connection_index_, this->address_str_.c_str(), param->ble_security.key_notif.passkey, + param->ble_security.key_notif.passkey); + // We probably want to something like this, however it has not yet been tested/verified. + // esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true); + break; + case ESP_GAP_BLE_OOB_REQ_EVT: { + ESP_LOGW(TAG, "[%d] [%s] ESP_GAP_BLE_OOB_REQ_EVT (Not implemented: esp_ble_oob_req_reply)", + this->connection_index_, this->address_str_.c_str()); + // We probably want to something like this, however it has not yet been tested/verified. + // uint8_t tk[16] = {1}; // If you paired with OOB, both devices need to use the same tk + // esp_ble_oob_req_reply(param->ble_security.ble_req.bd_addr, tk, sizeof(tk)); + break; + } + default: + break; + } +} + +// Parse GATT values into a float for a sensor. +// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/ +float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { + // A length of one means a single octet value. + if (length == 0) + return 0; + if (length == 1) + return (float) ((uint8_t) value[0]); + + switch (value[0]) { + case 0x1: // boolean. + case 0x2: // 2bit. + case 0x3: // nibble. + case 0x4: // uint8. + return (float) ((uint8_t) value[1]); + case 0x5: // uint12. + case 0x6: // uint16. + if (length > 2) { + return (float) encode_uint16(value[1], value[2]); + } + // fall through + case 0x7: // uint24. + if (length > 3) { + return (float) encode_uint24(value[1], value[2], value[3]); + } + // fall through + case 0x8: // uint32. + if (length > 4) { + return (float) encode_uint32(value[1], value[2], value[3], value[4]); + } + // fall through + case 0xC: // int8. + return (float) ((int8_t) value[1]); + case 0xD: // int12. + case 0xE: // int16. + if (length > 2) { + return (float) ((int16_t)(value[1] << 8) + (int16_t) value[2]); + } + // fall through + case 0xF: // int24. + if (length > 3) { + return (float) ((int32_t)(value[1] << 16) + (int32_t)(value[2] << 8) + (int32_t)(value[3])); + } + // fall through + case 0x10: // int32. + if (length > 4) { + return (float) ((int32_t)(value[1] << 24) + (int32_t)(value[2] << 16) + (int32_t)(value[3] << 8) + + (int32_t)(value[4])); + } + } + ESP_LOGW(TAG, "[%d] [%s] Cannot parse characteristic value of type 0x%x length %d", this->connection_index_, + this->address_str_.c_str(), value[0], length); + return NAN; +} + +BLEService *BLEClientBase::get_service(espbt::ESPBTUUID uuid) { + for (auto *svc : this->services_) { + if (svc->uuid == uuid) + return svc; + } + return nullptr; +} + +BLEService *BLEClientBase::get_service(uint16_t uuid) { return this->get_service(espbt::ESPBTUUID::from_uint16(uuid)); } + +BLECharacteristic *BLEClientBase::get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr) { + auto *svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + return svc->get_characteristic(chr); +} + +BLECharacteristic *BLEClientBase::get_characteristic(uint16_t service, uint16_t chr) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr)); +} + +BLECharacteristic *BLEClientBase::get_characteristic(uint16_t handle) { + for (auto *svc : this->services_) { + if (!svc->parsed) + svc->parse_characteristics(); + for (auto *chr : svc->characteristics) { + if (chr->handle == handle) + return chr; + } + } + return nullptr; +} + +BLEDescriptor *BLEClientBase::get_config_descriptor(uint16_t handle) { + auto *chr = this->get_characteristic(handle); + if (chr != nullptr) { + if (!chr->parsed) + chr->parse_descriptors(); + for (auto &desc : chr->descriptors) { + if (desc->uuid.get_uuid().uuid.uuid16 == ESP_GATT_UUID_CHAR_CLIENT_CONFIG) + return desc; + } + } + return nullptr; +} + +BLEDescriptor *BLEClientBase::get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr) { + auto *svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + auto *ch = svc->get_characteristic(chr); + if (ch == nullptr) + return nullptr; + return ch->get_descriptor(descr); +} + +BLEDescriptor *BLEClientBase::get_descriptor(uint16_t service, uint16_t chr, uint16_t descr) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr), + espbt::ESPBTUUID::from_uint16(descr)); +} + +BLEDescriptor *BLEClientBase::get_descriptor(uint16_t handle) { + for (auto *svc : this->services_) { + if (!svc->parsed) + svc->parse_characteristics(); + for (auto *chr : svc->characteristics) { + if (!chr->parsed) + chr->parse_descriptors(); + for (auto *desc : chr->descriptors) { + if (desc->handle == handle) + return desc; + } + } + } + return nullptr; +} + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/components/esp32_ble_client/ble_client_base.h b/components/esp32_ble_client/ble_client_base.h new file mode 100644 index 0000000..98c7086 --- /dev/null +++ b/components/esp32_ble_client/ble_client_base.h @@ -0,0 +1,99 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/component.h" + +#include "ble_service.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace esphome { +namespace esp32_ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClientBase : public espbt::ESPBTClient, public Component { + public: + void setup() override; + void loop() override; + float get_setup_priority() const override; + + bool parse_device(const espbt::ESPBTDevice &device) override; + void on_scan_end() override {} + bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + void connect() override; + void disconnect(); + void release_services(); + + bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; } + + void set_address(uint64_t address) { + this->address_ = address; + if (address == 0) { + memset(this->remote_bda_, 0, sizeof(this->remote_bda_)); + this->address_str_ = ""; + } else { + this->address_str_ = str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, (uint8_t)(this->address_ >> 40) & 0xff, + (uint8_t)(this->address_ >> 32) & 0xff, (uint8_t)(this->address_ >> 24) & 0xff, + (uint8_t)(this->address_ >> 16) & 0xff, (uint8_t)(this->address_ >> 8) & 0xff, + (uint8_t)(this->address_ >> 0) & 0xff); + } + } + void set_pin_code(uint32_t pin_code) { pin_code_ = pin_code; } + std::string address_str() const { return this->address_str_; } + + BLEService *get_service(espbt::ESPBTUUID uuid); + BLEService *get_service(uint16_t uuid); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr); + BLECharacteristic *get_characteristic(uint16_t service, uint16_t chr); + BLECharacteristic *get_characteristic(uint16_t handle); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr); + BLEDescriptor *get_descriptor(uint16_t service, uint16_t chr, uint16_t descr); + BLEDescriptor *get_descriptor(uint16_t handle); + // Get the configuration descriptor for the given characteristic handle. + BLEDescriptor *get_config_descriptor(uint16_t handle); + + float parse_char_value(uint8_t *value, uint16_t length); + + int get_gattc_if() const { return this->gattc_if_; } + uint8_t *get_remote_bda() { return this->remote_bda_; } + esp_ble_addr_type_t get_remote_addr_type() const { return this->remote_addr_type_; } + void set_remote_addr_type(esp_ble_addr_type_t address_type) { this->remote_addr_type_ = address_type; } + uint16_t get_conn_id() const { return this->conn_id_; } + uint64_t get_address() const { return this->address_; } + + uint8_t get_connection_index() const { return this->connection_index_; } + + virtual void set_connection_type(espbt::ConnectionType ct) { this->connection_type_ = ct; } + + protected: + int gattc_if_; + esp_bd_addr_t remote_bda_; + esp_ble_addr_type_t remote_addr_type_; + uint16_t conn_id_{0xFFFF}; + uint64_t address_{0}; + std::string address_str_{}; + uint8_t connection_index_; + int16_t service_count_{0}; + uint16_t mtu_{23}; + uint32_t pin_code_{0}; + espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; + + std::vector services_; +}; + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/components/esp32_ble_client/ble_descriptor.h b/components/esp32_ble_client/ble_descriptor.h new file mode 100644 index 0000000..c054301 --- /dev/null +++ b/components/esp32_ble_client/ble_descriptor.h @@ -0,0 +1,25 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +namespace esphome { +namespace esp32_ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLECharacteristic; + +class BLEDescriptor { + public: + espbt::ESPBTUUID uuid; + uint16_t handle; + + BLECharacteristic *characteristic; +}; + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/components/esp32_ble_client/ble_service.cpp b/components/esp32_ble_client/ble_service.cpp new file mode 100644 index 0000000..b22d2a1 --- /dev/null +++ b/components/esp32_ble_client/ble_service.cpp @@ -0,0 +1,77 @@ +#include "ble_service.h" +#include "ble_client_base.h" + +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_client { + +static const char *const TAG = "esp32_ble_client"; + +BLECharacteristic *BLEService::get_characteristic(espbt::ESPBTUUID uuid) { + if (!this->parsed) + this->parse_characteristics(); + for (auto &chr : this->characteristics) { + if (chr->uuid == uuid) + return chr; + } + return nullptr; +} + +BLECharacteristic *BLEService::get_characteristic(uint16_t uuid) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(uuid)); +} + +BLEService::~BLEService() { + for (auto &chr : this->characteristics) + delete chr; // NOLINT(cppcoreguidelines-owning-memory) +} + +void BLEService::release_characteristics() { + this->parsed = false; + for (auto &chr : this->characteristics) + delete chr; // NOLINT(cppcoreguidelines-owning-memory) + this->characteristics.clear(); +} + +void BLEService::parse_characteristics() { + this->parsed = true; + uint16_t offset = 0; + esp_gattc_char_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = + esp_ble_gattc_get_all_char(this->client->get_gattc_if(), this->client->get_conn_id(), this->start_handle, + this->end_handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->client->get_connection_index(), + this->client->address_str().c_str(), status); + break; + } + if (count == 0) { + break; + } + + BLECharacteristic *characteristic = new BLECharacteristic(); // NOLINT(cppcoreguidelines-owning-memory) + characteristic->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); + characteristic->properties = result.properties; + characteristic->handle = result.char_handle; + characteristic->service = this; + this->characteristics.push_back(characteristic); + ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(), + this->client->address_str().c_str(), characteristic->uuid.to_string().c_str(), characteristic->handle, + characteristic->properties); + offset++; + } +} + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/components/esp32_ble_client/ble_service.h b/components/esp32_ble_client/ble_service.h new file mode 100644 index 0000000..41fc3e8 --- /dev/null +++ b/components/esp32_ble_client/ble_service.h @@ -0,0 +1,36 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#include "ble_characteristic.h" + +#include + +namespace esphome { +namespace esp32_ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClientBase; + +class BLEService { + public: + ~BLEService(); + bool parsed = false; + espbt::ESPBTUUID uuid; + uint16_t start_handle; + uint16_t end_handle; + std::vector characteristics; + BLEClientBase *client; + void parse_characteristics(); + void release_characteristics(); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID uuid); + BLECharacteristic *get_characteristic(uint16_t uuid); +}; + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/components/esp32_ble_tracker/__init__.py b/components/esp32_ble_tracker/__init__.py new file mode 100644 index 0000000..ac84f58 --- /dev/null +++ b/components/esp32_ble_tracker/__init__.py @@ -0,0 +1,320 @@ +import re +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import ( + CONF_ACTIVE, + CONF_ID, + CONF_INTERVAL, + CONF_DURATION, + CONF_TRIGGER_ID, + CONF_MAC_ADDRESS, + CONF_SERVICE_UUID, + CONF_MANUFACTURER_ID, + CONF_ON_BLE_ADVERTISE, + CONF_ON_BLE_SERVICE_DATA_ADVERTISE, + CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, +) +from esphome.core import CORE +from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components import esp32_ble + +DEPENDENCIES = ["esp32"] + +CONF_ESP32_BLE_ID = "esp32_ble_id" +CONF_SCAN_PARAMETERS = "scan_parameters" +CONF_WINDOW = "window" +CONF_CONTINUOUS = "continuous" +CONF_ON_SCAN_END = "on_scan_end" +CONF_IO_CAPABILITY = "io_capability" +esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") +ESP32BLETracker = esp32_ble_tracker_ns.class_("ESP32BLETracker", cg.Component) +ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient") +ESPBTDeviceListener = esp32_ble_tracker_ns.class_("ESPBTDeviceListener") +ESPBTDevice = esp32_ble_tracker_ns.class_("ESPBTDevice") +ESPBTDeviceConstRef = ESPBTDevice.operator("ref").operator("const") +adv_data_t = cg.std_vector.template(cg.uint8) +adv_data_t_const_ref = adv_data_t.operator("ref").operator("const") +# Triggers +ESPBTAdvertiseTrigger = esp32_ble_tracker_ns.class_( + "ESPBTAdvertiseTrigger", automation.Trigger.template(ESPBTDeviceConstRef) +) +BLEServiceDataAdvertiseTrigger = esp32_ble_tracker_ns.class_( + "BLEServiceDataAdvertiseTrigger", automation.Trigger.template(adv_data_t_const_ref) +) +BLEManufacturerDataAdvertiseTrigger = esp32_ble_tracker_ns.class_( + "BLEManufacturerDataAdvertiseTrigger", + automation.Trigger.template(adv_data_t_const_ref), +) +BLEEndOfScanTrigger = esp32_ble_tracker_ns.class_( + "BLEEndOfScanTrigger", automation.Trigger.template() +) +# Actions +ESP32BLEStartScanAction = esp32_ble_tracker_ns.class_( + "ESP32BLEStartScanAction", automation.Action +) +ESP32BLEStopScanAction = esp32_ble_tracker_ns.class_( + "ESP32BLEStopScanAction", automation.Action +) + +IoCapability = esp32_ble_tracker_ns.enum("IoCapability") +IO_CAPABILITY = { + "none": IoCapability.IO_CAP_NONE, + "keyboard_only": IoCapability.IO_CAP_IN, + "keyboard_display": IoCapability.IO_CAP_KBDISP, + "display_only": IoCapability.IO_CAP_OUT, + "display_yes_no": IoCapability.IO_CAP_IO, +} + + +def validate_scan_parameters(config): + duration = config[CONF_DURATION] + interval = config[CONF_INTERVAL] + window = config[CONF_WINDOW] + + if window > interval: + raise cv.Invalid( + f"Scan window ({window}) needs to be smaller than scan interval ({interval})" + ) + + if interval.total_milliseconds * 3 > duration.total_milliseconds: + raise cv.Invalid( + "Scan duration needs to be at least three times the scan interval to" + "cover all BLE channels." + ) + + return config + + +bt_uuid16_format = "XXXX" +bt_uuid32_format = "XXXXXXXX" +bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + + +def bt_uuid(value): + in_value = cv.string_strict(value) + value = in_value.upper() + + if len(value) == len(bt_uuid16_format): + pattern = re.compile("^[A-F|0-9]{4,}$") + if not pattern.match(value): + raise cv.Invalid( + f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'" + ) + return value + if len(value) == len(bt_uuid32_format): + pattern = re.compile("^[A-F|0-9]{8,}$") + if not pattern.match(value): + raise cv.Invalid( + f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'" + ) + return value + if len(value) == len(bt_uuid128_format): + pattern = re.compile( + "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$" + ) + if not pattern.match(value): + raise cv.Invalid( + f"Invalid hexadecimal value for 128 UUID format: '{in_value}'" + ) + return value + raise cv.Invalid( + f"Service UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format" + ) + + +def as_hex(value): + return cg.RawExpression(f"0x{value}ULL") + + +def as_hex_array(value): + value = value.replace("-", "") + cpp_array = [ + f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] + ] + return cg.RawExpression(f"(uint8_t*)(const uint8_t[16]){{{','.join(cpp_array)}}}") + + +def as_reversed_hex_array(value): + value = value.replace("-", "") + cpp_array = [ + f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] + ] + return cg.RawExpression( + f"(uint8_t*)(const uint8_t[16]){{{','.join(reversed(cpp_array))}}}" + ) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32BLETracker), + cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( + cv.Schema( + { + cv.Optional( + CONF_DURATION, default="5min" + ): cv.positive_time_period_seconds, + cv.Optional( + CONF_INTERVAL, default="320ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_WINDOW, default="30ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ACTIVE, default=True): cv.boolean, + cv.Optional(CONF_CONTINUOUS, default=True): cv.boolean, + } + ), + validate_scan_parameters, + ), + cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum( + IO_CAPABILITY, lower=True + ), + cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + } + ), + cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEServiceDataAdvertiseTrigger + ), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_SERVICE_UUID): bt_uuid, + } + ), + cv.Optional( + CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE + ): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEManufacturerDataAdvertiseTrigger + ), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_MANUFACTURER_ID): bt_uuid, + } + ), + cv.Optional(CONF_ON_SCAN_END): automation.validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEEndOfScanTrigger)} + ), + } +).extend(cv.COMPONENT_SCHEMA) + +FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant + +ESP_BLE_DEVICE_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ESP32_BLE_ID): cv.use_id(ESP32BLETracker), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + params = config[CONF_SCAN_PARAMETERS] + cg.add(var.set_scan_duration(params[CONF_DURATION])) + cg.add(var.set_scan_interval(int(params[CONF_INTERVAL].total_milliseconds / 0.625))) + cg.add(var.set_scan_window(int(params[CONF_WINDOW].total_milliseconds / 0.625))) + cg.add(var.set_scan_active(params[CONF_ACTIVE])) + cg.add(var.set_scan_continuous(params[CONF_CONTINUOUS])) + cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) + for conf in config.get(CONF_ON_BLE_ADVERTISE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + if CONF_MAC_ADDRESS in conf: + cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) + await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) + for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format): + cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID]))) + elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid32_format): + cg.add(trigger.set_service_uuid32(as_hex(conf[CONF_SERVICE_UUID]))) + elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid128_format): + uuid128 = as_reversed_hex_array(conf[CONF_SERVICE_UUID]) + cg.add(trigger.set_service_uuid128(uuid128)) + if CONF_MAC_ADDRESS in conf: + cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) + await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf) + for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format): + cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID]))) + elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid32_format): + cg.add(trigger.set_manufacturer_uuid32(as_hex(conf[CONF_MANUFACTURER_ID]))) + elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid128_format): + uuid128 = as_reversed_hex_array(conf[CONF_MANUFACTURER_ID]) + cg.add(trigger.set_manufacturer_uuid128(uuid128)) + if CONF_MAC_ADDRESS in conf: + cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) + await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf) + for conf in config.get(CONF_ON_SCAN_END, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) + # https://github.com/espressif/esp-idf/issues/4101 + # https://github.com/espressif/esp-idf/issues/2503 + # Match arduino CONFIG_BTU_TASK_STACK_SIZE + # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 + add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) + + cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts + + +ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(ESP32BLETracker), + cv.Optional(CONF_CONTINUOUS, default=False): cv.templatable(cv.boolean), + } +) + + +@automation.register_action( + "esp32_ble_tracker.start_scan", + ESP32BLEStartScanAction, + ESP32_BLE_START_SCAN_ACTION_SCHEMA, +) +async def esp32_ble_tracker_start_scan_action_to_code( + config, action_id, template_arg, args +): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + cg.add(var.set_continuous(config[CONF_CONTINUOUS])) + return var + + +ESP32_BLE_STOP_SCAN_ACTION_SCHEMA = automation.maybe_simple_id( + cv.Schema( + { + cv.GenerateID(): cv.use_id(ESP32BLETracker), + } + ) +) + + +@automation.register_action( + "esp32_ble_tracker.stop_scan", + ESP32BLEStopScanAction, + ESP32_BLE_STOP_SCAN_ACTION_SCHEMA, +) +async def esp32_ble_tracker_stop_scan_action_to_code( + config, action_id, template_arg, args +): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def register_ble_device(var, config): + paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) + cg.add(paren.register_listener(var)) + return var + + +async def register_client(var, config): + paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) + cg.add(paren.register_client(var)) + return var diff --git a/components/esp32_ble_tracker/automation.h b/components/esp32_ble_tracker/automation.h new file mode 100644 index 0000000..6131d6d --- /dev/null +++ b/components/esp32_ble_tracker/automation.h @@ -0,0 +1,108 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_tracker { +class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { + public: + explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } + void set_address(uint64_t address) { this->address_ = address; } + + bool parse_device(const ESPBTDevice &device) override { + if (this->address_ && device.address_uint64() != this->address_) { + return false; + } + this->trigger(device); + return true; + } + + protected: + uint64_t address_ = 0; +}; + +class BLEServiceDataAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { + public: + explicit BLEServiceDataAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } + void set_address(uint64_t address) { this->address_ = address; } + void set_service_uuid16(uint16_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(uuid); } + void set_service_uuid128(uint8_t *uuid) { this->uuid_ = ESPBTUUID::from_raw(uuid); } + + bool parse_device(const ESPBTDevice &device) override { + if (this->address_ && device.address_uint64() != this->address_) { + return false; + } + for (auto &service_data : device.get_service_datas()) { + if (service_data.uuid == this->uuid_) { + this->trigger(service_data.data); + return true; + } + } + return false; + } + + protected: + uint64_t address_ = 0; + ESPBTUUID uuid_; +}; + +class BLEManufacturerDataAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { + public: + explicit BLEManufacturerDataAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } + void set_address(uint64_t address) { this->address_ = address; } + void set_manufacturer_uuid16(uint16_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(uuid); } + void set_manufacturer_uuid32(uint32_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(uuid); } + void set_manufacturer_uuid128(uint8_t *uuid) { this->uuid_ = ESPBTUUID::from_raw(uuid); } + + bool parse_device(const ESPBTDevice &device) override { + if (this->address_ && device.address_uint64() != this->address_) { + return false; + } + for (auto &manufacturer_data : device.get_manufacturer_datas()) { + if (manufacturer_data.uuid == this->uuid_) { + this->trigger(manufacturer_data.data); + return true; + } + } + return false; + } + + protected: + uint64_t address_ = 0; + ESPBTUUID uuid_; +}; + +class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener { + public: + explicit BLEEndOfScanTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } + + bool parse_device(const ESPBTDevice &device) override { return false; } + void on_scan_end() override { this->trigger(); } +}; + +template class ESP32BLEStartScanAction : public Action { + public: + ESP32BLEStartScanAction(ESP32BLETracker *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(bool, continuous) + void play(Ts... x) override { + this->parent_->set_scan_continuous(this->continuous_.value(x...)); + this->parent_->start_scan(); + } + + protected: + ESP32BLETracker *parent_; +}; + +template class ESP32BLEStopScanAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->stop_scan(); } +}; + +} // namespace esp32_ble_tracker +} // namespace esphome + +#endif diff --git a/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/components/esp32_ble_tracker/esp32_ble_tracker.cpp new file mode 100644 index 0000000..bdd9b23 --- /dev/null +++ b/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -0,0 +1,941 @@ +#ifdef USE_ESP32 + +#include "esp32_ble_tracker.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef USE_OTA +#include "esphome/components/ota/ota_component.h" +#endif + +#ifdef USE_ARDUINO +#include +#endif + +// bt_trace.h +#undef TAG + +namespace esphome { +namespace esp32_ble_tracker { + +static const char *const TAG = "esp32_ble_tracker"; + +ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +esp_ble_io_cap_t global_io_cap = ESP_IO_CAP_NONE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { + uint64_t u = 0; + u |= uint64_t(address[0] & 0xFF) << 40; + u |= uint64_t(address[1] & 0xFF) << 32; + u |= uint64_t(address[2] & 0xFF) << 24; + u |= uint64_t(address[3] & 0xFF) << 16; + u |= uint64_t(address[4] & 0xFF) << 8; + u |= uint64_t(address[5] & 0xFF) << 0; + return u; +} + +float ESP32BLETracker::get_setup_priority() const { return setup_priority::BLUETOOTH; } + +void ESP32BLETracker::setup() { + global_esp32_ble_tracker = this; + this->scan_result_lock_ = xSemaphoreCreateMutex(); + this->scan_end_lock_ = xSemaphoreCreateMutex(); + this->scanner_idle_ = true; + if (!ESP32BLETracker::ble_setup()) { + this->mark_failed(); + return; + } + +#ifdef USE_OTA + ota::global_ota_component->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t error) { + if (state == ota::OTA_STARTED) { + this->stop_scan(); + } + }); +#endif + + if (this->scan_continuous_) { + if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + this->start_scan_(true); + } else { + ESP_LOGW(TAG, "Cannot start scan!"); + } + } +} + +void ESP32BLETracker::loop() { + BLEEvent *ble_event = this->ble_events_.pop(); + while (ble_event != nullptr) { + if (ble_event->type_) { + this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, + &ble_event->event_.gattc.gattc_param); + } else { + this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); + } + delete ble_event; // NOLINT(cppcoreguidelines-owning-memory) + ble_event = this->ble_events_.pop(); + } + + int connecting = 0; + int discovered = 0; + int searching = 0; + int disconnecting = 0; + for (auto *client : this->clients_) { + switch (client->state()) { + case ClientState::DISCONNECTING: + disconnecting++; + break; + case ClientState::DISCOVERED: + discovered++; + break; + case ClientState::SEARCHING: + searching++; + break; + case ClientState::CONNECTING: + case ClientState::READY_TO_CONNECT: + connecting++; + break; + default: + break; + } + } + bool promote_to_connecting = discovered && !searching && !connecting; + + if (!this->scanner_idle_) { + if (this->scan_result_index_ && // if it looks like we have a scan result we will take the lock + xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { + uint32_t index = this->scan_result_index_; + if (index) { + if (index >= 16) { + ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); + } + for (size_t i = 0; i < index; i++) { + ESPBTDevice device; + device.parse_scan_rst(this->scan_result_buffer_[i]); + + bool found = false; + for (auto *listener : this->listeners_) { + if (listener->parse_device(device)) + found = true; + } + + for (auto *client : this->clients_) { + if (client->parse_device(device)) { + found = true; + if (!connecting && client->state() == ClientState::DISCOVERED) { + promote_to_connecting = true; + } + } + } + + if (!found && !this->scan_continuous_) { + this->print_bt_device_info(device); + } + } + this->scan_result_index_ = 0; + } + xSemaphoreGive(this->scan_result_lock_); + } + + /* + + Avoid starting the scanner if: + - we are already scanning + - we are connecting to a device + - we are disconnecting from a device + + Otherwise the scanner could fail to ever start again + and our only way to recover is to reboot. + + https://github.com/espressif/esp-idf/issues/6688 + + */ + if (!connecting && !disconnecting && xSemaphoreTake(this->scan_end_lock_, 0L)) { + if (this->scan_continuous_) { + if (!promote_to_connecting && !this->scan_start_failed_ && !this->scan_set_param_failed_) { + this->start_scan_(false); + } else { + // We didn't start the scan, so we need to release the lock + xSemaphoreGive(this->scan_end_lock_); + } + } else if (!this->scanner_idle_) { + this->end_of_scan_(); + return; + } + } + + if (this->scan_start_failed_ || this->scan_set_param_failed_) { + if (this->scan_start_fail_count_ == 255) { + ESP_LOGE(TAG, "ESP-IDF BLE scan could not restart after 255 attempts, rebooting to restore BLE stack..."); + App.reboot(); + } + if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + xSemaphoreGive(this->scan_end_lock_); + } else { + ESP_LOGD(TAG, "Stopping scan after failure..."); + esp_ble_gap_stop_scanning(); + this->cancel_timeout("scan"); + } + if (this->scan_start_failed_) { + ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_); + this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS; + } + if (this->scan_set_param_failed_) { + ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_); + this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; + } + } + } + + // If there is a discovered client and no connecting + // clients and no clients using the scanner to search for + // devices, then stop scanning and promote the discovered + // client to ready to connect. + if (promote_to_connecting) { + for (auto *client : this->clients_) { + if (client->state() == ClientState::DISCOVERED) { + if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + // Scanner is not running since we got the + // lock, so we can promote the client. + xSemaphoreGive(this->scan_end_lock_); + // We only want to promote one client at a time. + // once the scanner is fully stopped. + client->set_state(ClientState::READY_TO_CONNECT); + } else { + ESP_LOGD(TAG, "Pausing scan to make connection..."); + esp_ble_gap_stop_scanning(); + this->cancel_timeout("scan"); + } + break; + } + } + } +} + +void ESP32BLETracker::start_scan() { + if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + this->start_scan_(true); + } else { + ESP_LOGW(TAG, "Scan requested when a scan is already in progress. Ignoring."); + } +} + +void ESP32BLETracker::stop_scan() { + ESP_LOGD(TAG, "Stopping scan."); + this->scan_continuous_ = false; + esp_ble_gap_stop_scanning(); + this->cancel_timeout("scan"); +} + +bool ESP32BLETracker::ble_setup() { + // Initialize non-volatile storage for the bluetooth controller + esp_err_t err = nvs_flash_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "nvs_flash_init failed: %d", err); + return false; + } + +#ifdef USE_ARDUINO + if (!btStart()) { + ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); + return false; + } +#else + if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { + // start bt controller + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { + esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + err = esp_bt_controller_init(&cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err)); + return false; + } + while (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) + ; + } + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED) { + err = esp_bt_controller_enable(ESP_BT_MODE_BLE); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_bt_controller_enable failed: %s", esp_err_to_name(err)); + return false; + } + } + if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { + ESP_LOGE(TAG, "esp bt controller enable failed"); + return false; + } + } +#endif + + esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); + + err = esp_bluedroid_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err); + return false; + } + err = esp_bluedroid_enable(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); + return false; + } + err = esp_ble_gap_register_callback(ESP32BLETracker::gap_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); + return false; + } + err = esp_ble_gattc_register_callback(ESP32BLETracker::gattc_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); + return false; + } + + // Empty name + esp_ble_gap_set_device_name(""); + + err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &global_io_cap, sizeof(uint8_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); + return false; + } + + // BLE takes some time to be fully set up, 200ms should be more than enough + delay(200); // NOLINT + + return true; +} + +void ESP32BLETracker::start_scan_(bool first) { + // The lock must be held when calling this function. + if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + ESP_LOGE(TAG, "start_scan called without holding scan_end_lock_"); + return; + } + + ESP_LOGD(TAG, "Starting scan..."); + if (!first) { + for (auto *listener : this->listeners_) + listener->on_scan_end(); + } + this->already_discovered_.clear(); + this->scanner_idle_ = false; + this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE; + this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC; + this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL; + this->scan_params_.scan_interval = this->scan_interval_; + this->scan_params_.scan_window = this->scan_window_; + + esp_ble_gap_set_scan_params(&this->scan_params_); + esp_ble_gap_start_scanning(this->scan_duration_); + + this->set_timeout("scan", this->scan_duration_ * 2000, []() { + ESP_LOGE(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack..."); + App.reboot(); + }); +} + +void ESP32BLETracker::end_of_scan_() { + // The lock must be held when calling this function. + if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + ESP_LOGE(TAG, "end_of_scan_ called without holding the scan_end_lock_"); + return; + } + + ESP_LOGD(TAG, "End of scan."); + this->scanner_idle_ = true; + this->already_discovered_.clear(); + xSemaphoreGive(this->scan_end_lock_); + this->cancel_timeout("scan"); + + for (auto *listener : this->listeners_) + listener->on_scan_end(); +} + +void ESP32BLETracker::register_client(ESPBTClient *client) { + client->app_id = ++this->app_id_; + this->clients_.push_back(client); +} + +void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + BLEEvent *gap_event = new BLEEvent(event, param); // NOLINT(cppcoreguidelines-owning-memory) + global_esp32_ble_tracker->ble_events_.push(gap_event); +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +void ESP32BLETracker::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_RESULT_EVT: + this->gap_scan_result_(param->scan_rst); + break; + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + this->gap_scan_set_param_complete_(param->scan_param_cmpl); + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + this->gap_scan_start_complete_(param->scan_start_cmpl); + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + this->gap_scan_stop_complete_(param->scan_stop_cmpl); + break; + default: + break; + } + for (auto *client : this->clients_) { + client->gap_event_handler(event, param); + } +} + +void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) { + this->scan_set_param_failed_ = param.status; +} + +void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m) { + this->scan_start_failed_ = param.status; + if (param.status == ESP_BT_STATUS_SUCCESS) { + this->scan_start_fail_count_ = 0; + } else { + this->scan_start_fail_count_++; + xSemaphoreGive(this->scan_end_lock_); + } +} + +void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) { + xSemaphoreGive(this->scan_end_lock_); +} + +void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { + if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { + if (xSemaphoreTake(this->scan_result_lock_, 0L)) { + if (this->scan_result_index_ < 16) { + this->scan_result_buffer_[this->scan_result_index_++] = param; + } + xSemaphoreGive(this->scan_result_lock_); + } + } else if (param.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { + xSemaphoreGive(this->scan_end_lock_); + } +} + +void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + BLEEvent *gattc_event = new BLEEvent(event, gattc_if, param); // NOLINT(cppcoreguidelines-owning-memory) + global_esp32_ble_tracker->ble_events_.push(gattc_event); +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +void ESP32BLETracker::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + for (auto *client : this->clients_) { + client->gattc_event_handler(event, gattc_if, param); + } +} + +ESPBTUUID::ESPBTUUID() : uuid_() {} +ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { + ESPBTUUID ret; + ret.uuid_.len = ESP_UUID_LEN_16; + ret.uuid_.uuid.uuid16 = uuid; + return ret; +} +ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) { + ESPBTUUID ret; + ret.uuid_.len = ESP_UUID_LEN_32; + ret.uuid_.uuid.uuid32 = uuid; + return ret; +} +ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { + ESPBTUUID ret; + ret.uuid_.len = ESP_UUID_LEN_128; + for (size_t i = 0; i < ESP_UUID_LEN_128; i++) + ret.uuid_.uuid.uuid128[i] = data[i]; + return ret; +} +ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { + ESPBTUUID ret; + if (data.length() == 4) { + ret.uuid_.len = ESP_UUID_LEN_16; + ret.uuid_.uuid.uuid16 = 0; + for (int i = 0; i < data.length();) { + uint8_t msb = data.c_str()[i]; + uint8_t lsb = data.c_str()[i + 1]; + + if (msb > '9') + msb -= 7; + if (lsb > '9') + lsb -= 7; + ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (2 - i) * 4; + i += 2; + } + } else if (data.length() == 8) { + ret.uuid_.len = ESP_UUID_LEN_32; + ret.uuid_.uuid.uuid32 = 0; + for (int i = 0; i < data.length();) { + uint8_t msb = data.c_str()[i]; + uint8_t lsb = data.c_str()[i + 1]; + + if (msb > '9') + msb -= 7; + if (lsb > '9') + lsb -= 7; + ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (6 - i) * 4; + i += 2; + } + } else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be + // investigated (lack of time) + ret.uuid_.len = ESP_UUID_LEN_128; + memcpy(ret.uuid_.uuid.uuid128, (uint8_t *) data.data(), 16); + } else if (data.length() == 36) { + // If the length of the string is 36 bytes then we will assume it is a long hex string in + // UUID format. + ret.uuid_.len = ESP_UUID_LEN_128; + int n = 0; + for (int i = 0; i < data.length();) { + if (data.c_str()[i] == '-') + i++; + uint8_t msb = data.c_str()[i]; + uint8_t lsb = data.c_str()[i + 1]; + + if (msb > '9') + msb -= 7; + if (lsb > '9') + lsb -= 7; + ret.uuid_.uuid.uuid128[15 - n++] = ((msb & 0x0F) << 4) | (lsb & 0x0F); + i += 2; + } + } else { + ESP_LOGE(TAG, "ERROR: UUID value not 2, 4, 16 or 36 bytes - %s", data.c_str()); + } + return ret; +} +ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { + ESPBTUUID ret; + ret.uuid_.len = uuid.len; + if (uuid.len == ESP_UUID_LEN_16) { + ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; + } else if (uuid.len == ESP_UUID_LEN_32) { + ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; + } else if (uuid.len == ESP_UUID_LEN_128) { + memcpy(ret.uuid_.uuid.uuid128, uuid.uuid.uuid128, ESP_UUID_LEN_128); + } + return ret; +} +ESPBTUUID ESPBTUUID::as_128bit() const { + if (this->uuid_.len == ESP_UUID_LEN_128) { + return *this; + } + uint8_t data[] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint32_t uuid32; + if (this->uuid_.len == ESP_UUID_LEN_32) { + uuid32 = this->uuid_.uuid.uuid32; + } else { + uuid32 = this->uuid_.uuid.uuid16; + } + for (uint8_t i = 0; i < this->uuid_.len; i++) { + data[12 + i] = ((uuid32 >> i * 8) & 0xFF); + } + return ESPBTUUID::from_raw(data); +} +bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { + if (this->uuid_.len == ESP_UUID_LEN_16) { + return (this->uuid_.uuid.uuid16 >> 8) == data2 && (this->uuid_.uuid.uuid16 & 0xFF) == data1; + } else if (this->uuid_.len == ESP_UUID_LEN_32) { + for (uint8_t i = 0; i < 3; i++) { + bool a = ((this->uuid_.uuid.uuid32 >> i * 8) & 0xFF) == data1; + bool b = ((this->uuid_.uuid.uuid32 >> (i + 1) * 8) & 0xFF) == data2; + if (a && b) + return true; + } + } else { + for (uint8_t i = 0; i < 15; i++) { + if (this->uuid_.uuid.uuid128[i] == data1 && this->uuid_.uuid.uuid128[i + 1] == data2) + return true; + } + } + return false; +} +bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { + if (this->uuid_.len == uuid.uuid_.len) { + switch (this->uuid_.len) { + case ESP_UUID_LEN_16: + if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) { + return true; + } + break; + case ESP_UUID_LEN_32: + if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) { + return true; + } + break; + case ESP_UUID_LEN_128: + for (int i = 0; i < ESP_UUID_LEN_128; i++) { + if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) { + return false; + } + } + return true; + break; + } + } else { + return this->as_128bit() == uuid.as_128bit(); + } + return false; +} +esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } +std::string ESPBTUUID::to_string() const { + switch (this->uuid_.len) { + case ESP_UUID_LEN_16: + return str_snprintf("0x%02X%02X", 6, this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); + case ESP_UUID_LEN_32: + return str_snprintf("0x%02X%02X%02X%02X", 10, this->uuid_.uuid.uuid32 >> 24, + (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff), + this->uuid_.uuid.uuid32 & 0xff); + default: + case ESP_UUID_LEN_128: + std::string buf; + for (uint8_t i = 0; i < 16; i++) { + buf += str_snprintf("%02X", 2, this->uuid_.uuid.uuid128[i]); + if (i == 3 || i == 5 || i == 7 || i == 9) + buf += "-"; + } + return buf; + } + return ""; +} + +uint64_t ESPBTUUID::get_128bit_high() const { + esp_bt_uuid_t uuid = this->as_128bit().get_uuid(); + return ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | + ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | + ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | + ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]); +} +uint64_t ESPBTUUID::get_128bit_low() const { + esp_bt_uuid_t uuid = this->as_128bit().get_uuid(); + return ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | + ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | + ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | + ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]); +} + +ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); } +optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) { + if (!data.uuid.contains(0x4C, 0x00)) + return {}; + + if (data.data.size() != 23) + return {}; + return ESPBLEiBeacon(data.data.data()); +} + +void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { + this->scan_result_ = param; + for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) + this->address_[i] = param.bda[i]; + this->address_type_ = param.ble_addr_type; + this->rssi_ = param.rssi; + this->parse_adv_(param); + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + ESP_LOGVV(TAG, "Parse Result:"); + const char *address_type = ""; + switch (this->address_type_) { + case BLE_ADDR_TYPE_PUBLIC: + address_type = "PUBLIC"; + break; + case BLE_ADDR_TYPE_RANDOM: + address_type = "RANDOM"; + break; + case BLE_ADDR_TYPE_RPA_PUBLIC: + address_type = "RPA_PUBLIC"; + break; + case BLE_ADDR_TYPE_RPA_RANDOM: + address_type = "RPA_RANDOM"; + break; + } + ESP_LOGVV(TAG, " Address: %02X:%02X:%02X:%02X:%02X:%02X (%s)", this->address_[0], this->address_[1], + this->address_[2], this->address_[3], this->address_[4], this->address_[5], address_type); + + ESP_LOGVV(TAG, " RSSI: %d", this->rssi_); + ESP_LOGVV(TAG, " Name: '%s'", this->name_.c_str()); + for (auto &it : this->tx_powers_) { + ESP_LOGVV(TAG, " TX Power: %d", it); + } + if (this->appearance_.has_value()) { + ESP_LOGVV(TAG, " Appearance: %u", *this->appearance_); + } + if (this->ad_flag_.has_value()) { + ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_); + } + for (auto &uuid : this->service_uuids_) { + ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str()); + } + for (auto &data : this->manufacturer_datas_) { + ESP_LOGVV(TAG, " Manufacturer data: %s", format_hex_pretty(data.data).c_str()); + if (this->get_ibeacon().has_value()) { + auto ibeacon = this->get_ibeacon().value(); + ESP_LOGVV(TAG, " iBeacon data:"); + ESP_LOGVV(TAG, " UUID: %s", ibeacon.get_uuid().to_string().c_str()); + ESP_LOGVV(TAG, " Major: %u", ibeacon.get_major()); + ESP_LOGVV(TAG, " Minor: %u", ibeacon.get_minor()); + ESP_LOGVV(TAG, " TXPower: %d", ibeacon.get_signal_power()); + } + } + for (auto &data : this->service_datas_) { + ESP_LOGVV(TAG, " Service data:"); + ESP_LOGVV(TAG, " UUID: %s", data.uuid.to_string().c_str()); + ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str()); + } + + ESP_LOGVV(TAG, "Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); +#endif +} +void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { + size_t offset = 0; + const uint8_t *payload = param.ble_adv; + uint8_t len = param.adv_data_len + param.scan_rsp_len; + + while (offset + 2 < len) { + const uint8_t field_length = payload[offset++]; // First byte is length of adv record + if (field_length == 0) { + continue; // Possible zero padded advertisement data + } + + // first byte of adv record is adv record type + const uint8_t record_type = payload[offset++]; + const uint8_t *record = &payload[offset]; + const uint8_t record_length = field_length - 1; + offset += record_length; + + // See also Generic Access Profile Assigned Numbers: + // https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/ See also ADVERTISING AND SCAN + // RESPONSE DATA FORMAT: https://www.bluetooth.com/specifications/bluetooth-core-specification/ (vol 3, part C, 11) + // See also Core Specification Supplement: https://www.bluetooth.com/specifications/bluetooth-core-specification/ + // (called CSS here) + + switch (record_type) { + case ESP_BLE_AD_TYPE_NAME_SHORT: + case ESP_BLE_AD_TYPE_NAME_CMPL: { + // CSS 1.2 LOCAL NAME + // "The Local Name data type shall be the same as, or a shortened version of, the local name assigned to the + // device." CSS 1: Optional in this context; shall not appear more than once in a block. + // SHORTENED LOCAL NAME + // "The Shortened Local Name data type defines a shortened version of the Local Name data type. The Shortened + // Local Name data type shall not be used to advertise a name that is longer than the Local Name data type." + if (record_length > this->name_.length()) { + this->name_ = std::string(reinterpret_cast(record), record_length); + } + break; + } + case ESP_BLE_AD_TYPE_TX_PWR: { + // CSS 1.5 TX POWER LEVEL + // "The TX Power Level data type indicates the transmitted power level of the packet containing the data type." + // CSS 1: Optional in this context (may appear more than once in a block). + this->tx_powers_.push_back(*payload); + break; + } + case ESP_BLE_AD_TYPE_APPEARANCE: { + // CSS 1.12 APPEARANCE + // "The Appearance data type defines the external appearance of the device." + // See also https://www.bluetooth.com/specifications/gatt/characteristics/ + // CSS 1: Optional in this context; shall not appear more than once in a block and shall not appear in both + // the AD and SRD of the same extended advertising interval. + this->appearance_ = *reinterpret_cast(record); + break; + } + case ESP_BLE_AD_TYPE_FLAG: { + // CSS 1.3 FLAGS + // "The Flags data type contains one bit Boolean flags. The Flags data type shall be included when any of the + // Flag bits are non-zero and the advertising packet is connectable, otherwise the Flags data type may be + // omitted." + // CSS 1: Optional in this context; shall not appear more than once in a block. + this->ad_flag_ = *record; + break; + } + // CSS 1.1 SERVICE UUID + // The Service UUID data type is used to include a list of Service or Service Class UUIDs. + // There are six data types defined for the three sizes of Service UUIDs that may be returned: + // CSS 1: Optional in this context (may appear more than once in a block). + case ESP_BLE_AD_TYPE_16SRV_CMPL: + case ESP_BLE_AD_TYPE_16SRV_PART: { + // • 16-bit Bluetooth Service UUIDs + for (uint8_t i = 0; i < record_length / 2; i++) { + this->service_uuids_.push_back(ESPBTUUID::from_uint16(*reinterpret_cast(record + 2 * i))); + } + break; + } + case ESP_BLE_AD_TYPE_32SRV_CMPL: + case ESP_BLE_AD_TYPE_32SRV_PART: { + // • 32-bit Bluetooth Service UUIDs + for (uint8_t i = 0; i < record_length / 4; i++) { + this->service_uuids_.push_back(ESPBTUUID::from_uint32(*reinterpret_cast(record + 4 * i))); + } + break; + } + case ESP_BLE_AD_TYPE_128SRV_CMPL: + case ESP_BLE_AD_TYPE_128SRV_PART: { + // • Global 128-bit Service UUIDs + this->service_uuids_.push_back(ESPBTUUID::from_raw(record)); + break; + } + case ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE: { + // CSS 1.4 MANUFACTURER SPECIFIC DATA + // "The Manufacturer Specific data type is used for manufacturer specific data. The first two data octets shall + // contain a company identifier from Assigned Numbers. The interpretation of any other octets within the data + // shall be defined by the manufacturer specified by the company identifier." + // CSS 1: Optional in this context (may appear more than once in a block). + if (record_length < 2) { + ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE"); + break; + } + ServiceData data{}; + data.uuid = ESPBTUUID::from_uint16(*reinterpret_cast(record)); + data.data.assign(record + 2UL, record + record_length); + this->manufacturer_datas_.push_back(data); + break; + } + + // CSS 1.11 SERVICE DATA + // "The Service Data data type consists of a service UUID with the data associated with that service." + // CSS 1: Optional in this context (may appear more than once in a block). + case ESP_BLE_AD_TYPE_SERVICE_DATA: { + // «Service Data - 16 bit UUID» + // Size: 2 or more octets + // The first 2 octets contain the 16 bit Service UUID fol- lowed by additional service data + if (record_length < 2) { + ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_SERVICE_DATA"); + break; + } + ServiceData data{}; + data.uuid = ESPBTUUID::from_uint16(*reinterpret_cast(record)); + data.data.assign(record + 2UL, record + record_length); + this->service_datas_.push_back(data); + break; + } + case ESP_BLE_AD_TYPE_32SERVICE_DATA: { + // «Service Data - 32 bit UUID» + // Size: 4 or more octets + // The first 4 octets contain the 32 bit Service UUID fol- lowed by additional service data + if (record_length < 4) { + ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_32SERVICE_DATA"); + break; + } + ServiceData data{}; + data.uuid = ESPBTUUID::from_uint32(*reinterpret_cast(record)); + data.data.assign(record + 4UL, record + record_length); + this->service_datas_.push_back(data); + break; + } + case ESP_BLE_AD_TYPE_128SERVICE_DATA: { + // «Service Data - 128 bit UUID» + // Size: 16 or more octets + // The first 16 octets contain the 128 bit Service UUID followed by additional service data + if (record_length < 16) { + ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_128SERVICE_DATA"); + break; + } + ServiceData data{}; + data.uuid = ESPBTUUID::from_raw(record); + data.data.assign(record + 16UL, record + record_length); + this->service_datas_.push_back(data); + break; + } + case ESP_BLE_AD_TYPE_INT_RANGE: + // Avoid logging this as it's very verbose + break; + default: { + ESP_LOGV(TAG, "Unhandled type: advType: 0x%02x", record_type); + break; + } + } + } +} +std::string ESPBTDevice::address_str() const { + char mac[24]; + snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", this->address_[0], this->address_[1], this->address_[2], + this->address_[3], this->address_[4], this->address_[5]); + return mac; +} +uint64_t ESPBTDevice::address_uint64() const { return ble_addr_to_uint64(this->address_); } + +void ESP32BLETracker::dump_config() { + const char *io_capability_s; + switch (global_io_cap) { + case ESP_IO_CAP_OUT: + io_capability_s = "display_only"; + break; + case ESP_IO_CAP_IO: + io_capability_s = "display_yes_no"; + break; + case ESP_IO_CAP_IN: + io_capability_s = "keyboard_only"; + break; + case ESP_IO_CAP_NONE: + io_capability_s = "none"; + break; + case ESP_IO_CAP_KBDISP: + io_capability_s = "keyboard_display"; + break; + default: + io_capability_s = "invalid"; + break; + } + ESP_LOGCONFIG(TAG, "BLE Tracker:"); + ESP_LOGCONFIG(TAG, " Scan Duration: %u s", this->scan_duration_); + ESP_LOGCONFIG(TAG, " Scan Interval: %.1f ms", this->scan_interval_ * 0.625f); + ESP_LOGCONFIG(TAG, " Scan Window: %.1f ms", this->scan_window_ * 0.625f); + ESP_LOGCONFIG(TAG, " Scan Type: %s", this->scan_active_ ? "ACTIVE" : "PASSIVE"); + ESP_LOGCONFIG(TAG, " Continuous Scanning: %s", this->scan_continuous_ ? "True" : "False"); + ESP_LOGCONFIG(TAG, " IO Capability: %s", io_capability_s); +} + +void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { + const uint64_t address = device.address_uint64(); + for (auto &disc : this->already_discovered_) { + if (disc == address) + return; + } + this->already_discovered_.push_back(address); + + ESP_LOGD(TAG, "Found device %s RSSI=%d", device.address_str().c_str(), device.get_rssi()); + + const char *address_type_s; + switch (device.get_address_type()) { + case BLE_ADDR_TYPE_PUBLIC: + address_type_s = "PUBLIC"; + break; + case BLE_ADDR_TYPE_RANDOM: + address_type_s = "RANDOM"; + break; + case BLE_ADDR_TYPE_RPA_PUBLIC: + address_type_s = "RPA_PUBLIC"; + break; + case BLE_ADDR_TYPE_RPA_RANDOM: + address_type_s = "RPA_RANDOM"; + break; + default: + address_type_s = "UNKNOWN"; + break; + } + + ESP_LOGD(TAG, " Address Type: %s", address_type_s); + if (!device.get_name().empty()) { + ESP_LOGD(TAG, " Name: '%s'", device.get_name().c_str()); + } + for (auto &tx_power : device.get_tx_powers()) { + ESP_LOGD(TAG, " TX Power: %d", tx_power); + } +} + +} // namespace esp32_ble_tracker +} // namespace esphome + +#endif diff --git a/components/esp32_ble_tracker/esp32_ble_tracker.h b/components/esp32_ble_tracker/esp32_ble_tracker.h new file mode 100644 index 0000000..862708a --- /dev/null +++ b/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -0,0 +1,288 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" +#include "queue.h" + +#include +#include +#include + +#ifdef USE_ESP32 + +#include +#include +#include + +namespace esphome { +namespace esp32_ble_tracker { + +// NOLINTNEXTLINE +extern esp_ble_io_cap_t global_io_cap; + +enum IoCapability { + IO_CAP_OUT = ESP_IO_CAP_OUT, + IO_CAP_IO = ESP_IO_CAP_IO, + IO_CAP_IN = ESP_IO_CAP_IN, + IO_CAP_NONE = ESP_IO_CAP_NONE, + IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, +}; + +class ESPBTUUID { + public: + ESPBTUUID(); + + static ESPBTUUID from_uint16(uint16_t uuid); + + static ESPBTUUID from_uint32(uint32_t uuid); + + static ESPBTUUID from_raw(const uint8_t *data); + + static ESPBTUUID from_raw(const std::string &data); + + static ESPBTUUID from_uuid(esp_bt_uuid_t uuid); + + ESPBTUUID as_128bit() const; + + bool contains(uint8_t data1, uint8_t data2) const; + + bool operator==(const ESPBTUUID &uuid) const; + bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } + + esp_bt_uuid_t get_uuid() const; + + std::string to_string() const; + + uint64_t get_128bit_high() const; + uint64_t get_128bit_low() const; + + protected: + esp_bt_uuid_t uuid_; +}; + +using adv_data_t = std::vector; + +struct ServiceData { + ESPBTUUID uuid; + adv_data_t data; +}; + +class ESPBLEiBeacon { + public: + ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); } + ESPBLEiBeacon(const uint8_t *data); + static optional from_manufacturer_data(const ServiceData &data); + + uint16_t get_major() { return ((this->beacon_data_.major & 0xFF) << 8) | (this->beacon_data_.major >> 8); } + uint16_t get_minor() { return ((this->beacon_data_.minor & 0xFF) << 8) | (this->beacon_data_.minor >> 8); } + int8_t get_signal_power() { return this->beacon_data_.signal_power; } + ESPBTUUID get_uuid() { return ESPBTUUID::from_raw(this->beacon_data_.proximity_uuid); } + + protected: + struct { + uint8_t sub_type; + uint8_t length; + uint8_t proximity_uuid[16]; + uint16_t major; + uint16_t minor; + int8_t signal_power; + } PACKED beacon_data_; +}; + +class ESPBTDevice { + public: + void parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); + + std::string address_str() const; + + uint64_t address_uint64() const; + + const uint8_t *address() const { return address_; } + + esp_ble_addr_type_t get_address_type() const { return this->address_type_; } + int get_rssi() const { return rssi_; } + const std::string &get_name() const { return this->name_; } + + const std::vector &get_tx_powers() const { return tx_powers_; } + + const optional &get_appearance() const { return appearance_; } + const optional &get_ad_flag() const { return ad_flag_; } + const std::vector &get_service_uuids() const { return service_uuids_; } + + const std::vector &get_manufacturer_datas() const { return manufacturer_datas_; } + + const std::vector &get_service_datas() const { return service_datas_; } + + const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &get_scan_result() const { return scan_result_; } + + optional get_ibeacon() const { + for (auto &it : this->manufacturer_datas_) { + auto res = ESPBLEiBeacon::from_manufacturer_data(it); + if (res.has_value()) + return *res; + } + return {}; + } + + protected: + void parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); + + esp_bd_addr_t address_{ + 0, + }; + esp_ble_addr_type_t address_type_{BLE_ADDR_TYPE_PUBLIC}; + int rssi_{0}; + std::string name_{}; + std::vector tx_powers_{}; + optional appearance_{}; + optional ad_flag_{}; + std::vector service_uuids_; + std::vector manufacturer_datas_{}; + std::vector service_datas_{}; + esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_{}; +}; + +class ESP32BLETracker; + +class ESPBTDeviceListener { + public: + virtual void on_scan_end() {} + virtual bool parse_device(const ESPBTDevice &device) = 0; + void set_parent(ESP32BLETracker *parent) { parent_ = parent; } + + protected: + ESP32BLETracker *parent_{nullptr}; +}; + +enum class ClientState { + // Connection is allocated + INIT, + // Client is disconnecting + DISCONNECTING, + // Connection is idle, no device detected. + IDLE, + // Searching for device. + SEARCHING, + // Device advertisement found. + DISCOVERED, + // Device is discovered and the scanner is stopped + READY_TO_CONNECT, + // Connection in progress. + CONNECTING, + // Initial connection established. + CONNECTED, + // The client and sub-clients have completed setup. + ESTABLISHED, +}; + +enum class ConnectionType { + // The default connection type, we hold all the services in ram + // for the duration of the connection. + V1, + // The client has a cache of the services and mtu so we should not + // fetch them again + V3_WITH_CACHE, + // The client does not need the services and mtu once we send them + // so we should wipe them from memory as soon as we send them + V3_WITHOUT_CACHE +}; + +class ESPBTClient : public ESPBTDeviceListener { + public: + virtual bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) = 0; + virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; + virtual void connect() = 0; + virtual void set_state(ClientState st) { this->state_ = st; } + ClientState state() const { return state_; } + int app_id; + + protected: + ClientState state_; +}; + +class ESP32BLETracker : public Component { + public: + void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } + void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; } + void set_scan_window(uint32_t scan_window) { scan_window_ = scan_window; } + void set_scan_active(bool scan_active) { scan_active_ = scan_active; } + void set_scan_continuous(bool scan_continuous) { scan_continuous_ = scan_continuous; } + void set_io_capability(IoCapability io_capability) { global_io_cap = (esp_ble_io_cap_t) io_capability; } + + /// Setup the FreeRTOS task and the Bluetooth stack. + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + void loop() override; + + void register_listener(ESPBTDeviceListener *listener) { + listener->set_parent(this); + this->listeners_.push_back(listener); + } + + void register_client(ESPBTClient *client); + + void print_bt_device_info(const ESPBTDevice &device); + + void start_scan(); + void stop_scan(); + + protected: + /// The FreeRTOS task managing the bluetooth interface. + static bool ble_setup(); + /// Start a single scan by setting up the parameters and doing some esp-idf calls. + void start_scan_(bool first); + /// Called when a scan ends + void end_of_scan_(); + /// Callback that will handle all GAP events and redistribute them to other callbacks. + static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received. + void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); + /// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received. + void gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m); + /// Called when a `ESP_GAP_BLE_SCAN_START_COMPLETE_EVT` event is received. + void gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m); + /// Called when a `ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT` event is received. + void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m); + + int app_id_; + /// Callback that will handle all GATTC events and redistribute them to other callbacks. + static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + void real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + + /// Vector of addresses that have already been printed in print_bt_device_info + std::vector already_discovered_; + std::vector listeners_; + /// Client parameters. + std::vector clients_; + /// A structure holding the ESP BLE scan parameters. + esp_ble_scan_params_t scan_params_; + /// The interval in seconds to perform scans. + uint32_t scan_duration_; + uint32_t scan_interval_; + uint32_t scan_window_; + uint8_t scan_start_fail_count_; + bool scan_continuous_; + bool scan_active_; + bool scanner_idle_; + SemaphoreHandle_t scan_result_lock_; + SemaphoreHandle_t scan_end_lock_; + size_t scan_result_index_{0}; + esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_buffer_[16]; + esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; + esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; + + Queue ble_events_; +}; + +// NOLINTNEXTLINE +extern ESP32BLETracker *global_esp32_ble_tracker; + +} // namespace esp32_ble_tracker +} // namespace esphome + +#endif diff --git a/components/esp32_ble_tracker/queue.h b/components/esp32_ble_tracker/queue.h new file mode 100644 index 0000000..f1dcc33 --- /dev/null +++ b/components/esp32_ble_tracker/queue.h @@ -0,0 +1,109 @@ +#pragma once + +#ifdef USE_ESP32 +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +/* + * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather + * than trying to deal with various locking strategies, all incoming GAP and GATT + * events will simply be placed on a semaphore guarded queue. The next time the + * component runs loop(), these events are popped off the queue and handed at + * this safer time. + */ + +namespace esphome { +namespace esp32_ble_tracker { + +template class Queue { + public: + Queue() { m_ = xSemaphoreCreateMutex(); } + + void push(T *element) { + if (element == nullptr) + return; + if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { + q_.push(element); + xSemaphoreGive(m_); + } + } + + T *pop() { + T *element = nullptr; + + if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { + if (!q_.empty()) { + element = q_.front(); + q_.pop(); + } + xSemaphoreGive(m_); + } + return element; + } + + protected: + std::queue q_; + SemaphoreHandle_t m_; +}; + +// Received GAP and GATTC events are only queued, and get processed in the main loop(). +// This class stores each event in a single type. +class BLEEvent { + public: + BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->event_.gap.gap_event = e; + memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); + this->type_ = 0; + }; + + BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->event_.gattc.gattc_event = e; + this->event_.gattc.gattc_if = i; + memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); + // Need to also make a copy of relevant event data. + switch (e) { + case ESP_GATTC_NOTIFY_EVT: + this->data.assign(p->notify.value, p->notify.value + p->notify.value_len); + this->event_.gattc.gattc_param.notify.value = this->data.data(); + break; + case ESP_GATTC_READ_CHAR_EVT: + case ESP_GATTC_READ_DESCR_EVT: + this->data.assign(p->read.value, p->read.value + p->read.value_len); + this->event_.gattc.gattc_param.read.value = this->data.data(); + break; + default: + break; + } + this->type_ = 1; + }; + + union { + struct gap_event { // NOLINT(readability-identifier-naming) + esp_gap_ble_cb_event_t gap_event; + esp_ble_gap_cb_param_t gap_param; + } gap; + + struct gattc_event { // NOLINT(readability-identifier-naming) + esp_gattc_cb_event_t gattc_event; + esp_gatt_if_t gattc_if; + esp_ble_gattc_cb_param_t gattc_param; + } gattc; + } event_; + std::vector data{}; + uint8_t type_; // 0=gap 1=gattc +}; + +} // namespace esp32_ble_tracker +} // namespace esphome + +#endif diff --git a/components/votronic_ble/__init__.py b/components/votronic_ble/__init__.py new file mode 100644 index 0000000..a49bd14 --- /dev/null +++ b/components/votronic_ble/__init__.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ble_client +from esphome.const import CONF_ID, CONF_THROTTLE + +AUTO_LOAD = ["sensor", "text_sensor"] +CODEOWNERS = ["@syssi"] +MULTI_CONF = True + +CONF_VOTRONIC_BLE_ID = "votronic_ble_id" +CONF_ENABLE_FAKE_TRAFFIC = "enable_fake_traffic" + +votronic_ble_ns = cg.esphome_ns.namespace("votronic_ble") +VotronicBle = votronic_ble_ns.class_( + "VotronicBle", ble_client.BLEClientNode, cg.PollingComponent +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(VotronicBle), + cv.Optional( + CONF_THROTTLE, default="2s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ENABLE_FAKE_TRAFFIC, default=False): cv.boolean, + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.polling_component_schema("2s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await ble_client.register_ble_node(var, config) + + cg.add(var.set_throttle(config[CONF_THROTTLE])) + cg.add(var.set_enable_fake_traffic(config[CONF_ENABLE_FAKE_TRAFFIC])) diff --git a/components/votronic_ble/sensor.py b/components/votronic_ble/sensor.py new file mode 100644 index 0000000..ccb76e1 --- /dev/null +++ b/components/votronic_ble/sensor.py @@ -0,0 +1,71 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_CURRENT, + CONF_POWER, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + ICON_EMPTY, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_VOLT, + UNIT_WATT, +) + +from . import CONF_VOTRONIC_BLE_ID, VotronicBle + +DEPENDENCIES = ["votronic_ble"] + +CODEOWNERS = ["@syssi"] + +CONF_TOTAL_VOLTAGE = "total_voltage" + +ICON_CURRENT_DC = "mdi:current-dc" + +UNIT_SECONDS = "s" +UNIT_HOURS = "h" +UNIT_AMPERE_HOURS = "Ah" + +SENSORS = [ + CONF_TOTAL_VOLTAGE, + CONF_CURRENT, + CONF_POWER, +] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_VOTRONIC_BLE_ID): cv.use_id(VotronicBle), + cv.Optional(CONF_TOTAL_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + icon=ICON_EMPTY, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + icon=ICON_CURRENT_DC, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + icon=ICON_EMPTY, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_VOTRONIC_BLE_ID]) + for key in SENSORS: + if key in config: + conf = config[key] + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) diff --git a/components/votronic_ble/text_sensor.py b/components/votronic_ble/text_sensor.py new file mode 100644 index 0000000..50c99d2 --- /dev/null +++ b/components/votronic_ble/text_sensor.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_ICON, CONF_ID + +from . import CONF_VOTRONIC_BLE_ID, VotronicBle + +DEPENDENCIES = ["votronic_ble"] + +CODEOWNERS = ["@syssi"] + +CONF_ERRORS = "errors" +CONF_OPERATION_STATUS = "operation_status" + +ICON_ERRORS = "mdi:alert-circle-outline" +ICON_OPERATION_STATUS = "mdi:heart-pulse" + +TEXT_SENSORS = [ + CONF_ERRORS, + CONF_OPERATION_STATUS, +] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_VOTRONIC_BLE_ID): cv.use_id(VotronicBle), + cv.Optional(CONF_ERRORS): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + cv.Optional(CONF_ICON, default=ICON_ERRORS): cv.icon, + } + ), + cv.Optional(CONF_OPERATION_STATUS): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + cv.Optional(CONF_ICON, default=ICON_OPERATION_STATUS): cv.icon, + } + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_VOTRONIC_BLE_ID]) + for key in TEXT_SENSORS: + if key in config: + conf = config[key] + sens = cg.new_Pvariable(conf[CONF_ID]) + await text_sensor.register_text_sensor(sens, conf) + cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) diff --git a/components/votronic_ble/votronic_ble.cpp b/components/votronic_ble/votronic_ble.cpp new file mode 100644 index 0000000..0fe612d --- /dev/null +++ b/components/votronic_ble/votronic_ble.cpp @@ -0,0 +1,217 @@ +#include "votronic_ble.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace votronic_ble { + +static const char *const TAG = "votronic_ble"; + +void VotronicBle::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + break; + } + case ESP_GATTC_CONNECT_EVT: { + ESP_LOGD(TAG, "ESP_GATTC_CONNECT_EVT"); + esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT_MITM); + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + this->node_state = espbt::ClientState::IDLE; + + // this->publish_state_(this->voltage_sensor_, NAN); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto *char_battery = this->parent_->get_characteristic(this->service_monitoring_uuid_, this->char_battery_uuid_); + if (char_battery == nullptr) { + ESP_LOGW(TAG, "[%s] No battery characteristic found at device, no battery computer attached?", + this->parent_->address_str().c_str()); + break; + } + this->char_battery_handle_ = char_battery->handle; + + auto status = esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), this->parent()->get_remote_bda(), + char_battery->handle); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); + } + + auto *char_photovoltaic = + this->parent_->get_characteristic(this->service_monitoring_uuid_, this->char_photovoltaic_uuid_); + if (char_photovoltaic == nullptr) { + ESP_LOGW(TAG, "[%s] No Photovoltaic characteristic found at device, no Photovoltaic solar charger attached?", + this->parent_->address_str().c_str()); + break; + } + this->char_photovoltaic_handle_ = char_photovoltaic->handle; + + auto status2 = esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), this->parent()->get_remote_bda(), + char_photovoltaic->handle); + if (status2) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status2); + } + + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle != this->char_battery_handle_ && + param->notify.handle != this->char_photovoltaic_handle_) { + ESP_LOGD(TAG, "Notification skipped (handle %d): %s", param->notify.handle, + format_hex_pretty(param->notify.value, param->notify.value_len).c_str()); + break; + } + + ESP_LOGVV(TAG, "Notification received (handle %d): %s", param->notify.handle, + format_hex_pretty(param->notify.value, param->notify.value_len).c_str()); + + std::vector data(param->notify.value, param->notify.value + param->notify.value_len); + + this->on_votronic_ble_data_(param->notify.handle, data); + break; + } + default: + break; + } +} + +void VotronicBle::update() { + if (this->enable_fake_traffic_) { + this->char_photovoltaic_handle_ = 0x25; + this->char_battery_handle_ = 0x22; + + // Photovoltaic status frame + this->on_votronic_ble_data_(0x25, {0xE8, 0x04, 0x76, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x56, 0x00, 0x09, + 0x18, 0x00, 0x22, 0x00, 0x00, 0x00}); + + // Battery status frame + this->on_votronic_ble_data_(0x22, {0xE8, 0x04, 0xBF, 0x04, 0x09, 0x01, 0x60, 0x00, 0x5F, 0x00, + 0x9A, 0xFE, 0xFF, 0xF0, 0x0A, 0x5E, 0x14, 0x54, 0x02, 0x04}); + } + + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Not connected", this->parent_->address_str().c_str()); + return; + } +} + +void VotronicBle::on_votronic_ble_data_(const uint8_t &handle, const std::vector &data) { + if (handle == this->char_photovoltaic_handle_) { + this->decode_photovoltaic_data_(data); + return; + } + + if (handle == this->char_battery_handle_) { + this->decode_battery_data_(data); + return; + } + + ESP_LOGW(TAG, "Unhandled response received: %s", format_hex_pretty(&data.front(), data.size()).c_str()); +} + +void VotronicBle::decode_photovoltaic_data_(const std::vector &data) { + if (data.size() != 19) { + ESP_LOGW(TAG, "Invalid response size: %zu", data.size()); + return; + } + + const uint32_t now = millis(); + if (now - this->last_photovoltaic_info_ < this->throttle_) { + return; + } + this->last_photovoltaic_info_ = now; + + auto votronic_get_16bit = [&](size_t i) -> uint16_t { + return (uint16_t(data[i + 1]) << 8) | (uint16_t(data[i + 0]) << 0); + }; + + ESP_LOGI(TAG, "Photovoltaic data frame received"); + ESP_LOGI(TAG, " Primary battery voltage: %f V", votronic_get_16bit(0) * 0.01f); + ESP_LOGI(TAG, " PV voltage: %f V", votronic_get_16bit(2) * 0.01f); + ESP_LOGI(TAG, " PV current: %f A", votronic_get_16bit(4) * 0.01f); + ESP_LOGD(TAG, " Unknown (Byte 6): %d (0x%02X)", data[6], data[6]); + ESP_LOGD(TAG, " Unknown (Byte 7): %d (0x%02X)", data[7], data[7]); + ESP_LOGI(TAG, " Battery status bitmask: %d (0x%02X)", data[8], data[8]); + ESP_LOGI(TAG, " Controller status bitmask: %d (0?: Active, 1?: Standby, 2?: Reduce)", data[9]); + ESP_LOGD(TAG, " Unknown (Byte 10): %d (0x%02X)", data[10], data[10]); + ESP_LOGD(TAG, " Unknown (Byte 11): %d (0x%02X)", data[11], data[11]); + // Bit0: Standby + // Bit1: Active + // Bit2: Reduce + ESP_LOGD(TAG, " Controller status bitmask? (Byte 12): %d (0x%02X) (9: Active, 25?: Standby, 2?: Reduce)", data[12], + data[12]); + ESP_LOGI(TAG, " Charged capacity: %d Ah", votronic_get_16bit(13)); + ESP_LOGI(TAG, " Charged energy: %d Wh", votronic_get_16bit(15) * 10); + ESP_LOGD(TAG, " PV power? (Byte 16): %d W? (0x%02X)", data[16], data[16]); + ESP_LOGD(TAG, " PV power? (Byte 17): %d W? (0x%02X)", data[17], data[17]); + ESP_LOGD(TAG, " PV power? (Byte 18): %d W? (0x%02X)", data[18], data[18]); +} + +void VotronicBle::decode_battery_data_(const std::vector &data) { + if (data.size() != 20) { + ESP_LOGW(TAG, "Invalid response size: %zu", data.size()); + return; + } + + const uint32_t now = millis(); + if (now - this->last_battery_info_ < this->throttle_) { + return; + } + this->last_battery_info_ = now; + + this->last_photovoltaic_info_ = now; + auto votronic_get_16bit = [&](size_t i) -> uint16_t { + return (uint16_t(data[i + 1]) << 8) | (uint16_t(data[i + 0]) << 0); + }; + + ESP_LOGI(TAG, "Battery data frame received"); + ESP_LOGI(TAG, " Primary battery voltage: %f V", votronic_get_16bit(0) * 0.01f); + ESP_LOGI(TAG, " Secondary battery voltage: %f V", votronic_get_16bit(2) * 0.01f); + ESP_LOGI(TAG, " Primary battery capacity: %d Ah", votronic_get_16bit(4)); + ESP_LOGD(TAG, " Unknown (Byte 6): %d (0x%02X)", data[6], data[6]); + ESP_LOGD(TAG, " Unknown (Byte 7): %d (0x%02X)", data[7], data[7]); + ESP_LOGI(TAG, " State of charge: %d %%", data[8]); + ESP_LOGD(TAG, " Unknown (Byte 9): %d (0x%02X)", data[9], data[9]); + ESP_LOGI(TAG, " Current: %f A", (float) ((int16_t) votronic_get_16bit(10)) * 0.001f); + ESP_LOGD(TAG, " Unknown (Byte 12): %d (0x%02X)", data[12], data[12]); + ESP_LOGI(TAG, " Primary battery nominal capacity: %f Ah", votronic_get_16bit(13) * 0.1f); + ESP_LOGD(TAG, " Unknown (Byte 15): %d (0x%02X)", data[15], data[15]); + ESP_LOGD(TAG, " Unknown (Byte 16-17): %d (0x%02X 0x%02X)", votronic_get_16bit(16), data[16], data[17]); + ESP_LOGD(TAG, " Unknown (Byte 18-19): %d (0x%02X 0x%02X)", votronic_get_16bit(18), data[18], data[19]); +} + +void VotronicBle::dump_config() { + ESP_LOGCONFIG(TAG, "VotronicBle:"); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent_->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Monitoring Service UUID : %s", this->service_monitoring_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Battery Characteristic UUID : %s", this->char_battery_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Photovoltaic Characteristic UUID : %s", this->char_photovoltaic_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Fake traffic enabled: %s", YESNO(this->enable_fake_traffic_)); + + LOG_SENSOR("", "Total voltage", this->total_voltage_sensor_); + LOG_SENSOR("", "Current", this->current_sensor_); + LOG_SENSOR("", "Power", this->power_sensor_); +} + +void VotronicBle::publish_state_(sensor::Sensor *sensor, float value) { + if (sensor == nullptr) + return; + + sensor->publish_state(value); +} + +void VotronicBle::publish_state_(text_sensor::TextSensor *text_sensor, const std::string &state) { + if (text_sensor == nullptr) + return; + + text_sensor->publish_state(state); +} + +} // namespace votronic_ble +} // namespace esphome diff --git a/components/votronic_ble/votronic_ble.h b/components/votronic_ble/votronic_ble.h new file mode 100644 index 0000000..1dbb014 --- /dev/null +++ b/components/votronic_ble/votronic_ble.h @@ -0,0 +1,80 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace votronic_ble { + +namespace espbt = esphome::esp32_ble_tracker; + +class VotronicBle : public esphome::ble_client::BLEClientNode, public PollingComponent { + public: + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + void update() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_total_voltage_sensor(sensor::Sensor *total_voltage_sensor) { total_voltage_sensor_ = total_voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + + void set_operation_status_text_sensor(text_sensor::TextSensor *operation_status_text_sensor) { + operation_status_text_sensor_ = operation_status_text_sensor; + } + void set_errors_text_sensor(text_sensor::TextSensor *errors_text_sensor) { errors_text_sensor_ = errors_text_sensor; } + void set_throttle(uint16_t throttle) { this->throttle_ = throttle; } + void set_enable_fake_traffic(bool enable_fake_traffic) { enable_fake_traffic_ = enable_fake_traffic; } + + protected: + sensor::Sensor *total_voltage_sensor_; + sensor::Sensor *current_sensor_; + sensor::Sensor *power_sensor_; + + text_sensor::TextSensor *operation_status_text_sensor_; + text_sensor::TextSensor *errors_text_sensor_; + + uint16_t char_battery_handle_; + uint16_t char_photovoltaic_handle_; + uint32_t last_battery_info_{0}; + uint32_t last_photovoltaic_info_{0}; + uint16_t throttle_; + bool enable_fake_traffic_; + + esp32_ble_tracker::ESPBTUUID service_bond_uuid_ = + esp32_ble_tracker::ESPBTUUID::from_raw("70521e61-022d-f899-d046-4885a76acbd0"); + esp32_ble_tracker::ESPBTUUID service_monitoring_uuid_ = + esp32_ble_tracker::ESPBTUUID::from_raw("d0cb6aa7-8548-46d0-99f8-2d02611e5270"); + esp32_ble_tracker::ESPBTUUID service_log_data_uuid_ = + esp32_ble_tracker::ESPBTUUID::from_raw("ae64a924-1184-4554-8bbc-295db9f2324a"); + + esp32_ble_tracker::ESPBTUUID char_battery_uuid_ = + esp32_ble_tracker::ESPBTUUID::from_raw("9a082a4e-5bcc-4b1d-9958-a97cfccfa5ec"); + esp32_ble_tracker::ESPBTUUID char_photovoltaic_uuid_ = + esp32_ble_tracker::ESPBTUUID::from_raw("971ccec2-521d-42fd-b570-cf46fe5ceb65"); + + esp32_ble_tracker::ESPBTUUID char_management_uuid_ = + esp32_ble_tracker::ESPBTUUID::from_raw("ac12f485-cab7-4e0a-aac5-3585918852f6"); + esp32_ble_tracker::ESPBTUUID char_bulk_data_uuid_ = + esp32_ble_tracker::ESPBTUUID::from_raw("b8a37ffe-c57b-4007-b3c1-ca05a6b7f0c6"); + + void on_votronic_ble_data_(const uint8_t &handle, const std::vector &data); + void decode_photovoltaic_data_(const std::vector &data); + void decode_battery_data_(const std::vector &data); + void publish_state_(sensor::Sensor *sensor, float value); + void publish_state_(text_sensor::TextSensor *text_sensor, const std::string &state); +}; + +} // namespace votronic_ble +} // namespace esphome + +#endif diff --git a/docs/pdus/battery.txt b/docs/pdus/battery.txt new file mode 100644 index 0000000..b653512 --- /dev/null +++ b/docs/pdus/battery.txt @@ -0,0 +1,170 @@ +E8.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +E6.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +E6.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +ED.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +ED.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.CA.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.CA.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BD.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BD.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BD.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BD.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BD.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BD.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E6.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E6.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +ED.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +ED.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E6.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E6.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.CA.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.CA.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.C3.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.C3.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +E6.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +E6.04.BF.04.09.01.60.00.5F.00.82.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.C3.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.C3.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.CA.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.CA.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.BF.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.C3.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +EA.04.C3.04.09.01.60.00.5F.00.B2.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 +E8.04.BF.04.09.01.60.00.5F.00.9A.FE.FF.F0.0A.5E.14.54.02.04 diff --git a/docs/pdus/mppt.txt b/docs/pdus/mppt.txt new file mode 100644 index 0000000..f5c9717 --- /dev/null +++ b/docs/pdus/mppt.txt @@ -0,0 +1,79 @@ +E8.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +E8.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.78.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +ED.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +ED.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +ED.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +E8.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +E8.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +E8.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.78.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +ED.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +E8.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +E6.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +E6.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +E8.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.78.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.78.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.76.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 +EA.04.74.05.00.00.00.00.00.06.56.00.09.18.00.22.00.00.00 diff --git a/esp32-ble-example-debug.yaml b/esp32-ble-example-debug.yaml new file mode 100644 index 0000000..31eb06b --- /dev/null +++ b/esp32-ble-example-debug.yaml @@ -0,0 +1,13 @@ +<<: !include esp32-ble-example.yaml + +logger: + level: VERY_VERBOSE + logs: + component: DEBUG + scheduler: INFO + mqtt: INFO + mqtt.idf: INFO + mqtt.component: INFO + mqtt.sensor: INFO + mqtt.switch: INFO + esp32_ble_tracker: DEBUG diff --git a/esp32-ble-example-faker.yaml b/esp32-ble-example-faker.yaml new file mode 100644 index 0000000..dca5855 --- /dev/null +++ b/esp32-ble-example-faker.yaml @@ -0,0 +1,7 @@ +<<: !include esp32-ble-example-debug.yaml + +votronic_ble: + - ble_client_id: client0 + id: votronic0 + update_interval: 1s + enable_fake_traffic: true diff --git a/esp32-ble-example.yaml b/esp32-ble-example.yaml new file mode 100644 index 0000000..998d27c --- /dev/null +++ b/esp32-ble-example.yaml @@ -0,0 +1,69 @@ +substitutions: + name: votronic + device_description: "Monitor a votronic device via BLE" + external_components_source: github://syssi/esphome-votronic@main + mac_address: 60:A4:23:91:8F:55 + +esphome: + name: ${name} + comment: ${device_description} + project: + name: "syssi.esphome-votronic" + version: 1.0.0 + +esp32: + board: wemos_d1_mini32 + framework: + type: esp-idf + version: latest + +external_components: + - source: ${external_components_source} + refresh: 0s + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + +ota: +logger: + +# If you don't use Home Assistant please remove this `api` section and uncomment the `mqtt` component! +api: + +# The MQTT component is ESP-IDF compatible since ESPHome version 2022.4.0. If +# ESPHome suggests to use the arduino framework instead because of missing ESP-IDF +# framework support you should update your setup. +# mqtt: +# broker: !secret mqtt_host +# username: !secret mqtt_username +# password: !secret mqtt_password +# id: mqtt_client + +esp32_ble_tracker: + io_capability: keyboard_only + +ble_client: + - mac_address: ${mac_address} + id: client0 + pin_code: 173928 + +votronic_ble: + - ble_client_id: client0 + id: votronic0 + throttle: 5s + +sensor: + - platform: votronic_ble + votronic_ble_id: votronic0 + total_voltage: + name: "${name} total voltage" + current: + name: "${name} current" + power: + name: "${name} power" + +switch: + - platform: ble_client + ble_client_id: client0 + name: "${name} enable bluetooth connection" diff --git a/test-esp32.sh b/test-esp32.sh new file mode 100755 index 0000000..cf30521 --- /dev/null +++ b/test-esp32.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +esphome -s external_components_source components ${1:-run} ${2:-esp32-ble-example-faker.yaml}